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:
@@ -4,17 +4,21 @@
|
||||
* and the log of every decision about them.
|
||||
*
|
||||
* Nobody approves a promotion — the agent decides against the three criteria
|
||||
* shown at the top, and records why. This page is where a person reads those
|
||||
* decisions, and the two places they can disagree: retire an idea, or undo
|
||||
* the latest decision on one. Both ask for a reason, because the reason is
|
||||
* what the next decision about something similar is checked against.
|
||||
* shown at the top, and records why — and nobody approves a project's answer
|
||||
* either: the adoption matrix shows each project's answer to each canon idea
|
||||
* as the agent gave it. This page is where a person reads those decisions,
|
||||
* and the two places they can disagree: retire an idea, or undo the latest
|
||||
* decision on one (or on one project's answer). Both ask for a reason,
|
||||
* because the reason is what the next similar decision is checked against.
|
||||
*/
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import {
|
||||
listFamilyDecisions, listFamilyIdeas, retireFamilyIdea, undoFamilyDecision,
|
||||
type Criterion, type FamilyDecision, type FamilyIdea, type IdeaStatus,
|
||||
type AdoptionSnapshot, type ConflictGround, type Criterion, type FamilyDecision,
|
||||
type FamilyIdea, type IdeaSnapshot, type IdeaStatus,
|
||||
} from "@/api/family";
|
||||
import FamilyAdoptionMatrix from "@/components/FamilyAdoptionMatrix.vue";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { fmtStamp } from "@/utils/dateFormat";
|
||||
import { recordHref } from "@/utils/recordHref";
|
||||
@@ -23,6 +27,8 @@ const PAGE = 50;
|
||||
const toast = useToastStore();
|
||||
|
||||
const criteria = ref<Criterion[]>([]);
|
||||
const conflictOrder = ref<ConflictGround[]>([]);
|
||||
const matrixRef = ref<InstanceType<typeof FamilyAdoptionMatrix> | null>(null);
|
||||
const ideas = ref<FamilyIdea[]>([]);
|
||||
const decisions = ref<FamilyDecision[]>([]);
|
||||
const statusFilter = ref<"" | IdeaStatus>("");
|
||||
@@ -55,6 +61,7 @@ async function load() {
|
||||
]);
|
||||
ideas.value = ideaData.ideas;
|
||||
criteria.value = ideaData.criteria;
|
||||
conflictOrder.value = ideaData.conflict_order ?? [];
|
||||
decisions.value = log;
|
||||
moreDecisions.value = log.length === PAGE;
|
||||
} catch {
|
||||
@@ -103,7 +110,7 @@ async function confirmPending() {
|
||||
else await retireFamilyIdea(p.id, reason);
|
||||
toast.show(p.kind === "undo" ? "Decision undone" : "Idea retired");
|
||||
pending.value = null;
|
||||
await load();
|
||||
await Promise.all([load(), matrixRef.value?.load()]);
|
||||
} catch (e: unknown) {
|
||||
toast.show(apiErrorMessage(e, p.kind === "undo" ? "Could not undo" : "Could not retire"), "error");
|
||||
} finally {
|
||||
@@ -144,14 +151,44 @@ function evidenceLines(d: FamilyDecision): { label: string; lines: string[] }[]
|
||||
if (typeof ev.trigger === "string") {
|
||||
out.push({ label: "Trigger", lines: [ev.trigger] });
|
||||
}
|
||||
const conflict = ev.conflict as {
|
||||
ground: string; grounds_checked: Record<string, string>; canon: string; other: string;
|
||||
folded_as: string; conditions: { canon: string; other: string } | null;
|
||||
} | undefined;
|
||||
if (conflict) {
|
||||
const title = (k: string) => conflictOrder.value.find((g) => g.key === k)?.title ?? k;
|
||||
const lines = [
|
||||
`Decided by: ${title(conflict.ground)}`,
|
||||
...Object.entries(conflict.grounds_checked || {})
|
||||
.map(([k, why]) => `${title(k)} did not decide — ${why}`),
|
||||
`Canon: ${conflict.canon}; the other side (${conflict.other}) was folded into the note as ${
|
||||
conflict.folded_as === "condition" ? "the branch for its condition" : `a ${conflict.folded_as}`}`,
|
||||
];
|
||||
if (conflict.conditions) {
|
||||
lines.push(`When ${conflict.conditions.canon}: ${conflict.canon}`,
|
||||
`When ${conflict.conditions.other}: ${conflict.other}`);
|
||||
}
|
||||
out.push({ label: "Conflict", lines });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const ADOPTION_LABELS: Record<string, string> = {
|
||||
unassessed: "not assessed", adopted: "adopted", variant: "variant",
|
||||
exempt: "doesn't apply", owed: "owed",
|
||||
};
|
||||
|
||||
function stateLine(d: FamilyDecision): string {
|
||||
const a = d.after;
|
||||
if (!a) return "";
|
||||
const where = a.platforms.length ? ` · ${a.platforms.join(", ")}` : "";
|
||||
return `${a.status} v${a.canon_version}${where}`;
|
||||
if (d.project_id !== null) {
|
||||
const row = a as AdoptionSnapshot;
|
||||
const version = row.canon_version ? ` against v${row.canon_version}` : "";
|
||||
return `${ADOPTION_LABELS[row.status] ?? row.status}${version}`;
|
||||
}
|
||||
const idea = a as IdeaSnapshot;
|
||||
const where = idea.platforms.length ? ` · ${idea.platforms.join(", ")}` : "";
|
||||
return `${idea.status} v${idea.canon_version}${where}`;
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
@@ -246,6 +283,11 @@ onMounted(load);
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="fam-section" aria-labelledby="fam-matrix">
|
||||
<h2 id="fam-matrix">Adoption</h2>
|
||||
<FamilyAdoptionMatrix ref="matrixRef" />
|
||||
</section>
|
||||
|
||||
<section class="fam-section" aria-labelledby="fam-log">
|
||||
<h2 id="fam-log">Decision log</h2>
|
||||
<p v-if="!decisions.length" class="fam-muted">Nothing has been decided yet.</p>
|
||||
@@ -255,6 +297,10 @@ onMounted(load);
|
||||
<div class="fam-row-title">
|
||||
<span class="fam-action">{{ ACTION_LABELS[d.action] ?? d.action }}</span>
|
||||
<router-link :to="ideaLink(d)">{{ d.idea_title ?? `#${d.idea_id}` }}</router-link>
|
||||
<template v-if="d.project_id !== null">
|
||||
<span class="fam-muted">for</span>
|
||||
<router-link :to="`/projects/${d.project_id}`">{{ d.project_title ?? `project ${d.project_id}` }}</router-link>
|
||||
</template>
|
||||
<span class="fam-muted">
|
||||
#{{ d.id }} · {{ d.decided_via }} · {{ d.created_at ? fmtStamp(d.created_at) : "" }}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user