fix(family): a decision read as precedent says its reason once - to_brief for precedents and idea history; the decision log keeps the full row (#5131)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m13s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 40s

Each decision stored its reason three times (reason, before.reason, after.reason)
and a promotion carried the whole criteria reasoning in evidence. Precedents
(assess, promote, get_family_adoption, get_family_idea) and get_family_idea's
history now serve FamilyDecision.to_brief(): reason once, before/after as status,
canon_version and platforms, evidence as the cited list. list_family_decisions
and the decision echoed back by a write keep to_dict. The precedent count was
already bounded at five.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 18:30:48 -04:00
co-authored by Claude Opus 5.5
parent 07169e1274
commit f006f15058
6 changed files with 88 additions and 6 deletions
+44
View File
@@ -67,3 +67,47 @@ def test_a_canon_ideas_scope_lives_on_the_idea_and_nowhere_else():
assert "topic_id" in family.FamilyIdea.__table__.c
assert not {c.name for c in RulebookTopic.__table__.c} & {"platform_id", "platform_ids"}
# --- a decision read as precedent (#5131) ------------------------------------------
def _assessment():
reason = "the APK is built in CI and signed with the one release key"
return family.FamilyDecision(
id=7, idea_id=3, project_id=5, action="assess", reason=reason,
before={"status": "unassessed", "reason": "", "canon_version": None},
after={"status": "adopted", "reason": reason, "canon_version": 2},
evidence={"evidence": ["ci.yml::android-release"], "source": "shapes"},
precedent_ids=[4, 6], decided_via="agent", user_id=1,
)
def test_a_brief_carries_the_reason_once():
brief = _assessment().to_brief()
assert brief["reason"].startswith("the APK is built")
assert brief["before"] == {"status": "unassessed", "canon_version": None}
assert brief["after"] == {"status": "adopted", "canon_version": 2}
# Nothing in it says the reason a second time.
assert repr(brief).count("the APK is built") == 1
def test_a_brief_keeps_the_cited_evidence_and_drops_the_rest():
promotion = family.FamilyDecision(
id=8, idea_id=3, project_id=None, action="promote", reason="proven twice",
before=None,
after={"status": "canon", "applies_when": "an app that ships its own APK",
"canon_version": 1, "platforms": ["android-app"]},
evidence={"criteria": {"proven": "a long paragraph"}, "evidence": ["#12"],
"ledger_rows_opened": 3},
)
brief = promotion.to_brief()
assert brief["evidence"] == ["#12"]
assert brief["before"] is None
assert brief["after"] == {"status": "canon", "canon_version": 1,
"platforms": ["android-app"]}
def test_the_full_row_is_still_the_audit_shape():
full = _assessment().to_dict()
assert full["after"]["reason"] == full["reason"]
assert full["evidence"]["source"] == "shapes" and full["precedent_ids"] == [4, 6]
@@ -236,6 +236,9 @@ async def test_another_projects_answer_is_recorded_as_precedent(family):
assert first["decision"]["id"] in second["decision"]["precedent_ids"]
assert second["precedents"][0]["relation"] == "same idea, another project"
assert second["precedents"][0]["project_title"] == "android one"
# A precedent says its reason once: before/after are state only (#5131).
assert second["precedents"][0]["reason"] == "the source app"
assert "reason" not in second["precedents"][0]["after"]
# --- recheck --------------------------------------------------------------------------