feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s

Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
This commit is contained in:
Codex
2026-09-10 10:31:34 +02:00
parent 8fe526dd6e
commit 82e2c91f42
168 changed files with 11914 additions and 1772 deletions
@@ -0,0 +1,138 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { apiFetch, apiErrorMessage } from "../api/client";
import { listEvidence, getEvidence, type EvidencePage, type EvidenceUnit } from "../api/evidence";
import { useAuthGeneration } from "../auth/authState";
import { Button } from "../components/ui/button";
import { MarkdownView } from "../viewers/MarkdownView";
import { EvidenceSources } from "./EvidenceSourceReview";
const control = "rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:ring-3 focus-visible:ring-ring/50";
const kinds = ["domain", "glossary", "enum", "example", "mapping", "normalization", "formula", "reference"];
const states: Record<string, string> = { new: "New file", modified: "Modified", active: "Active locally", removed: "Removed file", review_required: "Needs review", legacy: "Conversion required", invalid: "Invalid file" };
const templates: Record<string, string> = {
domain: "## Rule\n\nDescribe the approved domain rule.",
glossary: "## Definition\n\nDefine the concept.\n\n## Synonyms\n\n- Alternative name\n\n## Variants\n",
enum: '## Column\n\nsales.orders.status\n\n## Values\n\n### "A"\n\nActive',
example: "## Question\n\nHow many orders?\n\n## Interpretation\n\nCount distinct orders.",
mapping: "## Concept\n\nOrders\n\n## Tables\n\n- sales.orders\n\n## Columns\n\n- sales.orders.id",
normalization: "## Input\n\nA\n\n## Output\n\nActive\n\n## Rule\n\nExpand the status abbreviation.",
formula: "## Concept\n\nTotal amount\n\n## Columns\n\n- sales.lines.amount\n\n## SQL\n\n```sql\nsum(amount)\n```",
reference: "## URL\n\nhttps://example.org/policy\n\n## Label\n\nPolicy\n\n## Description\n\nDescribe the reference.",
};
function CopyText({ text, label, showText = true }: { text: string; label: string; showText?: boolean }) {
const [notice, setNotice] = useState("");
return <div className="flex min-w-0 flex-wrap items-start gap-2">
{showText && <code className="min-w-0 flex-1 break-all text-xs select-text">{text}</code>}
<Button size="sm" variant="outline" aria-label={`Copy ${label}`} onClick={async () => {
try { await navigator.clipboard.writeText(text); setNotice("Copied"); }
catch { setNotice("Select the text and copy it manually."); }
}}>Copy</Button><span role="status" className="text-xs text-muted-foreground">{notice}</span>
</div>;
}
export function EvidenceManagementPage({ canManage }: { canManage: boolean }) {
const generation = useAuthGeneration();
const [workspace, setWorkspace] = useState("");
const workspaces = useQuery({ queryKey: ["evidence-workspaces", generation],
queryFn: () => apiFetch<Array<{ id: string; displayName?: string }>>("/workspaces"), enabled: canManage });
if (!canManage) return <main className="p-6">Evidence administration is not permitted.</main>;
return <main aria-label="Evidence management" className="flex min-h-0 flex-1 flex-col overflow-auto bg-background">
<header className="flex flex-wrap items-end justify-between gap-4 border-b px-6 py-5">
<div><h1 className="font-serif text-2xl font-semibold">Evidence management</h1>
<p className="mt-1 text-sm text-muted-foreground">Review workspace knowledge and maintain its Markdown files.</p></div>
<label className="grid min-w-52 gap-1 text-sm">Workspace
<select className={control} value={workspace} onChange={event => setWorkspace(event.target.value)}>
<option value="">Choose a workspace</option>
{workspaces.data?.map(w => <option key={w.id} value={w.id}>{w.displayName ?? w.id}</option>)}
</select></label>
</header>
{workspaces.isError && <p role="alert" className="p-6">{apiErrorMessage(workspaces.error)}</p>}
{workspace ? <WorkspaceEvidence key={`${generation}:${workspace}`} workspace={workspace} generation={generation} />
: <p className="p-8 text-muted-foreground">Choose a workspace to browse its Evidence and locate the files to edit.</p>}
</main>;
}
function WorkspaceEvidence({ workspace, generation }: { workspace: string; generation: number }) {
const [filters, setFilters] = useState<Record<string, string>>({ q: "", kind: "", purpose: "", status: "", sort: "title", direction: "asc" });
const [page, setPage] = useState(1);
const [selected, setSelected] = useState("");
const list = useQuery({ queryKey: ["evidence", generation, workspace, filters, page], queryFn: () => listEvidence(workspace, { ...filters, page, page_size: 25 }) });
const detail = useQuery({ queryKey: ["evidence-detail", generation, workspace, selected], queryFn: () => getEvidence(workspace, selected), enabled: Boolean(selected) });
function filter(key: string, value: string) { setFilters(old => ({ ...old, [key]: value })); setPage(1); }
return <div className="flex min-h-0 flex-1 flex-col gap-5 p-6">
{list.data && <Instructions data={list.data} />}
<EvidenceSources workspace={workspace} sources={list.data?.source_reviews ?? []} />
<div className="flex flex-wrap items-end gap-3">
<label className="grid flex-1 gap-1 text-sm">Search Evidence<input className={control} value={filters.q} onChange={e => filter("q", e.target.value)} placeholder="Title, identifier or content" /></label>
<Select label="Kind" value={filters.kind} values={kinds} onChange={v => filter("kind", v)} />
<Select label="Purpose" value={filters.purpose} values={["disambiguation", "rewriting", "schema_linking", "sql_generation"]} onChange={v => filter("purpose", v)} />
<Select label="Status" value={filters.status} values={Object.keys(states)} labels={states} onChange={v => filter("status", v)} />
<Button variant="outline" disabled={list.isFetching || detail.isFetching} onClick={() => { void list.refetch(); if (selected) void detail.refetch(); }}>Refresh files</Button>
</div>
<details><summary className="cursor-pointer text-sm font-medium">More filters and sorting</summary>
<div className="mt-3 flex flex-wrap gap-3">{["concept", "table", "column", "source"].map(key => <label key={key} className="grid gap-1 text-sm capitalize">{key}<input className={control} value={filters[key] ?? ""} onChange={e => filter(key, e.target.value)} /></label>)}
<Select label="Sort by" value={filters.sort} values={["title", "id", "kind", "status"]} onChange={v => filter("sort", v)} all={false} />
<Select label="Order" value={filters.direction} values={["asc", "desc"]} labels={{ asc: "Ascending", desc: "Descending" }} onChange={v => filter("direction", v)} all={false} />
</div></details>
{list.isError && <p role="alert">{apiErrorMessage(list.error)}</p>}
{list.isPending && <p role="status">Loading Evidence files…</p>}
{list.data?.errors.map(error => <div key={error.file} role="alert" className="rounded-md border border-destructive/40 p-3 text-sm"><strong>{error.file}</strong><p>{error.message}</p><p>Correct this file before consolidation.</p></div>)}
<div className="grid min-h-0 gap-6 xl:grid-cols-[minmax(20rem,2fr)_minmax(25rem,3fr)]">
<section aria-label="Evidence list" className="min-w-0">
<p className="mb-2 text-sm text-muted-foreground">{list.data?.total ?? 0} Evidence units</p>
<div className="overflow-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-muted-foreground"><th className="py-2 pr-3 font-medium">Evidence</th><th className="pr-3 font-medium">Kind</th><th className="font-medium">Status</th></tr></thead>
<tbody>{list.data?.items.map(item => <tr key={item.id} className={`border-b ${selected === item.id ? "bg-muted" : ""}`}><td className="py-3 pr-3"><button className="text-left font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring" onClick={() => setSelected(item.id)}>{item.title}</button><div className="mt-1 break-all text-xs text-muted-foreground">{item.id}</div></td><td className="pr-3 align-top pt-3">{item.kind}</td><td className="align-top pt-3">{states[item.status]}</td></tr>)}</tbody>
</table></div>
{list.data?.total === 0 && <p className="py-6 text-sm text-muted-foreground">No Evidence matches these filters. Clear the filters or use the file instructions to create a unit.</p>}
<nav aria-label="Evidence pages" className="mt-4 flex items-center gap-3"><Button variant="outline" size="sm" disabled={page === 1 || list.isFetching} onClick={() => setPage(p => p-1)}>Previous</Button><span className="text-sm">Page {page}</span><Button variant="outline" size="sm" disabled={!list.data || page*25 >= list.data.total || list.isFetching} onClick={() => setPage(p => p+1)}>Next</Button></nav>
</section>
<section aria-label="Evidence detail" className="min-w-0">
{detail.isError ? <p role="alert">{apiErrorMessage(detail.error)}</p> : selected && detail.isPending ? <p role="status">Loading Evidence…</p> : detail.data?.item ? <UnitDetail item={detail.data.item} location={detail.data.location} /> : <p className="py-10 text-sm text-muted-foreground">Select an Evidence unit to read its complete content and provenance.</p>}
</section>
</div>
</div>;
}
function Select({ label, value, values, labels = {}, onChange, all = true }: { label: string; value: string; values: string[]; labels?: Record<string, string>; onChange: (value: string) => void; all?: boolean }) {
return <label className="grid gap-1 text-sm">{label}<select className={control} value={value} onChange={e => onChange(e.target.value)}>{all && <option value="">All</option>}{values.map(v => <option key={v} value={v}>{labels[v] ?? v.replaceAll("_", " ")}</option>)}</select></label>;
}
function Instructions({ data }: { data: EvidencePage }) {
const [kind, setKind] = useState("domain");
const example = `---\nschema_version: 4\nid: evidence:my-unit\nkind: ${kind}\nlanguage: en\npurposes: [sql_generation]\n---\n\n# My Evidence\n\n${templates[kind]}\n`;
return <div className="border-b pb-5">
<p className="mb-2 text-sm font-medium">{data.pending_revision ? "Consolidation is incomplete. The previous active content remains available." : data.active_revision ? "The core uses the last successfully consolidated version." : "Run the first consolidation to convert and activate the local archive."}</p>
{data.location.workspace ? <><p className="mb-1 text-xs text-muted-foreground">Workspace on the installation host</p><CopyText text={data.location.workspace} label="workspace path" /></> : <p role="alert" className="text-sm">A host file path is unavailable for this installation. Configure a persistent workspace-registry bind mount before editing files from the host.</p>}
<details className="mt-3"><summary className="cursor-pointer text-sm font-medium">Edit files, consolidate and commit</summary>
<ol className="mt-3 max-w-[75ch] list-decimal space-y-3 pl-5 text-sm">
<li>Edit the files under <code>evidence/curated/&lt;kind&gt;/</code> with your preferred editor. Keep existing IDs. Remove a unit's file to delete it; keep the curated directory.</li>
<li>Check Git status and the diff. Saving a file does not update recall.
{data.location.git_commands.slice(0, 2).map(command => <div className="mt-2" key={command}><CopyText text={command} label="Git inspection command" /></div>)}</li>
<li>From the installation directory, run this command. It validates and activates Evidence without a full preprocessing run. If needed, add <code>--installation /absolute/path/thothii-installation.yaml</code> immediately after <code>tht</code>.
<div className="mt-2"><CopyText text={data.location.command} label="consolidation command" /></div></li>
<li>After success, review the generated metadata and complete Git commit/push manually. These commands use a POSIX shell on the installation host. A remote host's path is not a file on this browser's computer.
{data.location.git_commands.slice(2).map(command => <div className="mt-2" key={command}><CopyText text={command} label="Git publication command" /></div>)}</li>
</ol>
<p className="mt-3 text-sm text-muted-foreground">Source drafts are preserved under evidence/source/. Maintain the refined files under evidence/curated/. New source acquisition and refresh are not part of consolidation.</p>
<details className="mt-4"><summary className="cursor-pointer text-sm font-medium">Create a manual Evidence file</summary><div className="mt-3"><Select label="Example kind" value={kind} values={kinds} onChange={setKind} all={false} /><p className="my-2 text-sm">Save as <code>evidence/curated/{kind}/my-unit.md</code> with a unique ID. Replace the example with your domain knowledge. The system records manual provenance.</p><pre className="overflow-auto rounded-md bg-muted p-3 text-xs">{example}</pre><CopyText text={example} label="Markdown example" showText={false} /></div></details>
</details>
</div>;
}
function UnitDetail({ item, location }: { item: EvidenceUnit; location: EvidencePage["location"] }) {
const source = item.provenance.kind === "manual" ? item.provenance.original : item.provenance;
const file = location.workspace ? `${location.workspace.replace(/\\/g, "/")}/evidence/${item.file}` : null;
return <article className="max-w-[75ch] space-y-5 pb-8">
<header><p className="text-xs text-muted-foreground">{states[item.status]} · {item.purposes.join(", ")}</p><h2 className="mt-1 font-serif text-xl font-semibold">{item.title}</h2></header>
{file && <CopyText text={file} label="Evidence file path" />}
{item.status !== "active" && <p className="text-sm text-muted-foreground">{["invalid", "removed"].includes(item.status) ? "The file is invalid or removed. Its last active content is shown here." : "This is the file version awaiting consolidation."} The core continues to use its last active version.</p>}
{Object.entries(item.applies_to).some(([, values]) => values.length > 0) && <section><h3 className="font-medium">Scope</h3>{Object.entries(item.applies_to).map(([name, values]) => values.length > 0 && <p className="mt-2 break-words text-sm" key={name}><span className="capitalize">{name}</span>: {values.join(", ")}</p>)}</section>}
{Object.entries(item.payload).map(([field, value]) => <section key={field}><h3 className="mb-2 text-sm font-semibold capitalize">{field.replaceAll("_", " ")}</h3>{typeof value === "string" ? field === "sql" ? <pre className="overflow-auto rounded-md bg-muted p-3 text-sm">{value}</pre> : <MarkdownView source={value} /> : Array.isArray(value) ? <ul className="list-inside list-disc text-sm">{value.map(v => <li key={v}>{v}</li>)}</ul> : <dl className="space-y-2 text-sm">{Object.entries(value).map(([key, text]) => <div key={key}><dt className="font-mono font-medium">{key || "(empty value)"}</dt><dd>{text}</dd></div>)}</dl>}</section>)}
<section className="border-t pt-4"><h3 className="font-medium">Provenance</h3><p className="mt-2 text-sm">{item.provenance.kind === "manual" ? `Manual declaration by ${item.provenance.declared_by}.` : "Derived from a source document."}</p>
{source?.source_file && <><p className="mt-2 break-all text-xs">{item.provenance.kind === "manual" ? "Original document (lineage): " : "Source: "}{source.source_file}</p>{source.supporting_excerpts?.map((text, i) => <p key={i} className="mt-2 whitespace-pre-wrap text-sm text-muted-foreground">“{text}”</p>)}</>}
</section>
{item.review_items.length > 0 && <section><h3 className="font-medium">Review required</h3>{item.review_items.map((review, i) => <p key={i} className="mt-2 text-sm">{review.message}</p>)}</section>}
</article>;
}