fix(family): the owed task is tagged by the matching System only, and the ground fixtures build without duplicate keys (#4990)
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 1m4s
CI & Build / Python tests (push) Successful in 1m57s
CI & Build / Build & push image (push) Successful in 44s

assess_family_adoption took system_ids for the task it files, which made
the family tool module look like a System-tagging door and tripped the
parity registry. It is not one: the owed task is tagged to the project's
System matching the idea's canonical area, and update_task retags it.
The unit test for each conflict ground passed evidence/conditions twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 11:11:41 -04:00
co-authored by Claude Opus 5.5
parent 201e09901b
commit 233fa3eea9
3 changed files with 12 additions and 14 deletions
+5 -5
View File
@@ -253,7 +253,6 @@ async def assess_family_adoption(
reason: str,
evidence: list[str] | None = None,
precedent_ids: list[int] | None = None,
system_ids: list[int] | None = None,
) -> dict:
"""Answer one canon family idea for one project. YOU decide; nobody
approves. Judge in this order and stop at the first that holds:
@@ -268,8 +267,10 @@ async def assess_family_adoption(
3. adopted — it applies and the project does it. `evidence` names where
(a file, a commit, a task, a CI run).
4. owed — none of the above. A task is filed in THIS project naming the
gap and the reference implementation for its language. Nothing edits
another repository; the project picks the task up itself.
gap and the reference implementation for its language, filed under
the project's System matching the idea's area (retag it with
update_task if that guess is wrong). Nothing edits another
repository; the project picks the task up itself.
Read get_family_adoption first: answer consistently with its precedents
unless this project differs in a way you can name in `reason`. The
@@ -286,11 +287,10 @@ async def assess_family_adoption(
reason: why — required for every outcome.
evidence: where it is done (required for adopted), or what you checked.
precedent_ids: earlier family decisions you followed, if any.
system_ids: Systems for an owed task. Omit to match the idea's own.
"""
return await adoption_svc.assess(
current_user_id(), project_id, idea_id, outcome=outcome, reason=reason,
evidence=evidence, precedent_ids=precedent_ids, system_ids=system_ids,
evidence=evidence, precedent_ids=precedent_ids,
)
+6 -8
View File
@@ -519,8 +519,7 @@ async def _matching_systems(session, project_id: int, note_ids: list[int]) -> li
return list(dict.fromkeys(direct + list(mapped)))
async def _file_owed_task(user_id: int, row_id: int, reason: str,
system_ids: list[int] | None) -> Note:
async def _file_owed_task(user_id: int, row_id: int, reason: str) -> Note:
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
@@ -531,7 +530,7 @@ async def _file_owed_task(user_id: int, row_id: int, reason: str,
refs = await _references(session, row.idea_id)
langs = await _project_languages(session, row.project_id)
platforms = await family_svc._platform_slugs(session, row.idea_id)
systems = system_ids or await _matching_systems(
systems = await _matching_systems(
session, row.project_id, [row.idea_id] + [r["id"] for r in refs])
project_id, idea_id, version = row.project_id, row.idea_id, idea.canon_version
body = (
@@ -568,8 +567,7 @@ async def _set_task_status(user_id: int, task: Note, status: str, log: str) -> N
await task_logs.create_log(user_id, task.id, log)
async def _sync_owed_task(user_id: int, row_id: int, *, reason: str,
system_ids: list[int] | None = None) -> dict | None:
async def _sync_owed_task(user_id: int, row_id: int, *, reason: str) -> dict | None:
"""Make the owed task agree with the answer. Idempotent: an `owed` row
with an open task, or a settled row with a closed one, is left alone.
A task the caller cannot write is left alone too, and said so."""
@@ -594,7 +592,7 @@ async def _sync_owed_task(user_id: int, row_id: int, *, reason: str,
user_id, task, TaskStatus.todo.value,
f"Reopened: family idea #{idea_id} was assessed owed again — {reason}")
return ref(task, TaskStatus.todo.value, "reopened")
task = await _file_owed_task(user_id, row_id, reason, system_ids)
task = await _file_owed_task(user_id, row_id, reason)
async with async_session() as session:
row = await session.get(FamilyAdoption, row_id)
row.owed_task_id = task.id
@@ -645,7 +643,7 @@ def _answer(row: FamilyAdoption, *, status: str, reason: str, version: int,
async def assess(
user_id: int, project_id: int, idea_id: int, *, outcome: str, reason: str,
evidence: list[str] | None = None, precedent_ids: list[int] | None = None,
system_ids: list[int] | None = None, decided_via: str = "agent",
decided_via: str = "agent",
) -> dict:
"""Record one project's answer to one canon idea.
@@ -692,7 +690,7 @@ async def assess(
if decision is not None:
await session.refresh(decision)
row_id = row.id
task = await _sync_owed_task(user_id, row_id, reason=reason, system_ids=system_ids)
task = await _sync_owed_task(user_id, row_id, reason=reason)
rows = await list_adoptions(user_id, project_id=project_id, idea_id=idea_id)
return {
"changed": changed,
+1 -1
View File
@@ -62,7 +62,7 @@ GOOD = {
@pytest.mark.parametrize("ground", CONFLICT_KEYS)
def test_each_ground_holds_when_its_needs_are_met(ground):
kw = dict(evidence=None, conditions=None, **{**GOOD[ground]})
kw = {"evidence": None, "conditions": None, **GOOD[ground]}
assert conflict_problems(ground=ground, grounds_checked=_checked(ground),
fold="their reasoning", **kw) == []