feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s
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:
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Archive repair review</title></head><body><div id="root"></div><script type="module" src="/e2e/fixtures/archive-repair.tsx"></script></body></html>
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ArchiveRepairWidget } from "../../src/widgets/ArchiveRepairWidget";
|
||||
import "../../src/index.css";
|
||||
|
||||
function Preview() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [responses, setResponses] = useState<unknown[]>([]);
|
||||
return <main className="mx-auto max-w-4xl p-4"><ArchiveRepairWidget key={step} descriptor={{
|
||||
id: `repair-${step}`, widget: "archive-repair", reserved: [], repair: {
|
||||
repair_id: "fixture", reason: "Memory defines an order by its number alone. Evidence requires the financial year too. Choose which shared definition to correct.",
|
||||
can_apply: true, choice: step ? "e" : null, saved: step > 0, indexed: step > 1,
|
||||
status: ["proposed", "pending_activation", "active"][Math.min(step, 2)], options: [
|
||||
{ id: "m", label: "Include the financial year in Memory", archive: "memory", target_id: "mem-order-key",
|
||||
before: { subject: "Order identity", detail: "Use the order number.", scope: "Sales" },
|
||||
content: { subject: "Order identity", detail: "Use order number and financial year.", scope: "Sales" } },
|
||||
{ id: "e", label: "Correct Evidence to use the order number", archive: "evidence", target_id: "evidence:order-key",
|
||||
before: { title: "Order key", payload: { rule: "Use order number and financial year." } },
|
||||
content: { title: "Order key", payload: { rule: "Use the order number." },
|
||||
provenance: { kind: "manual", declared_by: "Sales curator" } } },
|
||||
],
|
||||
},
|
||||
}} onRespond={response => { setResponses([...responses, response]); setStep(step + 1); }} />
|
||||
<output id="response" className="sr-only">{JSON.stringify(responses)}</output></main>;
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(<Preview />);
|
||||
@@ -85,7 +85,7 @@ dwh:
|
||||
supported_transports: [postgres_direct]
|
||||
`;
|
||||
|
||||
async function prepareF1Workspace(root) {
|
||||
async function prepareF1Workspace(root, memoryOnly = false) {
|
||||
const source = join(root, "workspace-source");
|
||||
const remote = join(root, "workspace-remote.git");
|
||||
secureDirectory(source);
|
||||
@@ -97,7 +97,8 @@ async function prepareF1Workspace(root) {
|
||||
" name: Fixture workspace",
|
||||
"",
|
||||
].join("\n"));
|
||||
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"), F1_WORKSPACE_DESCRIPTOR);
|
||||
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"),
|
||||
memoryOnly ? F1_WORKSPACE_DESCRIPTOR.split("dwh:")[0] : F1_WORKSPACE_DESCRIPTOR);
|
||||
|
||||
await runFixtureCommand("git", ["init", "--bare", "--initial-branch=main", remote], root);
|
||||
chmodSync(remote, 0o700);
|
||||
@@ -332,7 +333,7 @@ function cleanBackendEnvironment(overrides) {
|
||||
return { ...env, ...overrides };
|
||||
}
|
||||
|
||||
export async function createAuthenticationStack({ withF1Workspace = false } = {}) {
|
||||
export async function createAuthenticationStack({ withF1Workspace = false, memoryRuntime = {} } = {}) {
|
||||
const credentials = resolveFixtureCredentials();
|
||||
const root = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-e2e-"));
|
||||
secureDirectory(root);
|
||||
@@ -363,7 +364,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
const [frontendPort, backendPort] = await Promise.all([freeLoopbackPort(), freeLoopbackPort()]);
|
||||
const publicUrl = `http://127.0.0.1:${frontendPort}`;
|
||||
const backendUrl = `http://127.0.0.1:${backendPort}`;
|
||||
const workspace = withF1Workspace ? await prepareF1Workspace(root) : undefined;
|
||||
const workspace = withF1Workspace ? await prepareF1Workspace(root, Object.keys(memoryRuntime).length > 0) : undefined;
|
||||
const provider = await startFakeOidcProvider({
|
||||
directory: providerRoot,
|
||||
registration: {
|
||||
@@ -515,6 +516,7 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
|
||||
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
|
||||
} : {}),
|
||||
...memoryRuntime,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -535,6 +537,10 @@ export async function createAuthenticationStack({ withF1Workspace = false } = {}
|
||||
|
||||
return {
|
||||
publicUrl,
|
||||
fixtureWorkspaceRoot() {
|
||||
if (!workspace) throw safeError("e2e_workspace_fixture_missing");
|
||||
return join(registryRoot, "repo", workspace.id);
|
||||
},
|
||||
localAccount(account) {
|
||||
const found = accounts[account];
|
||||
if (!found) throw safeError("e2e_local_account_unknown");
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
// So we skip the first two args after node (-c <configPath>).
|
||||
|
||||
const argv = process.argv.slice(2); // drop "node" + script path
|
||||
// Archive integration tests run the actual harness through the production runner.
|
||||
// Unrelated session/LLM operations remain deterministic fixtures.
|
||||
if ((argv[0] === "memory" || argv[0] === "evidence" && argv[1] === "admin") && process.env.THT_E2E_MEMORY_BIN) {
|
||||
const { spawnSync } = await import("node:child_process");
|
||||
const result = spawnSync(process.env.THT_E2E_MEMORY_BIN, argv, {
|
||||
env: process.env, stdio: ["ignore", "inherit", "inherit", 3],
|
||||
});
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
// Strip leading "-c <value>" pair (ThtRunner always prepends this).
|
||||
let args = argv;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
|
||||
<body><div id="root"></div><script type="module" src="./memory-review.tsx"></script></body></html>
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { MemoryReviewWidget } from "../../src/widgets/MemoryReviewWidget";
|
||||
import "../../src/index.css";
|
||||
|
||||
const card = { family: "sql_rule", subject: "Join orders within the same financial year",
|
||||
detail: "Join order lines using both order_id and financial_year. The order number repeats across years.",
|
||||
scope: "Sales reporting", rationale: "Confirmed during review of annual totals.",
|
||||
question: "What is the total value of shipped orders?", sql: "", concepts: ["Order grain"],
|
||||
dependencies: [{ database: "warehouse", schema_name: "sales", table: "orders", column: "financial_year" }],
|
||||
links: [] };
|
||||
const descriptor = { id: "review", widget: "memory-review", reserved: [],
|
||||
summary: { summary_id: "fixture", items: [
|
||||
{ id: "rule", card, reason: "Preserve the approved join correction.", source_seqs: [12], before: null, target_id: null },
|
||||
{ id: "solved", card: { ...card, family: "solved_question", subject: "Shipped orders by year",
|
||||
sql: "SELECT financial_year, SUM(total) FROM sales.orders GROUP BY financial_year" },
|
||||
reason: "Keep the approved solution as an example.", source_seqs: [18], before: null, target_id: null },
|
||||
] } };
|
||||
createRoot(document.getElementById("root")!).render(<main className="mx-auto max-w-3xl p-4">
|
||||
<MemoryReviewWidget descriptor={descriptor} onRespond={response => {
|
||||
document.querySelector("#response")!.textContent = JSON.stringify(response);
|
||||
}} /><output id="response" /></main>);
|
||||
Reference in New Issue
Block a user