feat(family): family canon reaches the session - entry readout, retrieval reach, skill, report cue (milestone 463 step 6, #4992)
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Failing after 1m53s
CI & Build / Python tests (push) Successful in 2m39s
CI & Build / Build & push image (push) Skipped
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Failing after 1m53s
CI & Build / Python tests (push) Successful in 2m39s
CI & Build / Build & push image (push) Skipped
- enter_project carries a `family` key, but only when the project has something to answer: counts of unassessed, owed and to-recheck answers, each with the list_family_adoptions call that lists it. It shows on every entry, never by platform touch: entry is when work is chosen, and an unanswered idea is otherwise invisible. - Retrieval: a widened project search (include_global_kinds) now also reaches the canon ideas on the project's platforms. It also reaches their references in the project's languages, or all of them when none matches. An off-platform project gets none, and the plain project filter (the duplicate gate) is unchanged. - Closing a task returns `family_owed`, the owed answers filed while it was open, and the report cue asks for them to be named. - New plugin skill family-canon (moment work.record) covers when to evaluate a promotion, answering in order, what counts as a reason, the precedent reflex and the conflict order. _INSTRUCTIONS, create_note, create_snippet, classify_shapes and reporting-back point at it. The plugin version is minted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1214,3 +1214,134 @@ async def _shapes_follow(user_id: int, project_id: int, idea_id: int, outcome: s
|
||||
for r in rows
|
||||
], via="agent")
|
||||
return int(result.get("classified") or 0)
|
||||
|
||||
|
||||
# --- delivery (milestone 463 step 6) -------------------------------------------------
|
||||
#
|
||||
# The ledger reaches a session three ways: a count on entering a project, the
|
||||
# ideas themselves by retrieval, and the owed answers filed while a task was
|
||||
# open, handed back when it closes.
|
||||
|
||||
def family_line(project_id: int, cells: list[dict]) -> dict | None:
|
||||
"""The `family` line enter_project carries: what this project has not
|
||||
answered, owes, or answered against an older canon version. None when
|
||||
all three are zero — the key is attached only when it asks something.
|
||||
|
||||
Shown on EVERY entry (the step settled this), not only when a session
|
||||
touches a platform: entering is when the agent chooses what to work on,
|
||||
an owed task already appears in open_tasks, and an unanswered idea has
|
||||
nowhere else to be seen. Counts only, each with the call that lists it."""
|
||||
unassessed = sum(1 for c in cells if c["in_scope"] and c["status"] == "unassessed")
|
||||
owed = sum(1 for c in cells if c["status"] == "owed")
|
||||
recheck = sum(1 for c in cells if c["needs_recheck"])
|
||||
if not (unassessed or owed or recheck):
|
||||
return None
|
||||
parts, calls = [], {}
|
||||
if unassessed:
|
||||
parts.append(f"{unassessed} unassessed")
|
||||
calls["unassessed"] = f'list_family_adoptions(project_id={project_id}, status="unassessed")'
|
||||
if owed:
|
||||
parts.append(f"{owed} owed")
|
||||
calls["owed"] = f'list_family_adoptions(project_id={project_id}, status="owed")'
|
||||
if recheck:
|
||||
parts.append(f"{recheck} to recheck")
|
||||
calls["needs_recheck"] = f"list_family_adoptions(project_id={project_id}, needs_recheck=true)"
|
||||
return {
|
||||
"line": "family canon on this project's platforms: " + " · ".join(parts),
|
||||
"unassessed": unassessed, "owed": owed, "needs_recheck": recheck,
|
||||
"calls": calls,
|
||||
"answer_with": ("get_family_adoption(project_id, idea_id) reads one with its "
|
||||
"precedents; assess_family_adoption answers it — or classify_shapes "
|
||||
"the code against it (idea_id=…). The family-canon skill has the order."),
|
||||
}
|
||||
|
||||
|
||||
async def family_readout(user_id: int, project_id: int) -> dict | None:
|
||||
matrix = await adoption_matrix(user_id, project_id=project_id)
|
||||
return family_line(project_id, matrix["cells"])
|
||||
|
||||
|
||||
async def family_reach_ids(project_id: int, session=None) -> set[int]:
|
||||
"""The family records a project's retrieval reaches beyond its own: every
|
||||
canon idea on a platform the project is a member of, and each idea's
|
||||
reference implementations in the project's languages — all of them when
|
||||
none is in those languages, so a cross-language idea still brings its
|
||||
reference. Readability is the search's own scope, not decided here."""
|
||||
if session is None:
|
||||
async with async_session() as own:
|
||||
return await family_reach_ids(project_id, own)
|
||||
ideas = set((await session.execute(
|
||||
select(FamilyIdea.note_id)
|
||||
.join(FamilyIdeaPlatform, FamilyIdeaPlatform.note_id == FamilyIdea.note_id)
|
||||
.join(ProjectPlatform, ProjectPlatform.platform_id == FamilyIdeaPlatform.platform_id)
|
||||
.where(
|
||||
FamilyIdea.status == "canon",
|
||||
ProjectPlatform.project_id == project_id,
|
||||
ProjectPlatform.state.in_(family_svc.MEMBER_STATES),
|
||||
)
|
||||
)).scalars().all())
|
||||
if not ideas:
|
||||
return set()
|
||||
langs = set(await _project_languages(session, project_id))
|
||||
refs: dict[int, list[tuple[int, str]]] = {}
|
||||
for iid, nid, lang in (await session.execute(
|
||||
select(FamilyIdeaReference.idea_id, Note.id, func.lower(Note.data["language"].astext))
|
||||
.join(Note, Note.id == FamilyIdeaReference.snippet_id)
|
||||
.where(FamilyIdeaReference.idea_id.in_(ideas), Note.deleted_at.is_(None))
|
||||
)).all():
|
||||
refs.setdefault(iid, []).append((nid, lang or ""))
|
||||
reach = set(ideas)
|
||||
for found in refs.values():
|
||||
local = [nid for nid, lang in found if lang in langs]
|
||||
reach.update(local or [nid for nid, _ in found])
|
||||
return reach
|
||||
|
||||
|
||||
async def owed_since(user_id: int, since) -> list[dict]:
|
||||
"""The owed answers this user recorded since ``since`` that are still
|
||||
owed — work filed into a project (often another one) that the report
|
||||
closing this task should name."""
|
||||
if since is None:
|
||||
return []
|
||||
async with async_session() as session:
|
||||
rows = (await session.execute(
|
||||
select(FamilyAdoption, Note.title, Project.title)
|
||||
.join(FamilyDecision, (FamilyDecision.idea_id == FamilyAdoption.idea_id)
|
||||
& (FamilyDecision.project_id == FamilyAdoption.project_id))
|
||||
.join(Note, Note.id == FamilyAdoption.idea_id)
|
||||
.join(Project, Project.id == FamilyAdoption.project_id)
|
||||
.where(
|
||||
FamilyDecision.user_id == user_id,
|
||||
FamilyDecision.created_at >= since,
|
||||
FamilyDecision.after["status"].astext == "owed",
|
||||
FamilyAdoption.status == "owed",
|
||||
)
|
||||
.distinct()
|
||||
.order_by(Project.title, Note.title)
|
||||
)).all()
|
||||
return [
|
||||
{"idea_id": row.idea_id, "idea_title": idea_title,
|
||||
"project_id": row.project_id, "project_title": project_title,
|
||||
"owed_task_id": row.owed_task_id}
|
||||
for row, idea_title, project_title in rows
|
||||
if await access.can_read_project(user_id, row.project_id)
|
||||
]
|
||||
|
||||
|
||||
OWED_CUE = ("Owed family adoptions were filed while this task was open (`family_owed`) — "
|
||||
"name each in the report: it is work now waiting in that project.")
|
||||
|
||||
|
||||
async def attach_owed_adoptions(user_id: int, data: dict, note) -> None:
|
||||
"""Ride the owed answers filed since the task started on its closing
|
||||
response — fail-open: a decoration never breaks the close it rides on."""
|
||||
try:
|
||||
owed = await owed_since(user_id, getattr(note, "started_at", None)
|
||||
or getattr(note, "created_at", None))
|
||||
if owed:
|
||||
data["family_owed"] = owed
|
||||
if "report_back" in data:
|
||||
data["report_back"] = f"{data['report_back']} {OWED_CUE}"
|
||||
except Exception:
|
||||
logger.warning("owed adoptions unreadable for task %s", getattr(note, "id", None),
|
||||
exc_info=True)
|
||||
|
||||
Reference in New Issue
Block a user