import { apiGet, apiPost, apiDelete } from "@/api/client"; /** * The moments of work rules mount on (milestone 458), and the actions that * reach each one on this install — `GET /api/retrieval/moments`, the payload * the `list_moments` MCP tool returns, plus the counts the Settings view shows * beside each moment. */ export interface Moment { name: string; /** What is happening at this moment. */ means: string; /** The kinds of action that typically reach it. */ reached_by: string; } /** A shape rather than an entry: `skill.`, one moment per procedure. */ export interface MomentFamily { name: string; prefix: string; means: string; reached_by: string; } /** `default` ships with the product; `install` is this install's own. */ export type ActionVia = "default" | "install"; export interface MomentAction { tool: string; /** Empty = every call of the tool. A command prefix for a command tool, * `field=value` pairs for any other. */ match: string; via: ActionVia; } /** A shipped default this install switched off. */ export interface RemovedDefault { id: number; tool: string; match: string; moment: string; reason: string | null; actor: string | null; created_at: string | null; } export interface MomentUsage { /** How many times a mounted rule arrived at this moment. */ delivered: number; /** How many distinct rules did. */ rules: number; /** Of those, how many an agent then opened — an upper bound: a pull records * the door, not the line that prompted it. */ opened: number; last_delivered_at: string | null; } export interface MomentsPayload { moments: Moment[]; families: MomentFamily[]; total: number; actions: Record; removed_defaults: RemovedDefault[]; /** Rules mounted per moment; a moment carrying none is absent. */ mounted: Record; /** The count read failed — `mounted` is empty for that reason, not because * nothing is mounted. */ mounted_failed?: boolean; usage: { by_moment: Record; days: number; /** Same distinction as `mounted_failed`, for the usage read. */ moment_usage_failed?: boolean; }; } export interface MappingChange { tool: string; match?: string; moment: string; reason?: string; } export function getMoments(days = 30): Promise { return apiGet(`/api/retrieval/moments?days=${days}`); } export function mapAction(change: MappingChange): Promise { return apiPost("/api/retrieval/moments/mappings", change); } /** Query parameters, not a body: DELETE bodies are not reliably sent. */ export function unmapAction(change: MappingChange): Promise { const q = new URLSearchParams({ tool: change.tool, match: change.match ?? "", moment: change.moment, reason: change.reason ?? "", }); return apiDelete(`/api/retrieval/moments/mappings?${q.toString()}`); } /** * Proposals about a rule's moments, waiting on a person. A `mount` proposal * (milestone 458 step 7) comes from a pass that read the rule, or from the * rule being opened just after the moment fired. An `unmount` proposal (step * 7b) comes from sessions reporting that the mount arrived where it did not * apply. `GET /api/retrieval/moments/proposals`, the payload the * `rule_moment_proposals` MCP tool returns. */ export type ProposalSource = "pass" | "signal" | "edit" | "misfire"; export interface MisfireReason { why: string; reached_by: string; at: string; } export interface MomentProposal { proposal: "mount" | "unmount"; moment: string; source: ProposalSource; why: string; /** For a misfire, `situations` counts distinct days and `co_surfaced` the reports. */ evidence: { situations: number; projects: number; co_surfaced: number }; created_at: string | null; /** Unmount proposals only: the newest reasons given, and how often each action reached the moment. */ reasons?: MisfireReason[]; reached_by?: Record; } export interface RuleProposals { id: number; title: string; kind: string; statement: string; when_to_apply: string; home: "global" | "project"; /** What the rule is mounted on now. */ mounted: string[]; proposals: MomentProposal[]; } export interface ProposalsPayload { rules: RuleProposals[]; total: number; } export type Verdict = "confirm" | "reject"; export interface MomentJudgment { rule_id: number; moment: string; verdict: Verdict; note?: string; } export interface JudgeResult { judged: { rule_id: number; moment: string; state: string }[]; refused: { rule_id: number; moment?: string; error: string }[]; } export function getProposals(ruleId?: number): Promise { return apiGet(`/api/retrieval/moments/proposals${ruleId ? `?rule_id=${ruleId}` : ""}`); } /** * A confirm MOUNTS the rule on the moment (on an unmount proposal, keeps it); * a reject unmounts it if mounted and stops the pair being proposed again. */ export function judgeProposals(judgments: MomentJudgment[]): Promise { return apiPost("/api/retrieval/moments/proposals/judge", { judgments }); }