proactive-gate 0.7.2__tar.gz → 0.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/PKG-INFO +1 -1
  2. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/pyproject.toml +1 -1
  3. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/__init__.py +1 -1
  4. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/explain.py +12 -3
  5. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/test_explain.py +21 -3
  6. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/.gitignore +0 -0
  7. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/README.md +0 -0
  8. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/checks.py +0 -0
  9. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/clock.py +0 -0
  10. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/conformance.py +0 -0
  11. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/gate.py +0 -0
  12. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/policy.py +0 -0
  13. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/presets.py +0 -0
  14. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/py.typed +0 -0
  15. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/stores.py +0 -0
  16. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/src/proactive_gate/types.py +0 -0
  17. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/conftest.py +0 -0
  18. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/test_clock.py +0 -0
  19. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/test_conformance.py +0 -0
  20. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/test_stores.py +0 -0
  21. {proactive_gate-0.7.2 → proactive_gate-0.7.3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: proactive-gate
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Decide whether a proactive assistant may speak now: ordered checks, budgets, quiet hours, JSON policies, shared conformance fixtures with the TypeScript package.
5
5
  Project-URL: Homepage, https://bubblegunn.github.io/proactive-gate/
6
6
  Project-URL: Documentation, https://bubblegunn.github.io/proactive-gate/start/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "proactive-gate"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "Decide whether a proactive assistant may speak now: ordered checks, budgets, quiet hours, JSON policies, shared conformance fixtures with the TypeScript package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -22,7 +22,7 @@ from .types import (
22
22
  UserState,
23
23
  )
24
24
 
25
- __version__ = "0.7.2"
25
+ __version__ = "0.7.3"
26
26
 
27
27
  __all__ = [
28
28
  "EN", "PRIORITY_RANK", "KNOWN_CHECKS", "AsyncGate", "AsyncMemoryStore", "AsyncSqliteStore", "AsyncStore", "Candidate", "Check",
@@ -501,10 +501,19 @@ def _summarize(decision: Decision, sentences: Mapping[str, SentenceTemplate]) ->
501
501
  clause = _t(sentences, template, facts)
502
502
  # For a deferral the hold ends at retryAt; when the clause already names the
503
503
  # instant (snooze's reason is "snoozed until X"), naming it twice reads worse.
504
+ #
505
+ # The summary may headline an instant ONLY when the decision will actually be
506
+ # reconsidered at it. quietHours can only reject, and its facts carry the
507
+ # window end as holdUntil, so this used to render a reject as "held until
508
+ # 08:00" -- telling the reader the message goes out at 08:00 when nothing
509
+ # would ever send it. The window is not lost: the clause still names it.
504
510
  clause_says_when = "until" in facts
505
- until = facts.get("holdUntil")
506
- if until is None and not clause_says_when and decision.deferred_by and decision.retry_at is not None:
507
- until = iso_z(decision.retry_at)
511
+ will_be_reconsidered = entry.outcome == "defer"
512
+ until = None
513
+ if will_be_reconsidered:
514
+ until = facts.get("holdUntil")
515
+ if until is None and not clause_says_when and decision.retry_at is not None:
516
+ until = iso_z(decision.retry_at)
508
517
  held_facts: Facts = {"clause": clause}
509
518
  if until is not None:
510
519
  held_facts["until"] = until
@@ -63,7 +63,21 @@ def test_plain_sentence_and_machine_reason_are_both_on_hand() -> None:
63
63
  d = evaluate([checks.QuietHours()], now=NIGHT)
64
64
  e = explain(d)
65
65
  assert d.reason == "quiet hours 22:00 to 08:00 Europe/Istanbul; priority normal is below the floor (critical)"
66
- assert e.summary == "Held until 08:00 because the user's quiet hours run 22:00 to 08:00 Europe/Istanbul and normal priority is below the critical floor needed to override them."
66
+ # Not "Held until 08:00": QuietHours only rejects, and nothing retries a
67
+ # reject. The expectation was changed because it pinned a defect, not
68
+ # because a test failed. See test_a_reject_never_names_an_instant below.
69
+ assert e.summary == "Held because the user's quiet hours run 22:00 to 08:00 Europe/Istanbul and normal priority is below the critical floor needed to override them."
70
+
71
+
72
+ def test_a_reject_never_names_an_instant() -> None:
73
+ """The summary may headline a time only when the decision is reconsidered at it."""
74
+ d = evaluate([checks.QuietHours()], now=NIGHT)
75
+ assert d.allowed is False
76
+ assert d.retry_at is None, "the fixture must be a reject, or this test proves nothing"
77
+ assert d.deferred_by is None
78
+ summary = explain(d).summary
79
+ assert "until" not in summary, f"a reject must not name an instant: {summary}"
80
+ assert "quiet hours run 22:00 to 08:00" in summary
67
81
 
68
82
 
69
83
  def test_not_stopped_every_check_gets_a_sentence() -> None:
@@ -353,9 +367,13 @@ def test_language_is_a_parameter() -> None:
353
367
  assert explain(d).summary == explain(d, language="en").summary
354
368
  with pytest.raises(ValueError, match='no sentence catalog for language "tr"'):
355
369
  explain(d, language="tr")
356
- tr = {"summary.held": lambda f: f"{f['until']} kadar bekletildi, çünkü {f['clause']}"}
370
+ # `until` is optional: the summary carries it only when the decision will be
371
+ # reconsidered at it, so a caller catalog must handle its absence the way the
372
+ # shipped English one does. This template used to index it directly and only
373
+ # worked because the fixture happened to be a reject that wrongly named a time.
374
+ tr = {"summary.held": lambda f: f"{f['until'] + ' kadar ' if f.get('until') else ''}bekletildi, çünkü {f['clause']}"}
357
375
  e = explain(d, language="tr", catalogs={"tr": tr})
358
- assert e.summary.startswith("08:00 kadar bekletildi, çünkü")
376
+ assert e.summary.startswith("Bekletildi, çünkü")
359
377
  assert len(EN) > 0
360
378
 
361
379
 
File without changes