diff --git a/frontend/src/api/family.ts b/frontend/src/api/family.ts new file mode 100644 index 00000000..9af9a96b --- /dev/null +++ b/frontend/src/api/family.ts @@ -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; + 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 { + 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 }); +} diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 0363f469..dfce0208 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -50,6 +50,7 @@ router.afterEach(() => { Projects Snippets Rulebooks + Family