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

- 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:
2026-10-06 11:05:00 -04:00
co-authored by Claude Opus 5.5
parent faa1b72307
commit 201e09901b
12 changed files with 2386 additions and 57 deletions
+25 -4
View File
@@ -1,7 +1,8 @@
"""Family canon routes — the web door to the promotion engine (milestone 463).
The agent decides promotions through the MCP tools; this door exists so a
person can READ every decision and its reasons, and retire an idea or undo a
The agent decides promotions and each project's answers through the MCP
tools; this door exists so a person can READ every decision and its reasons
— the ideas, the adoption matrix, the log — and retire an idea or undo a
decision when they disagree. The promote and propose endpoints are here for
parity with the agent's door (rule 33), and are recorded as the operator's.
Every write is gated on the idea's note in the service (rule 78).
@@ -13,6 +14,7 @@ from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
logger = logging.getLogger(__name__)
@@ -38,7 +40,10 @@ async def list_ideas_route():
platform=request.args.get("platform") or None,
limit=limit, offset=offset,
)
return jsonify({"ideas": ideas, "criteria": list(family_svc.CRITERIA)})
return jsonify({
"ideas": ideas, "criteria": list(family_svc.CRITERIA),
"conflict_order": list(adoption_svc.CONFLICT_ORDER),
})
@family_bp.route("/ideas/<int:note_id>", methods=["GET"])
@@ -100,13 +105,29 @@ async def retire_route(note_id: int):
return jsonify(result)
@family_bp.route("/matrix", methods=["GET"])
@login_required
async def matrix_route():
"""Projects × canon ideas — the adoption matrix. Read-only: the agent
answers through assess_family_adoption; a person reads the answers here
and undoes one from the decision log if they disagree."""
matrix = await adoption_svc.adoption_matrix(
get_current_user_id(),
platform=request.args.get("platform") or None,
project_id=request.args.get("project_id", type=int) or None,
)
return jsonify(matrix)
@family_bp.route("/decisions", methods=["GET"])
@login_required
async def list_decisions_route():
limit, offset = _page()
idea_id = request.args.get("idea_id", type=int)
project_id = request.args.get("project_id", type=int)
rows = await family_svc.list_decisions(
get_current_user_id(), idea_id=idea_id or None, limit=limit, offset=offset,
get_current_user_id(), idea_id=idea_id or None, project_id=project_id or None,
limit=limit, offset=offset,
)
return jsonify({"decisions": rows, "limit": limit, "offset": offset})