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
+81 -3
View File
@@ -40,15 +40,29 @@ export interface IdeaSnapshot {
platforms: string[];
}
export type AdoptionStatus = "unassessed" | "adopted" | "variant" | "exempt" | "owed";
/** One project's answer as a decision records it. */
export interface AdoptionSnapshot {
status: AdoptionStatus;
reason: string;
canon_version: number | null;
}
/**
* A decision about the idea itself (project_id null) snapshots the idea; a
* project's assessment (project_id set) snapshots that project's answer.
*/
export interface FamilyDecision {
id: number;
idea_id: number;
idea_title?: string;
project_id: number | null;
project_title?: string | null;
action: DecisionAction;
reason: string;
before: IdeaSnapshot | null;
after: IdeaSnapshot | null;
before: IdeaSnapshot | AdoptionSnapshot | null;
after: IdeaSnapshot | AdoptionSnapshot | null;
evidence: Record<string, unknown>;
precedent_ids: number[];
decided_via: "agent" | "operator" | "system";
@@ -57,7 +71,17 @@ export interface FamilyDecision {
undoable?: boolean;
}
export async function listFamilyIdeas(status = ""): Promise<{ ideas: FamilyIdea[]; criteria: Criterion[] }> {
/** One ground of the conflict order, in order. */
export interface ConflictGround {
key: string;
title: string;
test: string;
fold_as: "alternative" | "trap" | "condition";
}
export async function listFamilyIdeas(
status = "",
): Promise<{ ideas: FamilyIdea[]; criteria: Criterion[]; conflict_order?: ConflictGround[] }> {
const q = status ? `?status=${encodeURIComponent(status)}` : "";
return apiGet(`/api/family/ideas${q}`);
}
@@ -76,3 +100,57 @@ export async function undoFamilyDecision(id: number, reason: string) {
export async function retireFamilyIdea(noteId: number, reason: string) {
return apiPost<{ decision: FamilyDecision }>(`/api/family/ideas/${noteId}/retire`, { reason });
}
export interface Outcome {
key: AdoptionStatus;
title: string;
test: string;
}
/** One project's answer to one canon idea — a cell of the matrix. */
export interface AdoptionCell {
project_id: number;
project_title: string;
idea_id: number;
idea_title: string;
idea_version: number;
/** The project is on one of the idea's platforms. */
in_scope: boolean;
/** A ledger row exists. False: the idea reaches the project, nobody asked yet. */
reached: boolean;
status: AdoptionStatus;
reason: string;
canon_version: number | null;
assessed_at: string | null;
decided_via: "agent" | "operator" | "system" | null;
/** Answered against a canon version that is not the current one. */
needs_recheck: boolean;
owed_task: { id: number; title: string; status: string } | null;
}
export interface MatrixIdea {
note_id: number;
title: string;
note_type: string;
is_task: boolean;
canon_version: number;
applies_when: string;
platforms: string[];
}
export interface AdoptionMatrix {
ideas: MatrixIdea[];
projects: { id: number; title: string; platforms: string[] }[];
cells: AdoptionCell[];
outcomes: Outcome[];
}
export async function fetchAdoptionMatrix(
opts: { platform?: string; projectId?: number } = {},
): Promise<AdoptionMatrix> {
const q = new URLSearchParams();
if (opts.platform) q.set("platform", opts.platform);
if (opts.projectId) q.set("project_id", String(opts.projectId));
const qs = q.toString();
return apiGet(`/api/family/matrix${qs ? `?${qs}` : ""}`);
}