CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 1m9s
CI & Build / integration (push) Successful in 1m21s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 1m29s
Step 1 added family_ideas.topic_id but nothing could set it, so the note<->topic link the milestone promised existed only as a column. set_family_topic links or unlinks (0) a topic in a rulebook the caller owns, one idea per topic; the version does not move. get_family_idea and get_family_adoption list the topic's rules; list_family_ideas and the Family page name the topic. The model docstrings no longer claim the topic's rules are platform-scoped in retrieval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
/**
|
|
* 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 IdeaTopic {
|
|
id: number;
|
|
title: string;
|
|
rulebook_id: number;
|
|
/** Present on a single idea's read, not on the list. */
|
|
rules?: { id: number; title: string }[];
|
|
}
|
|
|
|
export interface FamilyIdea {
|
|
note_id: number;
|
|
status: IdeaStatus;
|
|
applies_when: string;
|
|
canon_version: number;
|
|
topic_id: number | null;
|
|
/** The rule topic holding the idea's binding norms; null when none is
|
|
* linked or the reader does not own its rulebook. */
|
|
topic: IdeaTopic | 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 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 | AdoptionSnapshot | null;
|
|
after: IdeaSnapshot | AdoptionSnapshot | null;
|
|
evidence: Record<string, unknown>;
|
|
precedent_ids: number[];
|
|
decided_via: "agent" | "operator" | "system";
|
|
user_id: number | null;
|
|
created_at: string | null;
|
|
undoable?: boolean;
|
|
}
|
|
|
|
/** 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}`);
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
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;
|
|
/** Live shapes in the project classified against one of the idea's references. */
|
|
shapes: { path: string; symbol: string; status: "canonical" | "instance" | "variant" }[];
|
|
}
|
|
|
|
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}` : ""}`);
|
|
}
|