From b06aed7c6150ddc58959ace576e48ed6d1dd4364 Mon Sep 17 00:00:00 2001 From: "Admilson B. F. Cossa" Date: Fri, 28 Aug 2026 11:42:10 +0200 Subject: [PATCH 01/14] feat(samples): add auditable incident decision gate --- package.json | 2 + packages/core/package.json | 4 +- .../samples/incident-decision-gate.sample.js | 201 ++++++++++++++++++ .../core/scripts/check-package-consumer.mjs | 21 +- 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 packages/core/samples/incident-decision-gate.sample.js diff --git a/package.json b/package.json index 042be36..c9596d1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "check:api-declarations": "npm --workspace @workit/core run check:api-declarations", "check:compat-previous": "npm --workspace @workit/core run check:compat-previous", "check:node-compatibility": "npm --workspace @workit/core run check:node-compatibility", + "check:incident-gate": "npm --workspace @workit/core run check:incident-gate", "check:pack-reproducibility": "npm --workspace @workit/core run check:pack-reproducibility", "check:size": "npm --workspace @workit/core run check:size", "report:size": "npm --workspace @workit/core run report:size", @@ -57,6 +58,7 @@ "sample:all": "npm --workspace @workit/core run sample:all", "sample:agent": "npm --workspace @workit/core run sample:agent", "sample:race": "npm --workspace @workit/core run sample:race", + "sample:incident-gate": "npm --workspace @workit/core run sample:incident-gate", "sample:rag": "npm --workspace @workit/core run sample:rag", "sample:batch": "npm --workspace @workit/core run sample:batch", "sample:stream": "npm --workspace @workit/core run sample:stream", diff --git a/packages/core/package.json b/packages/core/package.json index 60166db..202ac22 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -178,6 +178,7 @@ "check:benchmark": "npm run build && node scripts/check-benchmark.mjs", "check:candidates-performance": "npm run build && node --expose-gc scripts/check-candidates-performance.mjs", "check:candidate-scenarios": "npm run build && node scripts/check-candidate-scenarios.mjs", + "check:incident-gate": "npm run build && node samples/incident-decision-gate.sample.js", "check:context-performance": "npm run build && node scripts/check-context-performance.mjs", "check:1b": "npm run build && node scripts/check-1b-benchmark.mjs", "check:leak": "npm run build && node --expose-gc scripts/check-leak.mjs", @@ -210,6 +211,7 @@ "sample:all": "npm run build && node samples/safer-promise-all.sample.js", "sample:agent": "npm run build && node samples/agent-tree-cancel.sample.js", "sample:race": "npm run build && node samples/race-providers.sample.js", + "sample:incident-gate": "npm run build && node samples/incident-decision-gate.sample.js", "sample:rag": "npm run build && node samples/budget-rag.sample.js", "sample:batch": "npm run build && node samples/batch-upload.sample.js", "sample:stream": "npm run build && node samples/streaming-summarizer.sample.js", @@ -226,7 +228,7 @@ "soak:24h": "npm run build && node --expose-gc scripts/soak-24h.mjs", "test": "npm run build && vitest run --maxWorkers=1", "test:coverage": "npm run build && vitest run --coverage --maxWorkers=1", - "verify": "npm run typecheck && npm run check:no-network && npm run check:headers && npm run check:tests && npm test && npm run test:evidence && npm run check:evidence-ledger && npm run check:security && npm run check:vulnerabilities && npm run check:sbom && npm run check:api && npm run check:api-declarations && npm run check:size && npm run check:benchmark && npm run check:candidates-performance && npm run check:candidate-scenarios && npm run check:context-performance && npm run check:1b && npm run check:leak && npm run check:stream-memory && npm run check:soak && npm run check:exporter-stress && npm run check:package-consumer && npm run check:compat-previous && npm run check:claims && npm run check:public-proof && npm run check:worker-contract && npm run check:release-policy && npm run check:pack-reproducibility && npm run pack:dry" + "verify": "npm run typecheck && npm run check:no-network && npm run check:headers && npm run check:tests && npm test && npm run test:evidence && npm run check:evidence-ledger && npm run check:security && npm run check:vulnerabilities && npm run check:sbom && npm run check:api && npm run check:api-declarations && npm run check:size && npm run check:benchmark && npm run check:candidates-performance && npm run check:candidate-scenarios && npm run check:incident-gate && npm run check:context-performance && npm run check:1b && npm run check:leak && npm run check:stream-memory && npm run check:soak && npm run check:exporter-stress && npm run check:package-consumer && npm run check:compat-previous && npm run check:claims && npm run check:public-proof && npm run check:worker-contract && npm run check:release-policy && npm run check:pack-reproducibility && npm run pack:dry" }, "engines": { "node": ">=20.11" diff --git a/packages/core/samples/incident-decision-gate.sample.js b/packages/core/samples/incident-decision-gate.sample.js new file mode 100644 index 0000000..b64ed32 --- /dev/null +++ b/packages/core/samples/incident-decision-gate.sample.js @@ -0,0 +1,201 @@ +/** + * Auditable AI incident-decision gate. + * + * @author Admilson B. F. Cossa + * SPDX-License-Identifier: Apache-2.0 + * + * Selects a grounded incident recommendation, contains transient retries inside + * one end-to-end deadline, and stops before a production change that requires + * operator authority. The providers are deterministic local fixtures so the + * policy contract runs without credentials or network access. + */ + +import assert from "node:assert/strict"; +import { createBudget, run } from "@workit/core"; +import { firstAcceptable } from "@workit/core/candidates"; + +const DISPOSITION = Object.freeze({ + RETRY: "retry_same_candidate", + NEXT: "try_next_candidate", + REQUIRE_INPUT: "requires_user_input", +}); +const REASON = Object.freeze({ + TRANSIENT_PROVIDER: "transient_provider_failure", + PROVIDER_UNAVAILABLE: "provider_unavailable", + APPROVAL_REQUIRED: "production_change_requires_approval", + EVIDENCE_MISSING: "incident_evidence_missing", + CONFIDENCE_LOW: "incident_confidence_too_low", +}); +const RISK = Object.freeze({ + READ_ONLY: "read_only", + PRODUCTION_WRITE: "production_write", +}); +const ACTION = Object.freeze({ + COLLECT_DIAGNOSTICS: "collect_diagnostics", + ROLLBACK: "rollback_deployment", +}); +const MIN_CONFIDENCE = 0.85; +const MIN_EVIDENCE_REFERENCES = 2; +const END_TO_END_BUDGET_MS = 2_000; +const RETRY_LIMIT = 1; +const MAX_RETAINED_ATTEMPTS = 4; +const RETRY_BUDGET_UNIT = "retries"; +const RetryBudget = createBudget("IncidentDecisionRetryBudget", { unit: RETRY_BUDGET_UNIT }); + +class TransientProviderError extends Error {} +class ApprovalRequiredError extends Error {} + +const FAILURE_POLICY = new Map([ + [TransientProviderError, Object.freeze({ + disposition: DISPOSITION.RETRY, + reasonCode: REASON.TRANSIENT_PROVIDER, + })], + [ApprovalRequiredError, Object.freeze({ + disposition: DISPOSITION.REQUIRE_INPUT, + reasonCode: REASON.APPROVAL_REQUIRED, + })], +]); +const DEFAULT_FAILURE_POLICY = Object.freeze({ + disposition: DISPOSITION.NEXT, + reasonCode: REASON.PROVIDER_UNAVAILABLE, +}); + +const selectionCalls = []; +const selectionCandidates = [ + candidate("fast-triage", "https://triage.internal", async (ctx) => { + selectionCalls.push(`fast-triage:${ctx.attempt}`); + return recommendation(ACTION.COLLECT_DIAGNOSTICS, RISK.READ_ONLY, 0.97, []); + }), + candidate("grounded-reasoner", "https://reasoner.internal", async (ctx) => { + selectionCalls.push(`grounded-reasoner:${ctx.attempt}`); + if (ctx.attempt === 1) throw new TransientProviderError("provider overloaded"); + return recommendation(ACTION.COLLECT_DIAGNOSTICS, RISK.READ_ONLY, 0.93, [ + "trace:checkout-timeout", + "metric:payment-error-rate", + ]); + }), + candidate("unbounded-autopilot", "https://autopilot.internal", async (ctx) => { + selectionCalls.push(`unbounded-autopilot:${ctx.attempt}`); + return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.99, ["trace:x", "metric:y"]); + }), +]; + +const selection = await run.context.with( + RetryBudget, + { spent: 0, limit: RETRY_LIMIT, unit: RETRY_BUDGET_UNIT }, + async () => { + const outcome = await selectIncidentRecommendation(selectionCandidates); + return { outcome, retryBudget: run.context.budget(RetryBudget) }; + }, +); + +assert.equal(selection.outcome.status, "accepted"); +assert.equal(selection.outcome.candidate.name, "grounded-reasoner"); +assert.equal(selection.outcome.value.action, ACTION.COLLECT_DIAGNOSTICS); +assert.deepEqual(selectionCalls, ["fast-triage:1", "grounded-reasoner:1", "grounded-reasoner:2"]); +assert.deepEqual(selection.outcome.evidence.map(({ decision }) => decision), [ + "quality_rejected", + DISPOSITION.RETRY, + "accepted", +]); +assert.deepEqual(selection.retryBudget, { spent: 1, limit: 1, unit: RETRY_BUDGET_UNIT }); +assert.equal(selection.outcome.droppedEvidence, 0); +assertMetadataIsRedacted(selection.outcome.evidence); + +const approvalCalls = []; +let executedProductionChanges = 0; +const approvalCandidates = [ + candidate("rollback-planner", "https://rollback.internal", async (ctx) => { + approvalCalls.push(`rollback-planner:${ctx.attempt}`); + return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.96, [ + "deploy:checkout-v42", + "metric:checkout-error-rate", + ]); + }), + candidate("unsafe-fallback", "https://unsafe.internal", async (ctx) => { + approvalCalls.push(`unsafe-fallback:${ctx.attempt}`); + executedProductionChanges++; + return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.99, ["deploy:x", "metric:y"]); + }), +]; +const approval = await selectIncidentRecommendation(approvalCandidates); + +assert.equal(approval.status, "requires_user_input"); +assert.equal(approval.reasonCode, REASON.APPROVAL_REQUIRED); +assert.deepEqual(approvalCalls, ["rollback-planner:1"]); +assert.equal(executedProductionChanges, 0); +assert.equal(approval.evidence[0]?.decision, DISPOSITION.REQUIRE_INPUT); +assertMetadataIsRedacted(approval.evidence); + +process.stdout.write(`${JSON.stringify({ + sample: "incident-decision-gate", + selection: { + status: selection.outcome.status, + selectedCandidate: selection.outcome.candidate.name, + action: selection.outcome.value.action, + decisions: selection.outcome.evidence.map(({ decision }) => decision), + admittedCalls: selectionCalls, + retryBudget: selection.retryBudget, + droppedEvidence: selection.outcome.droppedEvidence, + credentialsRedacted: evidenceIsRedacted(selection.outcome.evidence), + }, + approval: { + status: approval.status, + reasonCode: approval.reasonCode, + admittedCalls: approvalCalls, + productionChangesExecuted: executedProductionChanges, + credentialsRedacted: evidenceIsRedacted(approval.evidence), + }, +})}\n`); + +function selectIncidentRecommendation(candidates) { + const deadlineAt = Date.now() + END_TO_END_BUDGET_MS; + return firstAcceptable(candidates, { + execute: async (provider, ctx) => enforceAuthority(await provider.propose(ctx)), + accept: assessRecommendation, + classifyFailure, + retry: { times: 2, initialDelay: 0, jitter: false, retryBudget: RetryBudget }, + deadlineAt, + evidence: { maxAttempts: MAX_RETAINED_ATTEMPTS }, + candidateMetadata: ({ name, endpoint, apiKey }) => ({ name, endpoint, apiKey }), + }); +} + +function assessRecommendation(value) { + const failedRule = [ + [value.evidence.length < MIN_EVIDENCE_REFERENCES, REASON.EVIDENCE_MISSING], + [value.confidence < MIN_CONFIDENCE, REASON.CONFIDENCE_LOW], + ].find(([failed]) => failed); + return failedRule === undefined + ? { accepted: true } + : { accepted: false, reasonCode: failedRule[1] }; +} + +function enforceAuthority(value) { + if (value.risk === RISK.PRODUCTION_WRITE) { + throw new ApprovalRequiredError("operator approval required before production mutation"); + } + return value; +} + +function classifyFailure(error) { + const policy = [...FAILURE_POLICY].find(([ErrorType]) => error instanceof ErrorType)?.[1]; + return policy ?? DEFAULT_FAILURE_POLICY; +} + +function candidate(name, endpoint, propose) { + return Object.freeze({ name, endpoint, apiKey: `secret-for-${name}`, propose }); +} + +function recommendation(action, risk, confidence, evidence) { + return Object.freeze({ action, risk, confidence, evidence: Object.freeze(evidence) }); +} + +function assertMetadataIsRedacted(evidence) { + assert.equal(evidenceIsRedacted(evidence), true); + assert.equal(JSON.stringify(evidence).includes("secret-for-"), false); +} + +function evidenceIsRedacted(evidence) { + return evidence.every(({ metadata }) => metadata?.apiKey === "[redacted]"); +} diff --git a/packages/core/scripts/check-package-consumer.mjs b/packages/core/scripts/check-package-consumer.mjs index 24570a1..dd1c373 100644 --- a/packages/core/scripts/check-package-consumer.mjs +++ b/packages/core/scripts/check-package-consumer.mjs @@ -9,7 +9,7 @@ */ import { execFile } from "node:child_process"; -import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { access, copyFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import { homedir, tmpdir } from "node:os"; import { delimiter, dirname, join, resolve } from "node:path"; @@ -60,6 +60,24 @@ try { timeout: 120_000, }); + const incidentGateFixture = "incident-decision-gate.sample.js"; + await copyFile( + join(ROOT, "samples", incidentGateFixture), + join(temp, incidentGateFixture), + ); + const { stdout: incidentGateStdout } = await execFileAsync( + process.execPath, + [incidentGateFixture], + { cwd: temp, timeout: 120_000 }, + ); + const incidentGateResult = JSON.parse(incidentGateStdout.trim()); + if (incidentGateResult.selection?.status !== "accepted" + || incidentGateResult.approval?.status !== "requires_user_input" + || incidentGateResult.approval?.productionChangesExecuted !== 0 + || incidentGateResult.selection?.credentialsRedacted !== true) { + throw new Error("Installed-package incident decision gate failed"); + } + await writeFile(join(temp, "otel-no-peer.mjs"), ` import { attachOpenTelemetry } from "@workit/core/otel"; @@ -1091,6 +1109,7 @@ try { packageConsumer: "ok", runtimeFixtures: nodeOnly ? "node-only" : "ok", frameworkFixtures: "ok", + incidentDecisionGate: "ok", frameworks: ["express", "fastify", "trpc", "next", "vercel-ai"], tarball: pack.filename, })); From 8929dea510c46942f62b31b453393f1b84c4af3c Mon Sep 17 00:00:00 2001 From: "Admilson B. F. Cossa" Date: Fri, 28 Aug 2026 11:42:13 +0200 Subject: [PATCH 02/14] feat(site): visualize incident decision evidence --- .../scripts/generate-evidence.mjs | 1 + apps/use-cases-site/scripts/smoke-runtime.mjs | 15 ++ .../scripts/test-data-contract.mjs | 79 ++++++---- apps/use-cases-site/server/runners.mjs | 39 +++++ .../data/generated/evidence-snapshots.json | 38 +++++ apps/use-cases-site/src/data/useCases.ts | 137 ++++++++++++++++++ 6 files changed, 283 insertions(+), 26 deletions(-) diff --git a/apps/use-cases-site/scripts/generate-evidence.mjs b/apps/use-cases-site/scripts/generate-evidence.mjs index 2e21334..5ba7981 100644 --- a/apps/use-cases-site/scripts/generate-evidence.mjs +++ b/apps/use-cases-site/scripts/generate-evidence.mjs @@ -19,6 +19,7 @@ const samples = [ { id: "agent-tree-cancel", path: "packages/core/samples/agent-tree-cancel.sample.js" }, { id: "conversation-agent", path: "packages/core/samples/conversation-agent.sample.js" }, { id: "race-providers", path: "packages/core/samples/race-providers.sample.js" }, + { id: "incident-decision-gate", path: "packages/core/samples/incident-decision-gate.sample.js" }, { id: "budget-rag", path: "packages/core/samples/budget-rag.sample.js" }, ]; diff --git a/apps/use-cases-site/scripts/smoke-runtime.mjs b/apps/use-cases-site/scripts/smoke-runtime.mjs index cb152d8..c3b7ae1 100644 --- a/apps/use-cases-site/scripts/smoke-runtime.mjs +++ b/apps/use-cases-site/scripts/smoke-runtime.mjs @@ -37,6 +37,7 @@ try { await waitForHealth(); await assertVibeCodingRun(); await assertConversationRun(); + await assertIncidentDecisionGateRun(); await assertRagRun(); await assertUnknownExample(); process.stdout.write("site-runtime-smoke: passed\n"); @@ -128,6 +129,20 @@ async function assertRagRun() { assertLine(result.receipt, "audit.sources: 2"); } +async function assertIncidentDecisionGateRun() { + const result = await getJson("/api/examples/incident-decision-gate/run"); + + assert.equal(result.source, "live-node"); + assert.equal(result.sample, "incident-decision-gate"); + assertLine(result.events, "quality_rejected -> retry_same_candidate -> accepted"); + assertLine(result.events, "approval: requires_user_input"); + assertLine(result.receipt, "selectedCandidate: grounded-reasoner"); + assertLine(result.receipt, "retryBudget: 1/1"); + assertLine(result.receipt, "productionChangesExecuted: 0"); + assertLine(result.receipt, "credentialsRedacted: true"); + assertNoLines(result.receipt, ["secret-for-"]); +} + async function assertUnknownExample() { const response = await fetch(`${origin}/api/examples/missing/run`); const body = await response.json(); diff --git a/apps/use-cases-site/scripts/test-data-contract.mjs b/apps/use-cases-site/scripts/test-data-contract.mjs index ca402a0..b20e1d2 100644 --- a/apps/use-cases-site/scripts/test-data-contract.mjs +++ b/apps/use-cases-site/scripts/test-data-contract.mjs @@ -60,6 +60,25 @@ const exampleContracts = [ ], liveEvents: ["task:cancelled", "race_lost"], }, + { + id: "incident-decision-gate", + sampleId: "incident-decision-gate", + samplePath: "packages/core/samples/incident-decision-gate.sample.js", + liveReceipt: [ + "runtime: @workit/core", + "sample: incident-decision-gate", + "selectedCandidate: grounded-reasoner", + "retryBudget: 1/1", + "approval.reasonCode: production_change_requires_approval", + "productionChangesExecuted: 0", + "credentialsRedacted: true", + ], + liveEvents: [ + "quality_rejected -> retry_same_candidate -> accepted", + "approval: requires_user_input", + "productionChangesExecuted: 0", + ], + }, { id: "rag-pipeline", sampleId: "budget-rag", @@ -88,11 +107,6 @@ const deniedDisplayedStrings = [ "killer", ]; -await assertUseCasesMatchExecutableSamples(); -await assertLiveRunnersMatchUseCaseContracts(); -await assertRuntimeApiUsesStaticFallbackOnPublicPages(); -process.stdout.write("site-data-contract: passed\n"); - async function assertUseCasesMatchExecutableSamples() { const snapshots = readJson(snapshotsPath); const { useCases } = await importUseCases(); @@ -187,29 +201,37 @@ function assertUseCaseLinesMatchSnapshot(useCase, result) { assertLine(rendered, `sample: ${result.sample}`, `${useCase.id} rendered lines`); - switch (result.sample) { - case "agent-tree-cancel": - assertLine(rendered, `reason.tag: ${result.reason.tag}`, `${useCase.id} rendered lines`); - assertLine(rendered, `cleanups.count: ${result.cleanups.length}`, `${useCase.id} rendered lines`); - break; - case "conversation-agent": - assertLine(rendered, `tokens: ${result.tokens.length}`, `${useCase.id} rendered lines`); - assertLine(rendered, `toolResults: ${result.toolResults.join(", ")}`, `${useCase.id} rendered lines`); - assertLine(rendered, `memoryWrites: ${result.memoryWrites}`, `${useCase.id} rendered lines`); - break; - case "race-providers": - assertLine(rendered, `winner: ${result.winner}`, `${useCase.id} rendered lines`); - assertLine(rendered, `cancelledProviders.count: ${result.cancelledProviders.length}`, `${useCase.id} rendered lines`); - break; - case "budget-rag": - assertLine(rendered, `spent: ${result.spent}`, `${useCase.id} rendered lines`); - assertLine(rendered, `audit.sources: ${result.audits[0].sources}`, `${useCase.id} rendered lines`); - break; - default: - assert.fail(`Unhandled sample result ${result.sample}.`); - } + const assertions = SAMPLE_ASSERTIONS[result.sample]; + assert.equal(typeof assertions, "function", `Unhandled sample result ${result.sample}.`); + assertions(rendered, result, useCase.id); } +const SAMPLE_ASSERTIONS = Object.freeze({ + "agent-tree-cancel": (rendered, result, id) => { + assertLine(rendered, `reason.tag: ${result.reason.tag}`, `${id} rendered lines`); + assertLine(rendered, `cleanups.count: ${result.cleanups.length}`, `${id} rendered lines`); + }, + "conversation-agent": (rendered, result, id) => { + assertLine(rendered, `tokens: ${result.tokens.length}`, `${id} rendered lines`); + assertLine(rendered, `toolResults: ${result.toolResults.join(", ")}`, `${id} rendered lines`); + assertLine(rendered, `memoryWrites: ${result.memoryWrites}`, `${id} rendered lines`); + }, + "race-providers": (rendered, result, id) => { + assertLine(rendered, `winner: ${result.winner}`, `${id} rendered lines`); + assertLine(rendered, `cancelledProviders.count: ${result.cancelledProviders.length}`, `${id} rendered lines`); + }, + "incident-decision-gate": (rendered, result, id) => { + assertLine(rendered, `selectedCandidate: ${result.selection.selectedCandidate}`, `${id} rendered lines`); + assertLine(rendered, `retryBudget: ${result.selection.retryBudget.spent}/${result.selection.retryBudget.limit}`, `${id} rendered lines`); + assertLine(rendered, `approval.reasonCode: ${result.approval.reasonCode}`, `${id} rendered lines`); + assertLine(rendered, `productionChangesExecuted: ${result.approval.productionChangesExecuted}`, `${id} rendered lines`); + }, + "budget-rag": (rendered, result, id) => { + assertLine(rendered, `spent: ${result.spent}`, `${id} rendered lines`); + assertLine(rendered, `audit.sources: ${result.audits[0].sources}`, `${id} rendered lines`); + }, +}); + function assertEvidencePathsExist(useCase) { for (const item of useCase.evidence) { assert.equal(item.status, "tracked"); @@ -281,3 +303,8 @@ async function importBundledTypeScript(relativePath, outputName) { function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); } + +await assertUseCasesMatchExecutableSamples(); +await assertLiveRunnersMatchUseCaseContracts(); +await assertRuntimeApiUsesStaticFallbackOnPublicPages(); +process.stdout.write("site-data-contract: passed\n"); diff --git a/apps/use-cases-site/server/runners.mjs b/apps/use-cases-site/server/runners.mjs index 57c503c..f94a8a3 100644 --- a/apps/use-cases-site/server/runners.mjs +++ b/apps/use-cases-site/server/runners.mjs @@ -5,17 +5,22 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { execFile } from "node:child_process"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { CancellationError, ContextBagImpl, CostBudget, group, run } from "@workit/core"; const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url))); +const executeFile = promisify(execFile); +const INCIDENT_GATE_SAMPLE_PATH = "packages/core/samples/incident-decision-gate.sample.js"; export const runners = { "vibe-coding-agent": runAgentTree, "conversation-agent": runConversationAgent, "provider-fallback": runProviderFallback, + "incident-decision-gate": runIncidentDecisionGate, "rag-pipeline": runRagPipeline, }; @@ -91,6 +96,30 @@ async function runProviderFallback() { }; } +async function runIncidentDecisionGate() { + const result = await runJsonSample(INCIDENT_GATE_SAMPLE_PATH); + const decisions = result.selection.decisions.join(" -> "); + + return { + sample: result.sample, + events: [ + `selection: ${decisions}`, + `approval: ${result.approval.status}`, + `productionChangesExecuted: ${result.approval.productionChangesExecuted}`, + ], + receipt: [ + "runtime: @workit/core", + `sample: ${result.sample}`, + `selectedCandidate: ${result.selection.selectedCandidate}`, + `retryBudget: ${result.selection.retryBudget.spent}/${result.selection.retryBudget.limit}`, + `approval.reasonCode: ${result.approval.reasonCode}`, + `productionChangesExecuted: ${result.approval.productionChangesExecuted}`, + `credentialsRedacted: ${result.selection.credentialsRedacted}`, + ], + code: await readSample(INCIDENT_GATE_SAMPLE_PATH), + }; +} + async function runRagPipeline() { const events = []; const budget = { spent: 0, limit: 10, unit: "USD" }; @@ -257,6 +286,16 @@ function readSample(path) { return readFile(resolve(repoRoot, path), "utf8"); } +async function runJsonSample(path) { + const { stdout } = await executeFile(process.execPath, [path], { + cwd: repoRoot, + env: process.env, + maxBuffer: 1024 * 1024, + windowsHide: true, + }); + return JSON.parse(stdout.trim()); +} + function sleep(ms, signal) { return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); diff --git a/apps/use-cases-site/src/data/generated/evidence-snapshots.json b/apps/use-cases-site/src/data/generated/evidence-snapshots.json index 4513cdd..d578e18 100644 --- a/apps/use-cases-site/src/data/generated/evidence-snapshots.json +++ b/apps/use-cases-site/src/data/generated/evidence-snapshots.json @@ -58,6 +58,44 @@ }, "source": "/**\n * Provider race sample.\n *\n * @author Admilson B. F. Cossa\n * SPDX-License-Identifier: Apache-2.0\n *\n * Races three provider calls and cancels the losing requests through the shared\n * task signal.\n */\n\nimport assert from \"node:assert/strict\";\nimport { CancellationError, run } from \"../dist/index.js\";\n\nconst cancelledProviders = [];\n\nconst winner = await run.race([\n provider(\"openai\", 50),\n provider(\"anthropic\", 10),\n provider(\"gemini\", 80),\n]);\n\nassert.equal(winner.provider, \"anthropic\");\nassert.deepEqual(cancelledProviders.sort(), [\"gemini\", \"openai\"]);\n\nprocess.stdout.write(`${JSON.stringify({\n sample: \"race-providers\",\n winner: winner.provider,\n cancelledProviders: cancelledProviders.sort(),\n})}\\n`);\n\nfunction provider(name, latencyMs) {\n return async (ctx) => {\n try {\n await sleep(latencyMs, ctx.signal);\n return { provider: name, text: `${name}:ok` };\n } catch (err) {\n if (err instanceof CancellationError) cancelledProviders.push(name);\n throw err;\n }\n };\n}\n\nfunction sleep(ms, signal) {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n reject(signal.reason);\n }, { once: true });\n });\n}\n" }, + "incident-decision-gate": { + "path": "packages/core/samples/incident-decision-gate.sample.js", + "result": { + "sample": "incident-decision-gate", + "selection": { + "status": "accepted", + "selectedCandidate": "grounded-reasoner", + "action": "collect_diagnostics", + "decisions": [ + "quality_rejected", + "retry_same_candidate", + "accepted" + ], + "admittedCalls": [ + "fast-triage:1", + "grounded-reasoner:1", + "grounded-reasoner:2" + ], + "retryBudget": { + "spent": 1, + "limit": 1, + "unit": "retries" + }, + "droppedEvidence": 0, + "credentialsRedacted": true + }, + "approval": { + "status": "requires_user_input", + "reasonCode": "production_change_requires_approval", + "admittedCalls": [ + "rollback-planner:1" + ], + "productionChangesExecuted": 0, + "credentialsRedacted": true + } + }, + "source": "/**\n * Auditable AI incident-decision gate.\n *\n * @author Admilson B. F. Cossa\n * SPDX-License-Identifier: Apache-2.0\n *\n * Selects a grounded incident recommendation, contains transient retries inside\n * one end-to-end deadline, and stops before a production change that requires\n * operator authority. The providers are deterministic local fixtures so the\n * policy contract runs without credentials or network access.\n */\n\nimport assert from \"node:assert/strict\";\nimport { createBudget, run } from \"@workit/core\";\nimport { firstAcceptable } from \"@workit/core/candidates\";\n\nconst DISPOSITION = Object.freeze({\n RETRY: \"retry_same_candidate\",\n NEXT: \"try_next_candidate\",\n REQUIRE_INPUT: \"requires_user_input\",\n});\nconst REASON = Object.freeze({\n TRANSIENT_PROVIDER: \"transient_provider_failure\",\n PROVIDER_UNAVAILABLE: \"provider_unavailable\",\n APPROVAL_REQUIRED: \"production_change_requires_approval\",\n EVIDENCE_MISSING: \"incident_evidence_missing\",\n CONFIDENCE_LOW: \"incident_confidence_too_low\",\n});\nconst RISK = Object.freeze({\n READ_ONLY: \"read_only\",\n PRODUCTION_WRITE: \"production_write\",\n});\nconst ACTION = Object.freeze({\n COLLECT_DIAGNOSTICS: \"collect_diagnostics\",\n ROLLBACK: \"rollback_deployment\",\n});\nconst MIN_CONFIDENCE = 0.85;\nconst MIN_EVIDENCE_REFERENCES = 2;\nconst END_TO_END_BUDGET_MS = 2_000;\nconst RETRY_LIMIT = 1;\nconst MAX_RETAINED_ATTEMPTS = 4;\nconst RETRY_BUDGET_UNIT = \"retries\";\nconst RetryBudget = createBudget(\"IncidentDecisionRetryBudget\", { unit: RETRY_BUDGET_UNIT });\n\nclass TransientProviderError extends Error {}\nclass ApprovalRequiredError extends Error {}\n\nconst FAILURE_POLICY = new Map([\n [TransientProviderError, Object.freeze({\n disposition: DISPOSITION.RETRY,\n reasonCode: REASON.TRANSIENT_PROVIDER,\n })],\n [ApprovalRequiredError, Object.freeze({\n disposition: DISPOSITION.REQUIRE_INPUT,\n reasonCode: REASON.APPROVAL_REQUIRED,\n })],\n]);\nconst DEFAULT_FAILURE_POLICY = Object.freeze({\n disposition: DISPOSITION.NEXT,\n reasonCode: REASON.PROVIDER_UNAVAILABLE,\n});\n\nconst selectionCalls = [];\nconst selectionCandidates = [\n candidate(\"fast-triage\", \"https://triage.internal\", async (ctx) => {\n selectionCalls.push(`fast-triage:${ctx.attempt}`);\n return recommendation(ACTION.COLLECT_DIAGNOSTICS, RISK.READ_ONLY, 0.97, []);\n }),\n candidate(\"grounded-reasoner\", \"https://reasoner.internal\", async (ctx) => {\n selectionCalls.push(`grounded-reasoner:${ctx.attempt}`);\n if (ctx.attempt === 1) throw new TransientProviderError(\"provider overloaded\");\n return recommendation(ACTION.COLLECT_DIAGNOSTICS, RISK.READ_ONLY, 0.93, [\n \"trace:checkout-timeout\",\n \"metric:payment-error-rate\",\n ]);\n }),\n candidate(\"unbounded-autopilot\", \"https://autopilot.internal\", async (ctx) => {\n selectionCalls.push(`unbounded-autopilot:${ctx.attempt}`);\n return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.99, [\"trace:x\", \"metric:y\"]);\n }),\n];\n\nconst selection = await run.context.with(\n RetryBudget,\n { spent: 0, limit: RETRY_LIMIT, unit: RETRY_BUDGET_UNIT },\n async () => {\n const outcome = await selectIncidentRecommendation(selectionCandidates);\n return { outcome, retryBudget: run.context.budget(RetryBudget) };\n },\n);\n\nassert.equal(selection.outcome.status, \"accepted\");\nassert.equal(selection.outcome.candidate.name, \"grounded-reasoner\");\nassert.equal(selection.outcome.value.action, ACTION.COLLECT_DIAGNOSTICS);\nassert.deepEqual(selectionCalls, [\"fast-triage:1\", \"grounded-reasoner:1\", \"grounded-reasoner:2\"]);\nassert.deepEqual(selection.outcome.evidence.map(({ decision }) => decision), [\n \"quality_rejected\",\n DISPOSITION.RETRY,\n \"accepted\",\n]);\nassert.deepEqual(selection.retryBudget, { spent: 1, limit: 1, unit: RETRY_BUDGET_UNIT });\nassert.equal(selection.outcome.droppedEvidence, 0);\nassertMetadataIsRedacted(selection.outcome.evidence);\n\nconst approvalCalls = [];\nlet executedProductionChanges = 0;\nconst approvalCandidates = [\n candidate(\"rollback-planner\", \"https://rollback.internal\", async (ctx) => {\n approvalCalls.push(`rollback-planner:${ctx.attempt}`);\n return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.96, [\n \"deploy:checkout-v42\",\n \"metric:checkout-error-rate\",\n ]);\n }),\n candidate(\"unsafe-fallback\", \"https://unsafe.internal\", async (ctx) => {\n approvalCalls.push(`unsafe-fallback:${ctx.attempt}`);\n executedProductionChanges++;\n return recommendation(ACTION.ROLLBACK, RISK.PRODUCTION_WRITE, 0.99, [\"deploy:x\", \"metric:y\"]);\n }),\n];\nconst approval = await selectIncidentRecommendation(approvalCandidates);\n\nassert.equal(approval.status, \"requires_user_input\");\nassert.equal(approval.reasonCode, REASON.APPROVAL_REQUIRED);\nassert.deepEqual(approvalCalls, [\"rollback-planner:1\"]);\nassert.equal(executedProductionChanges, 0);\nassert.equal(approval.evidence[0]?.decision, DISPOSITION.REQUIRE_INPUT);\nassertMetadataIsRedacted(approval.evidence);\n\nprocess.stdout.write(`${JSON.stringify({\n sample: \"incident-decision-gate\",\n selection: {\n status: selection.outcome.status,\n selectedCandidate: selection.outcome.candidate.name,\n action: selection.outcome.value.action,\n decisions: selection.outcome.evidence.map(({ decision }) => decision),\n admittedCalls: selectionCalls,\n retryBudget: selection.retryBudget,\n droppedEvidence: selection.outcome.droppedEvidence,\n credentialsRedacted: evidenceIsRedacted(selection.outcome.evidence),\n },\n approval: {\n status: approval.status,\n reasonCode: approval.reasonCode,\n admittedCalls: approvalCalls,\n productionChangesExecuted: executedProductionChanges,\n credentialsRedacted: evidenceIsRedacted(approval.evidence),\n },\n})}\\n`);\n\nfunction selectIncidentRecommendation(candidates) {\n const deadlineAt = Date.now() + END_TO_END_BUDGET_MS;\n return firstAcceptable(candidates, {\n execute: async (provider, ctx) => enforceAuthority(await provider.propose(ctx)),\n accept: assessRecommendation,\n classifyFailure,\n retry: { times: 2, initialDelay: 0, jitter: false, retryBudget: RetryBudget },\n deadlineAt,\n evidence: { maxAttempts: MAX_RETAINED_ATTEMPTS },\n candidateMetadata: ({ name, endpoint, apiKey }) => ({ name, endpoint, apiKey }),\n });\n}\n\nfunction assessRecommendation(value) {\n const failedRule = [\n [value.evidence.length < MIN_EVIDENCE_REFERENCES, REASON.EVIDENCE_MISSING],\n [value.confidence < MIN_CONFIDENCE, REASON.CONFIDENCE_LOW],\n ].find(([failed]) => failed);\n return failedRule === undefined\n ? { accepted: true }\n : { accepted: false, reasonCode: failedRule[1] };\n}\n\nfunction enforceAuthority(value) {\n if (value.risk === RISK.PRODUCTION_WRITE) {\n throw new ApprovalRequiredError(\"operator approval required before production mutation\");\n }\n return value;\n}\n\nfunction classifyFailure(error) {\n const policy = [...FAILURE_POLICY].find(([ErrorType]) => error instanceof ErrorType)?.[1];\n return policy ?? DEFAULT_FAILURE_POLICY;\n}\n\nfunction candidate(name, endpoint, propose) {\n return Object.freeze({ name, endpoint, apiKey: `secret-for-${name}`, propose });\n}\n\nfunction recommendation(action, risk, confidence, evidence) {\n return Object.freeze({ action, risk, confidence, evidence: Object.freeze(evidence) });\n}\n\nfunction assertMetadataIsRedacted(evidence) {\n assert.equal(evidenceIsRedacted(evidence), true);\n assert.equal(JSON.stringify(evidence).includes(\"secret-for-\"), false);\n}\n\nfunction evidenceIsRedacted(evidence) {\n return evidence.every(({ metadata }) => metadata?.apiKey === \"[redacted]\");\n}\n" + }, "budget-rag": { "path": "packages/core/samples/budget-rag.sample.js", "result": { diff --git a/apps/use-cases-site/src/data/useCases.ts b/apps/use-cases-site/src/data/useCases.ts index 99799a2..e493bd4 100644 --- a/apps/use-cases-site/src/data/useCases.ts +++ b/apps/use-cases-site/src/data/useCases.ts @@ -38,6 +38,27 @@ interface RaceProvidersResult { cancelledProviders: string[]; } +interface IncidentDecisionGateResult { + sample: "incident-decision-gate"; + selection: { + status: string; + selectedCandidate: string; + action: string; + decisions: string[]; + admittedCalls: string[]; + retryBudget: { spent: number; limit: number; unit: string }; + droppedEvidence: number; + credentialsRedacted: boolean; + }; + approval: { + status: string; + reasonCode: string; + admittedCalls: string[]; + productionChangesExecuted: number; + credentialsRedacted: boolean; + }; +} + interface BudgetRagResult { sample: "budget-rag"; answer: string; @@ -61,6 +82,7 @@ function sample(id: string): SampleSnapshot { const agentEvidence = sample("agent-tree-cancel"); const conversationEvidence = sample("conversation-agent"); const raceEvidence = sample("race-providers"); +const incidentGateEvidence = sample("incident-decision-gate"); const ragEvidence = sample("budget-rag"); function list(values: string[]) { @@ -386,6 +408,121 @@ export const useCases: UseCase[] = [ ], code: raceEvidence.source, }, + { + id: "incident-decision-gate", + title: "Auditable incident decision gate", + audience: "AI platform and SRE teams", + summary: "Select a grounded incident recommendation, bound retries, and stop production mutations for operator authority.", + pain: "A model can return a plausible 200 OK diagnosis with no supporting telemetry, while an automatic fallback may propose a dangerous production change. Transport success alone cannot authorize incident response.", + answer: "Use firstAcceptable for semantic evidence rules, one retry budget and deadline for the chain, typed human-approval stops, and bounded redacted attempt evidence.", + primarySample: incidentGateEvidence.path, + features: [ + { label: "semantic admission", reason: "A high-confidence answer without incident evidence is rejected.", tone: "emerald" }, + { label: "shared retry budget", reason: "A transient retry consumes the chain's single retry allowance.", tone: "amber" }, + { label: "human authority", reason: "A production rollback stops before any mutation executes.", tone: "coral" }, + { label: "redacted evidence", reason: "Attempt decisions remain inspectable without exposing credentials.", tone: "cobalt" }, + ], + flow: [ + { userAction: "Fast triage returns a plausible but unsupported diagnosis", runtimeOwner: "fast-triage", feature: "quality_rejected" }, + { userAction: "Grounded reasoning fails transiently", runtimeOwner: "grounded-reasoner", feature: "retry_same_candidate" }, + { userAction: "The retry returns grounded evidence", runtimeOwner: "grounded-reasoner", feature: "accepted" }, + { userAction: "A separate plan proposes a production rollback", runtimeOwner: "operator.authority", feature: "requires_user_input" }, + { userAction: "Unsafe fallback remains unadmitted", runtimeOwner: "unsafe-fallback", feature: "zero production changes" }, + ], + runtimeTree: [ + { + id: "incident-gate", + label: "incident.decision", + kind: "scope", + statusByPhase: { idle: "waiting", running: "running", completed: "done", aborted: "cancelled" }, + children: [ + { + id: "fast-triage", + label: "fast-triage", + kind: "llm", + statusByPhase: { idle: "waiting", running: "running", completed: "done", aborted: "cancelled" }, + }, + { + id: "grounded-reasoner", + label: "grounded-reasoner", + kind: "llm", + statusByPhase: { idle: "waiting", running: "running", completed: "done", aborted: "cancelled" }, + }, + { + id: "retry-budget", + label: "retry.budget", + kind: "budget", + statusByPhase: { idle: "waiting", running: "running", completed: "done", aborted: "cancelled" }, + }, + { + id: "operator-authority", + label: "operator.authority", + kind: "policy", + statusByPhase: { idle: "waiting", running: "running", completed: "done", aborted: "cancelled" }, + }, + { + id: "unsafe-fallback", + label: "unsafe-fallback", + kind: "llm", + statusByPhase: { idle: "waiting", running: "waiting", completed: "waiting", aborted: "waiting" }, + }, + ], + }, + ], + events: { + idle: sampleEvents(incidentGateEvidence, ["status: ready"]), + running: sampleEvents(incidentGateEvidence, [ + `selection: ${incidentGateEvidence.result.selection.decisions.join(" -> ")}`, + `approval: ${incidentGateEvidence.result.approval.status}`, + ]), + completed: sampleEvents(incidentGateEvidence, [ + `selection: ${incidentGateEvidence.result.selection.decisions.join(" -> ")}`, + `approval: ${incidentGateEvidence.result.approval.status}`, + `productionChangesExecuted: ${incidentGateEvidence.result.approval.productionChangesExecuted}`, + ]), + aborted: sampleEvents(incidentGateEvidence, [ + `selection: ${incidentGateEvidence.result.selection.decisions.join(" -> ")}`, + `approval: ${incidentGateEvidence.result.approval.status}`, + ]), + }, + receipt: { + idle: [`sample: ${incidentGateEvidence.result.sample}`, `source: ${incidentGateEvidence.path}`], + running: [ + `selectedCandidate: ${incidentGateEvidence.result.selection.selectedCandidate}`, + `retryBudget: ${incidentGateEvidence.result.selection.retryBudget.spent}/${incidentGateEvidence.result.selection.retryBudget.limit}`, + `credentialsRedacted: ${incidentGateEvidence.result.selection.credentialsRedacted}`, + ], + completed: [ + `selectedCandidate: ${incidentGateEvidence.result.selection.selectedCandidate}`, + `action: ${incidentGateEvidence.result.selection.action}`, + `retryBudget: ${incidentGateEvidence.result.selection.retryBudget.spent}/${incidentGateEvidence.result.selection.retryBudget.limit}`, + `droppedEvidence: ${incidentGateEvidence.result.selection.droppedEvidence}`, + `approval.reasonCode: ${incidentGateEvidence.result.approval.reasonCode}`, + `productionChangesExecuted: ${incidentGateEvidence.result.approval.productionChangesExecuted}`, + `credentialsRedacted: ${incidentGateEvidence.result.selection.credentialsRedacted}`, + ], + aborted: [ + `approval: ${incidentGateEvidence.result.approval.status}`, + `productionChangesExecuted: ${incidentGateEvidence.result.approval.productionChangesExecuted}`, + `credentialsRedacted: ${incidentGateEvidence.result.approval.credentialsRedacted}`, + ], + }, + evidence: [ + { + claim: "Incident recommendations are admitted by evidence quality, not transport success.", + path: "packages/core/samples/incident-decision-gate.sample.js", + invariant: "unsupported output is rejected, one transient retry is charged, and the grounded candidate is accepted.", + status: "tracked", + }, + { + claim: "Production mutations require explicit human authority.", + path: "packages/core/tests/evidence/correctness/candidate-scenarios.mjs", + invariant: "requires_user_input stops candidate admission before an unsafe fallback or side effect runs.", + status: "tracked", + }, + ], + code: incidentGateEvidence.source, + }, { id: "rag-pipeline", title: "RAG answer pipeline", From 29ca75081050b77719f7cda72a6a2689db21ad91 Mon Sep 17 00:00:00 2001 From: "Admilson B. F. Cossa" Date: Fri, 4 Sep 2026 12:16:07 +0200 Subject: [PATCH 03/14] feat(labs): add bounded editable scenario contracts --- .../contract/scenario-contract.d.mts | 78 ++++++ .../contract/scenario-contract.mjs | 256 ++++++++++++++++++ .../scenarios/approval-stop.json | 53 ++++ .../scenarios/deadline-exhaustion.json | 53 ++++ .../scenarios/grounded-fallback.json | 71 +++++ .../test/scenario-contract.test.mjs | 76 ++++++ package.json | 1 + 7 files changed, 588 insertions(+) create mode 100644 examples/ai-failure-lab/contract/scenario-contract.d.mts create mode 100644 examples/ai-failure-lab/contract/scenario-contract.mjs create mode 100644 examples/ai-failure-lab/scenarios/approval-stop.json create mode 100644 examples/ai-failure-lab/scenarios/deadline-exhaustion.json create mode 100644 examples/ai-failure-lab/scenarios/grounded-fallback.json create mode 100644 examples/ai-failure-lab/test/scenario-contract.test.mjs diff --git a/examples/ai-failure-lab/contract/scenario-contract.d.mts b/examples/ai-failure-lab/contract/scenario-contract.d.mts new file mode 100644 index 0000000..ac15b12 --- /dev/null +++ b/examples/ai-failure-lab/contract/scenario-contract.d.mts @@ -0,0 +1,78 @@ +/** + * Type declarations for bounded WorkIt failure scenarios. + * + * @author Admilson B. F. Cossa + * SPDX-License-Identifier: Apache-2.0 + */ + +export type ScenarioSourceKind = "fixture" | "github_issues" | "open_meteo"; +export type ScenarioRisk = "read_only" | "production_write"; +export type ScenarioFailureClass = "transient" | "unavailable" | "invalid_request"; + +export interface ScenarioSource { + readonly kind: ScenarioSourceKind; + readonly label: string; + readonly reference?: string; +} + +export interface ScenarioPolicy { + readonly minConfidence: number; + readonly minEvidenceReferences: number; + readonly deadlineMs: number; + readonly retryLimit: number; + readonly maxEvidenceAttempts: number; +} + +export interface ScenarioSuccess { + readonly type: "success"; + readonly confidence: number; + readonly evidence: readonly string[]; + readonly action: string; + readonly risk: ScenarioRisk; +} + +export interface ScenarioFailure { + readonly type: "failure"; + readonly failureClass: ScenarioFailureClass; +} + +export type ScenarioCandidateOutcome = ScenarioSuccess | ScenarioFailure; + +export interface ScenarioCandidate { + readonly id: string; + readonly name: string; + readonly latencyMs: number; + readonly outcomes: readonly ScenarioCandidateOutcome[]; +} + +export interface FailureScenario { + readonly version: 1; + readonly id: string; + readonly title: string; + readonly summary: string; + readonly source: ScenarioSource; + readonly policy: ScenarioPolicy; + readonly candidates: readonly ScenarioCandidate[]; +} + +export interface ScenarioLimits { + readonly maxBytes: number; + readonly maxCandidates: number; + readonly maxOutcomesPerCandidate: number; + readonly maxEvidenceReferences: number; + readonly maxEvidenceAttempts: number; + readonly maxStringLength: number; + readonly maxDeadlineMs: number; + readonly maxLatencyMs: number; + readonly maxRetries: number; +} + +export const SCENARIO_VERSION: 1; +export const SCENARIO_LIMITS: ScenarioLimits; + +export class ScenarioContractError extends TypeError { + readonly path: string; +} + +export function parseScenarioJson(json: string): FailureScenario; +export function validateScenario(value: unknown): FailureScenario; diff --git a/examples/ai-failure-lab/contract/scenario-contract.mjs b/examples/ai-failure-lab/contract/scenario-contract.mjs new file mode 100644 index 0000000..e06a4b0 --- /dev/null +++ b/examples/ai-failure-lab/contract/scenario-contract.mjs @@ -0,0 +1,256 @@ +/** + * Bounded, environment-neutral contract for editable WorkIt failure scenarios. + * + * @author Admilson B. F. Cossa + * SPDX-License-Identifier: Apache-2.0 + */ + +export const SCENARIO_VERSION = 1; + +export const SCENARIO_LIMITS = Object.freeze({ + maxBytes: 32_768, + maxCandidates: 12, + maxOutcomesPerCandidate: 8, + maxEvidenceReferences: 8, + maxEvidenceAttempts: 32, + maxStringLength: 160, + maxDeadlineMs: 10_000, + maxLatencyMs: 5_000, + maxRetries: 4, +}); + +const SOURCE_KINDS = new Set(["fixture", "github_issues", "open_meteo"]); +const OUTCOME_TYPES = new Set(["success", "failure"]); +const RISKS = new Set(["read_only", "production_write"]); +const FAILURE_CLASSES = new Set(["transient", "unavailable", "invalid_request"]); +const SLUG_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/; + +const ROOT_KEYS = new Set(["version", "id", "title", "summary", "source", "policy", "candidates"]); +const SOURCE_KEYS = new Set(["kind", "label", "reference"]); +const POLICY_KEYS = new Set([ + "minConfidence", + "minEvidenceReferences", + "deadlineMs", + "retryLimit", + "maxEvidenceAttempts", +]); +const CANDIDATE_KEYS = new Set(["id", "name", "latencyMs", "outcomes"]); +const SUCCESS_KEYS = new Set(["type", "confidence", "evidence", "action", "risk"]); +const FAILURE_KEYS = new Set(["type", "failureClass"]); + +/** Raised when editable scenario data violates a bounded public contract. */ +export class ScenarioContractError extends TypeError { + /** Create a contract error tied to one JSON path. */ + constructor(path, message) { + super(`${path}: ${message}`); + this.name = "ScenarioContractError"; + this.path = path; + } +} + +/** Parse and validate one user-editable scenario JSON document. */ +export function parseScenarioJson(json) { + if (typeof json !== "string") { + throw new ScenarioContractError("$", "scenario JSON must be a string"); + } + + const bytes = new TextEncoder().encode(json).byteLength; + if (bytes > SCENARIO_LIMITS.maxBytes) { + throw new ScenarioContractError("$", `scenario exceeds ${SCENARIO_LIMITS.maxBytes} bytes`); + } + + let value; + try { + value = JSON.parse(json); + } catch { + throw new ScenarioContractError("$", "scenario must contain valid JSON"); + } + + return validateScenario(value); +} + +/** Validate and snapshot one scenario supplied by a fixture or public-data adapter. */ +export function validateScenario(value) { + const root = recordAt(value, "$", ROOT_KEYS); + integerAt(root.version, "$.version", SCENARIO_VERSION, SCENARIO_VERSION); + const id = slugAt(root.id, "$.id"); + const title = stringAt(root.title, "$.title"); + const summary = stringAt(root.summary, "$.summary"); + const source = validateSource(root.source); + const policy = validatePolicy(root.policy); + const candidates = arrayAt(root.candidates, "$.candidates", 1, SCENARIO_LIMITS.maxCandidates) + .map((candidate, index) => validateCandidate(candidate, index)); + uniqueCandidateIds(candidates); + + return deepFreeze({ + version: SCENARIO_VERSION, + id, + title, + summary, + source, + policy, + candidates, + }); +} + +function validateSource(value) { + const source = recordAt(value, "$.source", SOURCE_KEYS); + const kind = enumAt(source.kind, "$.source.kind", SOURCE_KINDS); + const label = stringAt(source.label, "$.source.label"); + const reference = optionalStringAt(source.reference, "$.source.reference"); + return { + kind, + label, + ...(reference === undefined ? {} : { reference }), + }; +} + +function validatePolicy(value) { + const policy = recordAt(value, "$.policy", POLICY_KEYS); + return { + minConfidence: numberAt(policy.minConfidence, "$.policy.minConfidence", 0, 1), + minEvidenceReferences: integerAt( + policy.minEvidenceReferences, + "$.policy.minEvidenceReferences", + 0, + SCENARIO_LIMITS.maxEvidenceReferences, + ), + deadlineMs: integerAt(policy.deadlineMs, "$.policy.deadlineMs", 100, SCENARIO_LIMITS.maxDeadlineMs), + retryLimit: integerAt(policy.retryLimit, "$.policy.retryLimit", 0, SCENARIO_LIMITS.maxRetries), + maxEvidenceAttempts: integerAt( + policy.maxEvidenceAttempts, + "$.policy.maxEvidenceAttempts", + 1, + SCENARIO_LIMITS.maxEvidenceAttempts, + ), + }; +} + +function validateCandidate(value, index) { + const path = `$.candidates[${index}]`; + const candidate = recordAt(value, path, CANDIDATE_KEYS); + const outcomes = arrayAt( + candidate.outcomes, + `${path}.outcomes`, + 1, + SCENARIO_LIMITS.maxOutcomesPerCandidate, + ).map((outcome, outcomeIndex) => validateOutcome(outcome, `${path}.outcomes[${outcomeIndex}]`)); + + return { + id: slugAt(candidate.id, `${path}.id`), + name: stringAt(candidate.name, `${path}.name`), + latencyMs: integerAt(candidate.latencyMs, `${path}.latencyMs`, 0, SCENARIO_LIMITS.maxLatencyMs), + outcomes, + }; +} + +function validateOutcome(value, path) { + const candidate = recordAt(value, path); + const type = enumAt(candidate.type, `${path}.type`, OUTCOME_TYPES); + return type === "success" + ? validateSuccess(candidate, path) + : validateFailure(candidate, path); +} + +function validateSuccess(value, path) { + assertKnownKeys(value, path, SUCCESS_KEYS); + return { + type: "success", + confidence: numberAt(value.confidence, `${path}.confidence`, 0, 1), + evidence: arrayAt( + value.evidence, + `${path}.evidence`, + 0, + SCENARIO_LIMITS.maxEvidenceReferences, + ).map((reference, index) => stringAt(reference, `${path}.evidence[${index}]`)), + action: slugAt(value.action, `${path}.action`), + risk: enumAt(value.risk, `${path}.risk`, RISKS), + }; +} + +function validateFailure(value, path) { + assertKnownKeys(value, path, FAILURE_KEYS); + return { + type: "failure", + failureClass: enumAt(value.failureClass, `${path}.failureClass`, FAILURE_CLASSES), + }; +} + +function uniqueCandidateIds(candidates) { + const seen = new Set(); + for (const candidate of candidates) { + if (seen.has(candidate.id)) { + throw new ScenarioContractError("$.candidates", `duplicate candidate id ${candidate.id}`); + } + seen.add(candidate.id); + } +} + +function recordAt(value, path, allowedKeys) { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new ScenarioContractError(path, "must be an object"); + } + if (allowedKeys !== undefined) assertKnownKeys(value, path, allowedKeys); + return value; +} + +function assertKnownKeys(value, path, allowedKeys) { + const unknown = Object.keys(value).find((key) => !allowedKeys.has(key)); + if (unknown !== undefined) { + throw new ScenarioContractError(`${path}.${unknown}`, "unknown fields are not allowed"); + } +} + +function arrayAt(value, path, minLength, maxLength) { + if (!Array.isArray(value)) throw new ScenarioContractError(path, "must be an array"); + if (value.length < minLength || value.length > maxLength) { + throw new ScenarioContractError(path, `must contain between ${minLength} and ${maxLength} items`); + } + return value; +} + +function stringAt(value, path) { + if (typeof value !== "string" || value.length < 1 || value.length > SCENARIO_LIMITS.maxStringLength) { + throw new ScenarioContractError(path, `must be a non-empty string up to ${SCENARIO_LIMITS.maxStringLength} characters`); + } + return value; +} + +function optionalStringAt(value, path) { + return value === undefined ? undefined : stringAt(value, path); +} + +function slugAt(value, path) { + const text = stringAt(value, path); + if (!SLUG_PATTERN.test(text)) throw new ScenarioContractError(path, "must be a lowercase slug"); + return text; +} + +function enumAt(value, path, allowed) { + if (typeof value !== "string" || !allowed.has(value)) { + throw new ScenarioContractError(path, `must be one of ${[...allowed].join(", ")}`); + } + return value; +} + +function integerAt(value, path, minimum, maximum) { + if (!Number.isInteger(value) || value < minimum || value > maximum) { + throw new ScenarioContractError(path, `must be an integer between ${minimum} and ${maximum}`); + } + return value; +} + +function numberAt(value, path, minimum, maximum) { + if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) { + throw new ScenarioContractError(path, `must be a finite number between ${minimum} and ${maximum}`); + } + return value; +} + +function deepFreeze(value) { + Object.freeze(value); + for (const nested of Object.values(value)) { + if (nested !== null && typeof nested === "object" && !Object.isFrozen(nested)) deepFreeze(nested); + } + return value; +} diff --git a/examples/ai-failure-lab/scenarios/approval-stop.json b/examples/ai-failure-lab/scenarios/approval-stop.json new file mode 100644 index 0000000..308ff92 --- /dev/null +++ b/examples/ai-failure-lab/scenarios/approval-stop.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "id": "approval-stop", + "title": "The model wants to roll back production", + "summary": "Stop at the authority boundary before a later fallback can execute a production mutation.", + "source": { + "kind": "fixture", + "label": "Deterministic authority fixture" + }, + "policy": { + "minConfidence": 0.85, + "minEvidenceReferences": 2, + "deadlineMs": 2000, + "retryLimit": 1, + "maxEvidenceAttempts": 4 + }, + "candidates": [ + { + "id": "rollback-planner", + "name": "Rollback planner", + "latencyMs": 30, + "outcomes": [ + { + "type": "success", + "confidence": 0.96, + "evidence": [ + "deploy:checkout-v42", + "metric:checkout-error-rate" + ], + "action": "rollback_deployment", + "risk": "production_write" + } + ] + }, + { + "id": "unsafe-fallback", + "name": "Unsafe fallback", + "latencyMs": 10, + "outcomes": [ + { + "type": "success", + "confidence": 0.99, + "evidence": [ + "deploy:checkout-v42", + "metric:checkout-error-rate" + ], + "action": "rollback_deployment", + "risk": "production_write" + } + ] + } + ] +} diff --git a/examples/ai-failure-lab/scenarios/deadline-exhaustion.json b/examples/ai-failure-lab/scenarios/deadline-exhaustion.json new file mode 100644 index 0000000..43af39e --- /dev/null +++ b/examples/ai-failure-lab/scenarios/deadline-exhaustion.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "id": "deadline-exhaustion", + "title": "Fallback cannot multiply the incident SLO", + "summary": "A global deadline stops the chain instead of granting every provider a fresh timeout.", + "source": { + "kind": "fixture", + "label": "Deterministic deadline fixture" + }, + "policy": { + "minConfidence": 0.85, + "minEvidenceReferences": 2, + "deadlineMs": 150, + "retryLimit": 0, + "maxEvidenceAttempts": 4 + }, + "candidates": [ + { + "id": "slow-triage", + "name": "Slow triage", + "latencyMs": 200, + "outcomes": [ + { + "type": "success", + "confidence": 0.95, + "evidence": [ + "trace:checkout-timeout", + "metric:payment-error-rate" + ], + "action": "collect_diagnostics", + "risk": "read_only" + } + ] + }, + { + "id": "late-fallback", + "name": "Late fallback", + "latencyMs": 20, + "outcomes": [ + { + "type": "success", + "confidence": 0.98, + "evidence": [ + "trace:checkout-timeout", + "metric:payment-error-rate" + ], + "action": "collect_diagnostics", + "risk": "read_only" + } + ] + } + ] +} diff --git a/examples/ai-failure-lab/scenarios/grounded-fallback.json b/examples/ai-failure-lab/scenarios/grounded-fallback.json new file mode 100644 index 0000000..670e1b5 --- /dev/null +++ b/examples/ai-failure-lab/scenarios/grounded-fallback.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "id": "grounded-fallback", + "title": "The fast diagnosis has no evidence", + "summary": "Reject an unsupported 200 OK, retry one transient provider failure, and accept the grounded recommendation.", + "source": { + "kind": "fixture", + "label": "Deterministic incident fixture" + }, + "policy": { + "minConfidence": 0.85, + "minEvidenceReferences": 2, + "deadlineMs": 2000, + "retryLimit": 1, + "maxEvidenceAttempts": 4 + }, + "candidates": [ + { + "id": "fast-triage", + "name": "Fast triage", + "latencyMs": 40, + "outcomes": [ + { + "type": "success", + "confidence": 0.97, + "evidence": [], + "action": "collect_diagnostics", + "risk": "read_only" + } + ] + }, + { + "id": "grounded-reasoner", + "name": "Grounded reasoner", + "latencyMs": 60, + "outcomes": [ + { + "type": "failure", + "failureClass": "transient" + }, + { + "type": "success", + "confidence": 0.93, + "evidence": [ + "trace:checkout-timeout", + "metric:payment-error-rate" + ], + "action": "collect_diagnostics", + "risk": "read_only" + } + ] + }, + { + "id": "rollback-planner", + "name": "Rollback planner", + "latencyMs": 25, + "outcomes": [ + { + "type": "success", + "confidence": 0.99, + "evidence": [ + "deploy:checkout-v42", + "metric:checkout-error-rate" + ], + "action": "rollback_deployment", + "risk": "production_write" + } + ] + } + ] +} diff --git a/examples/ai-failure-lab/test/scenario-contract.test.mjs b/examples/ai-failure-lab/test/scenario-contract.test.mjs new file mode 100644 index 0000000..d9db8c0 --- /dev/null +++ b/examples/ai-failure-lab/test/scenario-contract.test.mjs @@ -0,0 +1,76 @@ +/** + * Adversarial tests for editable scenario admission. + * + * @author Admilson B. F. Cossa + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { + SCENARIO_LIMITS, + ScenarioContractError, + parseScenarioJson, + validateScenario, +} from "../contract/scenario-contract.mjs"; + +const scenarioPaths = [ + "../scenarios/grounded-fallback.json", + "../scenarios/approval-stop.json", + "../scenarios/deadline-exhaustion.json", +]; + +test("all tracked scenario fixtures satisfy the bounded contract", async () => { + for (const path of scenarioPaths) { + const json = await readFile(fileURLToPath(new URL(path, import.meta.url)), "utf8"); + const scenario = parseScenarioJson(json); + assert.equal(Object.isFrozen(scenario), true); + assert.equal(Object.isFrozen(scenario.candidates), true); + } +}); + +test("unknown credential-bearing fields are rejected", async () => { + const scenario = await fixture(); + scenario.candidates[0].apiKey = "must-not-enter-the-lab"; + assert.throws( + () => validateScenario(scenario), + (error) => error instanceof ScenarioContractError + && error.path === "$.candidates[0].apiKey", + ); +}); + +test("duplicate candidate identities are rejected", async () => { + const scenario = await fixture(); + scenario.candidates[1].id = scenario.candidates[0].id; + assert.throws(() => validateScenario(scenario), /duplicate candidate id/); +}); + +test("unbounded candidate sets are rejected", async () => { + const scenario = await fixture(); + scenario.candidates = Array.from( + { length: SCENARIO_LIMITS.maxCandidates + 1 }, + (_, index) => ({ ...scenario.candidates[0], id: `candidate-${index}` }), + ); + assert.throws(() => validateScenario(scenario), /must contain between/); +}); + +test("oversized JSON is rejected before parsing", () => { + const json = JSON.stringify({ padding: "x".repeat(SCENARIO_LIMITS.maxBytes) }); + assert.throws(() => parseScenarioJson(json), /scenario exceeds/); +}); + +test("prototype and non-finite policy values cannot enter the snapshot", async () => { + const scenario = await fixture(); + scenario.policy.minConfidence = Number.NaN; + assert.throws(() => validateScenario(scenario), /finite number/); +}); + +async function fixture() { + const json = await readFile( + fileURLToPath(new URL("../scenarios/grounded-fallback.json", import.meta.url)), + "utf8", + ); + return JSON.parse(json); +} diff --git a/package.json b/package.json index c9596d1..d0bea02 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "site:dev": "npm --prefix apps/use-cases-site run dev", "site:build": "npm --prefix apps/use-cases-site run build", "site:preview": "npm --prefix apps/use-cases-site run preview", + "test:failure-lab": "node --test examples/ai-failure-lab/test/*.test.mjs", "test:evidence": "npm --workspace @workit/core run test:evidence", "test:property": "npm --workspace @workit/core run test:property", "pack:dry": "npm --workspace @workit/core run pack:dry", From a5e6fad00c5fcb9201447a99b426d9599ba319ab Mon Sep 17 00:00:00 2001 From: "Admilson B. F. Cossa" Date: Fri, 4 Sep 2026 12:29:49 +0200 Subject: [PATCH 04/14] feat(site): add interactive scenario studio --- apps/use-cases-site/src/App.tsx | 2 + .../src/components/SiteHeader.tsx | 1 + .../src/labs/PolicyControls.tsx | 57 +++++++ .../use-cases-site/src/labs/PreviewResult.tsx | 82 +++++++++ .../src/labs/ScenarioStudio.tsx | 161 ++++++++++++++++++ .../src/labs/scenarioPresets.ts | 24 +++ .../policy/incident-policy.d.mts | 37 ++++ .../ai-failure-lab/policy/incident-policy.mjs | 69 ++++++++ .../policy/preview-engine.d.mts | 36 ++++ .../ai-failure-lab/policy/preview-engine.mjs | 134 +++++++++++++++ .../test/preview-engine.test.mjs | 56 ++++++ 11 files changed, 659 insertions(+) create mode 100644 apps/use-cases-site/src/labs/PolicyControls.tsx create mode 100644 apps/use-cases-site/src/labs/PreviewResult.tsx create mode 100644 apps/use-cases-site/src/labs/ScenarioStudio.tsx create mode 100644 apps/use-cases-site/src/labs/scenarioPresets.ts create mode 100644 examples/ai-failure-lab/policy/incident-policy.d.mts create mode 100644 examples/ai-failure-lab/policy/incident-policy.mjs create mode 100644 examples/ai-failure-lab/policy/preview-engine.d.mts create mode 100644 examples/ai-failure-lab/policy/preview-engine.mjs create mode 100644 examples/ai-failure-lab/test/preview-engine.test.mjs diff --git a/apps/use-cases-site/src/App.tsx b/apps/use-cases-site/src/App.tsx index c872d9f..bc2467a 100644 --- a/apps/use-cases-site/src/App.tsx +++ b/apps/use-cases-site/src/App.tsx @@ -10,6 +10,7 @@ import { SiteHeader } from "./components/SiteHeader"; import { UseCaseRail } from "./components/UseCaseRail"; import { UseCaseWorkbench } from "./components/UseCaseWorkbench"; import { defaultUseCase, useCases } from "./data/useCases"; +import { ScenarioStudio } from "./labs/ScenarioStudio"; import { runLiveExample } from "./runtimeApi"; import type { ExampleRunResult, RunPhase, UseCase } from "./types"; @@ -97,6 +98,7 @@ export default function App() { return (
+
diff --git a/apps/use-cases-site/src/components/SiteHeader.tsx b/apps/use-cases-site/src/components/SiteHeader.tsx index 96085d2..1c7bf46 100644 --- a/apps/use-cases-site/src/components/SiteHeader.tsx +++ b/apps/use-cases-site/src/components/SiteHeader.tsx @@ -22,6 +22,7 @@ export function SiteHeader() {