feat(family): the adoption ledger - assessment, the conflict order, owed->task, recheck, the adoption matrix (milestone 463 step 4, #4990)
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 1m0s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Failing after 1m28s
CI & Build / Build & push image (push) Skipped
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 1m0s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Failing after 1m28s
CI & Build / Build & push image (push) Skipped
- services/family_adoption.py: assess one project against one canon idea by the four outcomes in order (exempt, variant, adopted, owed). Every outcome needs a reason and adopted needs evidence. The engine records the precedents itself: this idea's answers elsewhere, and this project's answers to the nearest ideas. The same answer given twice records nothing. - owed files a task in the OWING project, tagged to the System matching the idea's canonical area, naming the gap and the reference for that project's language. The task follows the answer: adopted closes it, exempt or variant cancels it, owed again reopens it. Each move is logged on the task. - the conflict order is enforced: every ground above the deciding one must say why it did not decide. The losing side is folded into the idea's note as a trap, an alternative or a condition branch, the version moves, and both rows are answered against the revision. - recheck is derived (row version != idea version). family.revise moves the version when substance changes. undo covers a project's latest answer too. - set_family_references names the reference implementations. - MCP: get/list/assess adoption, resolve_family_conflict, revise_family_idea, set_family_references. Web: GET /api/family/matrix. - UI: an adoption matrix on /family (platform filter, cell detail with reason, recheck and owed-task link) and the same matrix narrowed to one project on its Family tab. The decision log now reads project-level decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,9 +163,11 @@ _READ_ONLY_TOOLS = frozenset({
|
||||
# The platform catalog and a project's answers (milestone 463). A pure
|
||||
# read; set_project_platforms is the write.
|
||||
"list_platforms",
|
||||
# Family canon (milestone 463): ideas, one idea with its precedents, and
|
||||
# the decision log. Reads of records the caller can read.
|
||||
# Family canon (milestone 463): ideas, one idea with its precedents, the
|
||||
# decision log, and the adoption ledger. Reads of records the caller can
|
||||
# read.
|
||||
"list_family_ideas", "get_family_idea", "list_family_decisions",
|
||||
"get_family_adoption", "list_family_adoptions",
|
||||
# The pass over the corpus and its queue (milestone 458 step 7): which
|
||||
# rules are unjudged and which proposals wait. Reads of the caller's own
|
||||
# rules, as list_rules is.
|
||||
@@ -193,6 +195,9 @@ _WRITE_TOOLS = frozenset({
|
||||
# family canon — the promotion engine
|
||||
"propose_family_idea", "promote_family_idea", "retire_family_idea",
|
||||
"undo_family_decision",
|
||||
# family canon — the adoption ledger
|
||||
"revise_family_idea", "assess_family_adoption", "resolve_family_conflict",
|
||||
"set_family_references",
|
||||
"bind_repo", "unbind_repo",
|
||||
# snippets, processes, the shape ledger
|
||||
"create_snippet", "update_snippet", "delete_snippet", "verify_snippet",
|
||||
|
||||
+205
-11
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from scribe.mcp._context import current_user_id
|
||||
from scribe.services import family as family_svc
|
||||
from scribe.services import family_adoption as adoption_svc
|
||||
|
||||
|
||||
async def list_family_ideas(
|
||||
@@ -32,8 +33,9 @@ async def list_family_ideas(
|
||||
|
||||
async def get_family_idea(note_id: int) -> dict:
|
||||
"""One family idea with what an evaluation needs: its state, platforms,
|
||||
full decision history, ledger counts, THE THREE CRITERIA, and the
|
||||
`precedents` — the decisions on the ideas nearest this one by meaning.
|
||||
full decision history, ledger counts, THE THREE CRITERIA, the `ledger`
|
||||
(every project's answer, with `needs_recheck`), and the `precedents` —
|
||||
the decisions on the ideas nearest this one by meaning.
|
||||
Read the precedents before deciding, and decide consistently with them
|
||||
unless this idea differs in a way you can name.
|
||||
|
||||
@@ -45,6 +47,7 @@ async def get_family_idea(note_id: int) -> dict:
|
||||
if idea is None:
|
||||
raise ValueError(f"#{note_id} is not a family idea you can read")
|
||||
idea["precedents"] = await family_svc.precedents(uid, note_id)
|
||||
idea["ledger"] = await adoption_svc.list_adoptions(uid, idea_id=note_id)
|
||||
return idea
|
||||
|
||||
|
||||
@@ -146,10 +149,12 @@ async def retire_family_idea(note_id: int, reason: str) -> dict:
|
||||
|
||||
|
||||
async def undo_family_decision(decision_id: int, reason: str) -> dict:
|
||||
"""Reverse a family decision — a promotion, a retirement or a proposal —
|
||||
restoring the state it recorded as `before`. Only the latest idea-level
|
||||
decision on an idea can be undone; the undo is logged too, so the history
|
||||
keeps both.
|
||||
"""Reverse a family decision — a promotion, a revision, a retirement, a
|
||||
proposal, or one project's assessment — restoring the state it recorded
|
||||
as `before`. Only the latest decision on an idea (or on one project's
|
||||
answer to it) can be undone; the undo is logged too, so the history
|
||||
keeps both. Undoing an owed answer closes its task; undoing back to owed
|
||||
reopens it.
|
||||
|
||||
Args:
|
||||
decision_id: the decision (list_family_decisions).
|
||||
@@ -158,26 +163,215 @@ async def undo_family_decision(decision_id: int, reason: str) -> dict:
|
||||
return await family_svc.undo(current_user_id(), decision_id, reason=reason)
|
||||
|
||||
|
||||
async def list_family_decisions(note_id: int = 0, limit: int = 50, offset: int = 0) -> dict:
|
||||
async def list_family_decisions(
|
||||
note_id: int = 0, project_id: int = 0, limit: int = 50, offset: int = 0,
|
||||
) -> dict:
|
||||
"""The family decision log, newest first: every proposal, promotion,
|
||||
veto, retirement and undo, with its reason, evidence and the precedents
|
||||
it followed. `undoable` marks the decision an undo would reverse.
|
||||
veto, revision, retirement, assessment and undo, with its reason,
|
||||
evidence and the precedents it followed. `undoable` marks the decision an
|
||||
undo would reverse — per idea, and per project's answer.
|
||||
|
||||
Args:
|
||||
note_id: only this idea's decisions. 0 = all.
|
||||
project_id: only this project's assessments. 0 = all.
|
||||
limit / offset: page through the log.
|
||||
"""
|
||||
rows = await family_svc.list_decisions(
|
||||
current_user_id(), idea_id=note_id or None,
|
||||
current_user_id(), idea_id=note_id or None, project_id=project_id or None,
|
||||
limit=max(1, min(limit, 200)), offset=max(0, offset),
|
||||
)
|
||||
return {"decisions": rows, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
async def revise_family_idea(
|
||||
note_id: int, reason: str, applies_when: str = "", platforms: list[str] | None = None,
|
||||
evidence: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Record that a canon idea's SUBSTANCE changed — you rewrote its note's
|
||||
approach, its traps or its checklist, or its applicability or platforms
|
||||
moved. The canon version moves, so every project's answer given against
|
||||
the old version reads `needs_recheck` until it is assessed again.
|
||||
|
||||
A typo fix is not a revision. A change a project that adopted the old
|
||||
version would need to act on is.
|
||||
|
||||
Args:
|
||||
note_id: the canon idea.
|
||||
reason: what changed, in a sentence.
|
||||
applies_when: a new 'when it applies'. Empty = keep the current one.
|
||||
platforms: new platform slugs. Omit = keep the current ones.
|
||||
evidence: what prompted the change, if anything.
|
||||
"""
|
||||
return await family_svc.revise(
|
||||
current_user_id(), note_id, reason=reason,
|
||||
applies_when=applies_when or None, platforms=platforms, evidence=evidence,
|
||||
)
|
||||
|
||||
|
||||
async def get_family_adoption(project_id: int, idea_id: int) -> dict:
|
||||
"""Everything one assessment needs: the idea (its 'when it applies',
|
||||
platforms and version), this project's current answer, THE FOUR
|
||||
OUTCOMES, the `precedents` — this idea's answers in other projects and
|
||||
this project's answers to the nearest ideas — and the reference
|
||||
implementations beside this project's languages. Read it before
|
||||
assess_family_adoption.
|
||||
|
||||
Args:
|
||||
project_id: the project answering.
|
||||
idea_id: the canon idea.
|
||||
"""
|
||||
return await adoption_svc.get_adoption(current_user_id(), project_id, idea_id)
|
||||
|
||||
|
||||
async def list_family_adoptions(
|
||||
project_id: int = 0, idea_id: int = 0, status: str = "", needs_recheck: bool = False,
|
||||
platform: str = "",
|
||||
) -> dict:
|
||||
"""The adoption ledger: each project's answer to each canon idea that
|
||||
reaches it — `unassessed`, `adopted`, `variant`, `exempt` or `owed`, with
|
||||
its reason, the canon version it was given against, `needs_recheck` when
|
||||
the idea has moved on since, and the owed task.
|
||||
|
||||
Args:
|
||||
project_id: one project's answers. 0 = all you can read.
|
||||
idea_id: one idea's answers. 0 = all canon.
|
||||
status: one outcome. Empty = all.
|
||||
needs_recheck: only answers given against an older canon version.
|
||||
platform: only ideas for this platform slug.
|
||||
"""
|
||||
rows = await adoption_svc.list_adoptions(
|
||||
current_user_id(), project_id=project_id or None, idea_id=idea_id or None,
|
||||
status=status or None, recheck_only=needs_recheck, platform=platform or None,
|
||||
)
|
||||
return {"adoptions": rows}
|
||||
|
||||
|
||||
async def assess_family_adoption(
|
||||
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,
|
||||
) -> dict:
|
||||
"""Answer one canon family idea for one project. YOU decide; nobody
|
||||
approves. Judge in this order and stop at the first that holds:
|
||||
|
||||
1. exempt — the idea's 'when it applies' is false for this project.
|
||||
`reason` names the fact about the project that makes it false.
|
||||
2. variant — it applies, and the project departs for a reason that names
|
||||
a FACT about itself the canon did not account for. A preference, a
|
||||
taste, or "we already did it another way" is not a reason: that is
|
||||
owed — or, if this project's way is better rather than different, a
|
||||
conflict (resolve_family_conflict).
|
||||
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.
|
||||
|
||||
Read get_family_adoption first: answer consistently with its precedents
|
||||
unless this project differs in a way you can name in `reason`. The
|
||||
engine also records the precedents it found itself.
|
||||
|
||||
The owed task follows the answer: adopted closes it as done, exempt or
|
||||
variant cancels it, owed again reopens it. The same answer given twice
|
||||
records nothing the second time.
|
||||
|
||||
Args:
|
||||
project_id: the project answering.
|
||||
idea_id: the canon idea.
|
||||
outcome: exempt | variant | adopted | owed.
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
async def resolve_family_conflict(
|
||||
idea_id: int,
|
||||
canon_project_id: int,
|
||||
other_project_id: int,
|
||||
ground: str,
|
||||
fold: str,
|
||||
reason: str,
|
||||
grounds_checked: dict | None = None,
|
||||
evidence: list[str] | None = None,
|
||||
conditions: dict | None = None,
|
||||
precedent_ids: list[int] | None = None,
|
||||
) -> dict:
|
||||
"""Settle two projects that solve the same canon idea differently, each
|
||||
for reasons it believes. YOU decide, by THE CONFLICT ORDER — the first
|
||||
ground that applies wins, and for every ground above it you say in
|
||||
`grounds_checked` why it did not decide:
|
||||
|
||||
1. operator_stance — one side follows a stance the operator stated (a
|
||||
rule, a preference, a recorded decision). Name it in `evidence`.
|
||||
2. covers_failure — one side covers a recorded failure (an incident, an
|
||||
issue, a lesson) the other does not. Name it in `evidence`. The other
|
||||
side becomes owed.
|
||||
3. split_by_condition — both are right under different conditions. The
|
||||
canon splits: `conditions={"canon": …, "other": …}`, and each side is
|
||||
canon where its condition holds.
|
||||
4. most_recent_complete — none of the above: the side verified most
|
||||
recently and covering the most wins. Name the verification.
|
||||
|
||||
`canon_project_id` is the side whose approach the idea's note states
|
||||
after this. If the note says something else now, rewrite it first
|
||||
(update_note) — the note is the canon.
|
||||
|
||||
The losing side's reasoning, `fold`, is appended to the idea's note as a
|
||||
trap (ground 2), an alternative (1, 4) or the branch for its condition
|
||||
(3). It is never dropped. The version moves, so every other project's
|
||||
answer reads `needs_recheck`. The canon side is answered adopted; the
|
||||
other owed (with a task in its project) or, in a split, adopted.
|
||||
|
||||
Args:
|
||||
idea_id: the canon idea.
|
||||
canon_project_id: the side whose approach is canon after this.
|
||||
other_project_id: the side that loses, or the split's other branch.
|
||||
ground: operator_stance | covers_failure | split_by_condition | most_recent_complete.
|
||||
fold: the other side's reasoning, as it should read in the note.
|
||||
reason: the decision in one or two sentences.
|
||||
grounds_checked: {earlier ground: why it did not decide}.
|
||||
evidence: the stance, the failure or the verification.
|
||||
conditions: for a split, {"canon": condition, "other": condition}.
|
||||
precedent_ids: earlier family decisions you followed, if any.
|
||||
"""
|
||||
return await adoption_svc.resolve_conflict(
|
||||
current_user_id(), idea_id, canon_project_id=canon_project_id,
|
||||
other_project_id=other_project_id, ground=ground, grounds_checked=grounds_checked,
|
||||
fold=fold, evidence=evidence, reason=reason, conditions=conditions,
|
||||
precedent_ids=precedent_ids,
|
||||
)
|
||||
|
||||
|
||||
async def set_family_references(note_id: int, snippet_ids: list[int]) -> dict:
|
||||
"""Set a family idea's reference implementations — the snippets an owed
|
||||
task points a project at, ideally one per language. Replaces the list.
|
||||
The idea is what transfers; a reference is where to start, not code to
|
||||
copy.
|
||||
|
||||
Args:
|
||||
note_id: the family idea.
|
||||
snippet_ids: snippets implementing it. [] clears the list.
|
||||
"""
|
||||
refs = await adoption_svc.set_references(current_user_id(), note_id, snippet_ids or [])
|
||||
return {"references": refs}
|
||||
|
||||
|
||||
def register(mcp) -> None:
|
||||
for fn in (
|
||||
list_family_ideas, get_family_idea, propose_family_idea,
|
||||
promote_family_idea, retire_family_idea, undo_family_decision,
|
||||
list_family_decisions,
|
||||
list_family_decisions, revise_family_idea, get_family_adoption,
|
||||
list_family_adoptions, assess_family_adoption, resolve_family_conflict,
|
||||
set_family_references,
|
||||
):
|
||||
mcp.tool(name=fn.__name__)(fn)
|
||||
|
||||
Reference in New Issue
Block a user