feat(family): the promotion engine - triggers, the three criteria, the decision log and undo (milestone 463 step 3, #4989)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Failing after 1m5s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Failing after 1m5s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Skipped
The agent promotes a family idea when all three criteria hold, and no person approves it. The criteria are product text: services/family.py states them, and the promote tool's docstring names every criterion the service enforces.
- Criteria: each one vetoes on its own when its reasoning is blank. Platform terms also needs an applies-when and a platform scope; proven also needs named evidence. A veto keeps the idea a candidate and is logged, so it becomes precedent.
- Precedent: every promotion stores the decisions on the nearest ideas by meaning, plus any the caller names.
- Promotion sets canon, the applicability test and the platform scope, and opens an unassessed ledger row for each member project the promoter can write. Re-promotion moves the version past every version the idea has held.
- Retire and undo: undo reverses only the latest idea-level decision, restores its recorded before-state, and logs itself with the undone decision as its precedent. Settled here: leaving canon closes the unassessed rows but keeps the judged ones, which read as needing a recheck after a re-promotion.
- Triggers open evaluations but never promote:
- a cross-project lineage citation ("matching #N") on a note or task write;
- a same-meaning record in another project on a shared platform, on create, at 0.80 (measured: the known pattern's builds scored 0.79-0.82, an unrelated project's best match 0.65);
- a milestone closing on a platform.
Each fails open and rides the response as family_hint.
- Doors: seven MCP tools, /api/family REST endpoints, and a Family page (nav, /family) showing the criteria, the ideas, and the decision log with undo and retire.
- utils/recordHref.ts holds the one copy of "where a record opens", now shared with LessonDetailView.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Family canon (milestone 463): ideas every project on a platform shares, and
|
||||
* the log of every decision about them.
|
||||
*
|
||||
* The agent promotes by written criteria — no approval step. This door is for
|
||||
* reading what was decided and why, and for retiring an idea or undoing a
|
||||
* decision when a person disagrees.
|
||||
*/
|
||||
import { apiGet, apiPost } from "@/api/client";
|
||||
|
||||
export type IdeaStatus = "candidate" | "canon" | "retired";
|
||||
export type DecisionAction = "propose" | "promote" | "revise" | "retire" | "assess" | "undo";
|
||||
|
||||
export interface Criterion {
|
||||
key: string;
|
||||
title: string;
|
||||
test: string;
|
||||
}
|
||||
|
||||
export interface FamilyIdea {
|
||||
note_id: number;
|
||||
status: IdeaStatus;
|
||||
applies_when: string;
|
||||
canon_version: number;
|
||||
topic_id: number | null;
|
||||
title: string;
|
||||
note_type: string;
|
||||
is_task: boolean;
|
||||
project_id: number | null;
|
||||
platforms: string[];
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
/** A state snapshot a decision records — slugs, never ids. */
|
||||
export interface IdeaSnapshot {
|
||||
status: IdeaStatus;
|
||||
applies_when: string;
|
||||
canon_version: number;
|
||||
platforms: string[];
|
||||
}
|
||||
|
||||
export interface FamilyDecision {
|
||||
id: number;
|
||||
idea_id: number;
|
||||
idea_title?: string;
|
||||
project_id: number | null;
|
||||
action: DecisionAction;
|
||||
reason: string;
|
||||
before: IdeaSnapshot | null;
|
||||
after: IdeaSnapshot | null;
|
||||
evidence: Record<string, unknown>;
|
||||
precedent_ids: number[];
|
||||
decided_via: "agent" | "operator" | "system";
|
||||
user_id: number | null;
|
||||
created_at: string | null;
|
||||
undoable?: boolean;
|
||||
}
|
||||
|
||||
export async function listFamilyIdeas(status = ""): Promise<{ ideas: FamilyIdea[]; criteria: Criterion[] }> {
|
||||
const q = status ? `?status=${encodeURIComponent(status)}` : "";
|
||||
return apiGet(`/api/family/ideas${q}`);
|
||||
}
|
||||
|
||||
export async function listFamilyDecisions(limit = 50, offset = 0): Promise<FamilyDecision[]> {
|
||||
const data = await apiGet<{ decisions: FamilyDecision[] }>(
|
||||
`/api/family/decisions?limit=${limit}&offset=${offset}`,
|
||||
);
|
||||
return data.decisions;
|
||||
}
|
||||
|
||||
export async function undoFamilyDecision(id: number, reason: string) {
|
||||
return apiPost<{ decision: FamilyDecision }>(`/api/family/decisions/${id}/undo`, { reason });
|
||||
}
|
||||
|
||||
export async function retireFamilyIdea(noteId: number, reason: string) {
|
||||
return apiPost<{ decision: FamilyDecision }>(`/api/family/ideas/${noteId}/retire`, { reason });
|
||||
}
|
||||
Reference in New Issue
Block a user