CI & Build / Python lint (push) Successful in 9s
CI & Build / Plugin hooks (push) Successful in 22s
CI & Build / TypeScript typecheck (push) Successful in 1m0s
CI & Build / integration (push) Successful in 1m55s
CI & Build / Python tests (push) Successful in 2m20s
CI & Build / Build & push image (push) Successful in 1m7s
Components, stores, api clients, router, types and shared CSS now state what each piece is and why, without the milestone that introduced it. diff.ts names its consumers instead of the copy history; stores/notes and tasks state their current scope instead of the cleanup that set it; theme.css states the token practice instead of the sweep that established it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
/**
|
|
* Family canon: 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}` : ""}`);
|
|
}
|