feat(moments): rules mount on moments, through every rule door (milestone 458 step 3, #4921)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m0s
CI & Build / Python tests (push) Successful in 1m51s
CI & Build / Build & push image (push) Successful in 29s

rule_moments (migration 0117) records which moments a rule arrives at, by
catalog name, cascading with the rule. rule_detail, the one seam every
rule door already returns through, gains moments beside system_ids: None
leaves the mounts alone, a list replaces them. get_rule and both
list_rules doors read them back, batched per page.

All five MCP rule/preference writes and the three REST ones take moments
and validate them before their create or update. An unknown name is
refused with the catalog listed and leaves no half-made rule behind; a
parity test pins that ordering on every door.

Backup v22 carries the mounts as a join table remapped through the rule
map; a real-Postgres round trip checks they land on the restored rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:03:51 -04:00
co-authored by Claude Opus 5.5
parent cf3de5bae1
commit cc26054437
13 changed files with 560 additions and 26 deletions
+17
View File
@@ -132,3 +132,20 @@ def test_both_doors_read_one_catalog():
assert routes.moments_svc is moments
assert tool.actions_svc is moment_actions
assert routes.moment_actions_svc is moment_actions
def test_a_rules_moments_are_normalised_and_deduplicated():
assert moments.require_moments([" Work.Finish", "reply.report", "work.finish"]) == [
"work.finish", "reply.report",
]
assert moments.require_moments("work.finish") == ["work.finish"]
assert moments.require_moments([]) == []
def test_absent_moments_mean_leave_them_alone():
assert moments.require_moments(None) is None
def test_one_unknown_moment_refuses_the_whole_set():
with pytest.raises(ValueError, match="work.finished"):
moments.require_moments(["work.finish", "work.finished"])