2sagaco48 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/diagnostics.mjs +141 -0
- package/dist/compute.worker-j5ahIpF-.js +1 -0
- package/dist/graph.worker-DG6iGCB9.js +1 -0
- package/dist/index.html +2314 -0
- package/flatten-guide.md +248 -0
- package/index.html +12 -0
- package/package.json +38 -0
- package/src/App.orig.tsx +204 -0
- package/src/App.orig1.tsx +27 -0
- package/src/App.orig2.tsx +25 -0
- package/src/App.tsx +3 -0
- package/src/BaseApp.tsx +18 -0
- package/src/INTEGRATION_LEDGER.md +22 -0
- package/src/PERSIST_CANARY.orig.txt +1 -0
- package/src/PERSIST_CANARY.txt +1 -0
- package/src/REGRESSION_LEDGER.md +651 -0
- package/src/TEMPLATE_PIPELINE_AUDIT.md +208 -0
- package/src/WILLIAMS_PERSONA_GUIDE.md +62 -0
- package/src/chat/VeritasChat.tsx +69 -0
- package/src/chat/synthesis.ts +31 -0
- package/src/chat/tier.ts +45 -0
- package/src/components/AdaptersPage.orig.tsx +99 -0
- package/src/components/AdaptersPage.tsx +3 -0
- package/src/components/AdversarialPanel.tsx +136 -0
- package/src/components/AnchorBaselinePanel.tsx +75 -0
- package/src/components/AnswerPanel.tsx +60 -0
- package/src/components/ArtifactPanel.tsx +43 -0
- package/src/components/ChatApp.orig.tsx +1582 -0
- package/src/components/ChatApp.orig1.tsx +722 -0
- package/src/components/ChatApp.tsx +95 -0
- package/src/components/ChatAugmentPanels.tsx +73 -0
- package/src/components/CitationLedgerPanel.tsx +313 -0
- package/src/components/ComputeSandboxPanel.tsx +130 -0
- package/src/components/ConfigPanel.tsx +110 -0
- package/src/components/ControlPlanePage.tsx +195 -0
- package/src/components/CreativeTreeLifePage.tsx +234 -0
- package/src/components/CreativeTreeOfLifePanel.tsx +149 -0
- package/src/components/DebugTracePanel.tsx +137 -0
- package/src/components/DeepReasoningTrace.tsx +48 -0
- package/src/components/EntitySheetPanel.tsx +51 -0
- package/src/components/FailureModesPanel.tsx +104 -0
- package/src/components/GBSDashboard.tsx +239 -0
- package/src/components/GraphView.tsx +116 -0
- package/src/components/HUD.tsx +125 -0
- package/src/components/HypothesisPanel.tsx +67 -0
- package/src/components/InnovationGenomeEngine.tsx +792 -0
- package/src/components/InnovationPersonaGuide.tsx +348 -0
- package/src/components/InnovationPersonaPanel.tsx +289 -0
- package/src/components/LiveResourceHUD.tsx +109 -0
- package/src/components/LongWriterPanel.tsx +112 -0
- package/src/components/MainPipelineV10Bridge.tsx +72 -0
- package/src/components/MarkdownLite.tsx +63 -0
- package/src/components/MemoryInspector.tsx +38 -0
- package/src/components/MemoryMonitor.tsx +52 -0
- package/src/components/MemoryStressPanel.tsx +176 -0
- package/src/components/ModulesPage.tsx +171 -0
- package/src/components/PipelineDebugConsole.orig.tsx +1856 -0
- package/src/components/PipelineDebugConsole.tsx +3 -0
- package/src/components/PreFlightHUD.tsx +67 -0
- package/src/components/PrismaFetchTracePanel.tsx +87 -0
- package/src/components/ReportOSPanel.tsx +165 -0
- package/src/components/ResourceEstimatorPage.tsx +327 -0
- package/src/components/RichText.tsx +181 -0
- package/src/components/SagaOsPanel.tsx +605 -0
- package/src/components/SagaOsReference.tsx +421 -0
- package/src/components/SagaOsV2Panel.tsx +594 -0
- package/src/components/SagaStyleTab.tsx +382 -0
- package/src/components/SageBlueprintPanel.tsx +416 -0
- package/src/components/SharedChatInput.tsx +77 -0
- package/src/components/StatFinancePanel.tsx +190 -0
- package/src/components/StylePersonaPanel.tsx +128 -0
- package/src/components/TemplatesPage.tsx +300 -0
- package/src/components/TestPanel.tsx +63 -0
- package/src/components/TraceLog.tsx +89 -0
- package/src/components/V15CalibrationAugment.tsx +1134 -0
- package/src/components/V15CalibrationDialog.tsx +1871 -0
- package/src/components/V15Overlay.orig.tsx +120 -0
- package/src/components/V15Overlay.tsx +4 -0
- package/src/components/V15OverlayWrapper.tsx +362 -0
- package/src/components/V15Toggle.tsx +34 -0
- package/src/console/DiagnosticRecoveryPanel.tsx +358 -0
- package/src/console/EngineeringConsole.tsx +103 -0
- package/src/console/FictionSystemPanels.tsx +882 -0
- package/src/console/InspectorPanels.tsx +254 -0
- package/src/console/ManuscriptGraderPanel.tsx +182 -0
- package/src/console/RecursiveReviserPanel.tsx +245 -0
- package/src/console/SeedControlPanel.tsx +135 -0
- package/src/console/StoryControllerPanel.tsx +443 -0
- package/src/console/engineBridge.ts +258 -0
- package/src/console/gemini.ts +106 -0
- package/src/console/grading.ts +250 -0
- package/src/console/util.ts +45 -0
- package/src/fiction/continuity.ts +372 -0
- package/src/fiction/crash-protection.ts +111 -0
- package/src/fiction/diagnostics.ts +208 -0
- package/src/fiction/diff.ts +74 -0
- package/src/fiction/entity-state-machine.ts +214 -0
- package/src/fiction/nive-engine.ts +404 -0
- package/src/fiction/pipeline-selftest.ts +132 -0
- package/src/fiction/seed-control.ts +193 -0
- package/src/fiction/story-engine.ts +326 -0
- package/src/fiction/types.ts +68 -0
- package/src/flatten-guide.md +379 -0
- package/src/index.css +14 -0
- package/src/index.orig.css +13 -0
- package/src/lib/academic-sources.base.ts +188 -0
- package/src/lib/academic-sources.orig.ts +28 -0
- package/src/lib/academic-sources.ts +10 -0
- package/src/lib/advanced-math.ts +119 -0
- package/src/lib/adversarial-engine.base.ts +409 -0
- package/src/lib/adversarial-engine.ts +82 -0
- package/src/lib/ai-revision.ts +623 -0
- package/src/lib/app-state.tsx +426 -0
- package/src/lib/artifact-registry.ts +97 -0
- package/src/lib/artifacts.ts +180 -0
- package/src/lib/atlas-dr.ts +167 -0
- package/src/lib/browser-mtls.ts +127 -0
- package/src/lib/browser-queue.ts +221 -0
- package/src/lib/browser-search-scraper.base.ts +382 -0
- package/src/lib/browser-search-scraper.orig.ts +90 -0
- package/src/lib/browser-search-scraper.ts +10 -0
- package/src/lib/calc-interceptor.ts +82 -0
- package/src/lib/citation-lane-tap.ts +78 -0
- package/src/lib/citation-ledger-store.ts +369 -0
- package/src/lib/citation-ledger.ts +203 -0
- package/src/lib/compute-sandbox.ts +215 -0
- package/src/lib/compute.worker.ts +153 -0
- package/src/lib/connectors/gemini.ts +120 -0
- package/src/lib/connectors/jina.ts +233 -0
- package/src/lib/connectors/marketdata.ts +51 -0
- package/src/lib/connectors/prismafetch.base.ts +175 -0
- package/src/lib/connectors/prismafetch.ts +1 -0
- package/src/lib/connectors/serpapi.ts +65 -0
- package/src/lib/connectors/wikidata.ts +44 -0
- package/src/lib/constraints.ts +331 -0
- package/src/lib/continuation-detector.base.ts +153 -0
- package/src/lib/continuation-detector.ts +48 -0
- package/src/lib/contradraft.ts +168 -0
- package/src/lib/cors-proxy.ts +120 -0
- package/src/lib/coverage.ts +81 -0
- package/src/lib/debug/architecture-prescription.ts +556 -0
- package/src/lib/debug/covea-repair.ts +543 -0
- package/src/lib/debug/helios-ground.ts +319 -0
- package/src/lib/debug/intent-decomposer.ts +281 -0
- package/src/lib/debug/intent-lattice.ts +349 -0
- package/src/lib/debug/pipeline-diagnosis.ts +1149 -0
- package/src/lib/debug/pipeline-trace-bus.ts +424 -0
- package/src/lib/debug/prompt-forge.ts +678 -0
- package/src/lib/debug/repair-sites.ts +513 -0
- package/src/lib/debug/research-phase.ts +266 -0
- package/src/lib/debug/retrieval-context.ts +76 -0
- package/src/lib/debug/retrieval-hardener.ts +142 -0
- package/src/lib/debug/scraper-debug-runner.ts +576 -0
- package/src/lib/debug/scraper-forensics.ts +418 -0
- package/src/lib/debug/scraper-lane-roadmap.ts +136 -0
- package/src/lib/debug/self-test.ts +415 -0
- package/src/lib/debug/step-attribution.ts +211 -0
- package/src/lib/debug/template-rubric.ts +301 -0
- package/src/lib/debug/unified-innovation.ts +126 -0
- package/src/lib/debug/veritas-hybrid-scraper.ts +335 -0
- package/src/lib/defense-registry.ts +104 -0
- package/src/lib/defenses.ts +162 -0
- package/src/lib/deterministic-citation-ledger.ts +174 -0
- package/src/lib/elo-registry.base.ts +117 -0
- package/src/lib/elo-registry.ts +1 -0
- package/src/lib/entity-resolver.ts +291 -0
- package/src/lib/failure-modes.ts +267 -0
- package/src/lib/feature-registry.ts +69 -0
- package/src/lib/flaw-registry.ts +143 -0
- package/src/lib/flaws/_template.ts +52 -0
- package/src/lib/flaws/builtins.ts +63 -0
- package/src/lib/flaws/finance.ts +169 -0
- package/src/lib/flaws/fixers.ts +10 -0
- package/src/lib/flaws/index.ts +32 -0
- package/src/lib/flaws/legal.ts +157 -0
- package/src/lib/flaws/medical.ts +188 -0
- package/src/lib/flaws/original-defenses-pack.ts +81 -0
- package/src/lib/flaws/sample-declarative-pack.json +38 -0
- package/src/lib/flaws/selftest.ts +201 -0
- package/src/lib/flaws/software-extended.ts +125 -0
- package/src/lib/flaws/software-rn-webgl.ts +182 -0
- package/src/lib/flaws/statistics-advanced.ts +46 -0
- package/src/lib/flaws/statistics.ts +61 -0
- package/src/lib/gbse/config.ts +20 -0
- package/src/lib/gbse/engine.ts +226 -0
- package/src/lib/gbse/graph.ts +118 -0
- package/src/lib/gbse/graph.worker.ts +21 -0
- package/src/lib/gbse/tests.ts +117 -0
- package/src/lib/gbse/tiers.ts +65 -0
- package/src/lib/gbse/types.ts +94 -0
- package/src/lib/innovation-genome-engine-v2.orig.ts +1001 -0
- package/src/lib/innovation-genome-engine-v2.ts +3 -0
- package/src/lib/innovation-genome-engine.orig.ts +330 -0
- package/src/lib/innovation-genome-engine.ts +3 -0
- package/src/lib/innovation-genome-v10.orig.ts +314 -0
- package/src/lib/innovation-genome-v10.ts +2 -0
- package/src/lib/innovation-genome-v3.orig.ts +2015 -0
- package/src/lib/innovation-genome-v3.ts +2 -0
- package/src/lib/innovation-genome-v4.orig.ts +1959 -0
- package/src/lib/innovation-genome-v4.ts +2 -0
- package/src/lib/innovation-genome-v5.orig.ts +1210 -0
- package/src/lib/innovation-genome-v5.ts +2 -0
- package/src/lib/innovation-genome-v7.orig.ts +2549 -0
- package/src/lib/innovation-genome-v7.ts +2 -0
- package/src/lib/innovation-genome-v8.orig.ts +1485 -0
- package/src/lib/innovation-genome-v8.ts +2 -0
- package/src/lib/innovation-genome-v9.orig.ts +643 -0
- package/src/lib/innovation-genome-v9.ts +2 -0
- package/src/lib/jina.base.ts +293 -0
- package/src/lib/jina.ts +1 -0
- package/src/lib/live-telemetry.ts +61 -0
- package/src/lib/longwriter.ts +84 -0
- package/src/lib/manuscript-grade-v2.ts +298 -0
- package/src/lib/memory-governor.ts +173 -0
- package/src/lib/memory-stress-tests.ts +248 -0
- package/src/lib/model-intelligence.ts +48 -0
- package/src/lib/model-rotator.ts +75 -0
- package/src/lib/models.orig.ts +513 -0
- package/src/lib/models.ts +151 -0
- package/src/lib/n-deep.base.ts +592 -0
- package/src/lib/n-deep.ts +51 -0
- package/src/lib/nih-simulator.ts +56 -0
- package/src/lib/nih-vulnerability-fixes.ts +460 -0
- package/src/lib/omega-templates.base.ts +294 -0
- package/src/lib/omega-templates.ts +225 -0
- package/src/lib/omni-nexus.ts +64 -0
- package/src/lib/oracle-adapters.ts +67 -0
- package/src/lib/oracle-registry.ts +163 -0
- package/src/lib/orchestrator.ts +357 -0
- package/src/lib/output-boundary.ts +235 -0
- package/src/lib/overrides/vite-native-chaos-harness.ts +154 -0
- package/src/lib/overrides/vite-native-contract-plane.ts +245 -0
- package/src/lib/overrides/vite-native-doctor-plane.ts +208 -0
- package/src/lib/overrides/vite-native-knowledge-store.ts +267 -0
- package/src/lib/overrides/vite-native-policy-plane.ts +190 -0
- package/src/lib/overrides/vite-native-replay-plane.ts +197 -0
- package/src/lib/overrides/vite-native-runtime-plane.ts +106 -0
- package/src/lib/overrides/vite-native-scraper.ts +848 -0
- package/src/lib/overrides/vite-native-selftest-functional.ts +217 -0
- package/src/lib/overrides/vite-native-selftest.ts +183 -0
- package/src/lib/overrides/vite-native-snapshot-plane.ts +123 -0
- package/src/lib/philosophy-toolkit.ts +75 -0
- package/src/lib/pipeline.orig.ts +659 -0
- package/src/lib/pipeline.ts +42 -0
- package/src/lib/precache.ts +128 -0
- package/src/lib/py-sandbox.ts +205 -0
- package/src/lib/quality-score.ts +96 -0
- package/src/lib/quant-engine.ts +231 -0
- package/src/lib/quant-lib.ts +80 -0
- package/src/lib/reportos.ts +175 -0
- package/src/lib/research-os.ts +306 -0
- package/src/lib/resource-estimator.ts +114 -0
- package/src/lib/rpm-governor.ts +93 -0
- package/src/lib/runtime-estimator.ts +135 -0
- package/src/lib/sage/empty-space-v6.ts +560 -0
- package/src/lib/sage/empty-space-v7.ts +222 -0
- package/src/lib/sage/empty-space.ts +733 -0
- package/src/lib/sage/engine.ts +786 -0
- package/src/lib/sage/index.ts +17 -0
- package/src/lib/sage/judge.ts +246 -0
- package/src/lib/sage/prng.ts +53 -0
- package/src/lib/sage/reality-grounding.ts +380 -0
- package/src/lib/sage/repetition.ts +151 -0
- package/src/lib/sage/saga-os-v2-engines.ts +1870 -0
- package/src/lib/sage/saga-os-v2.ts +274 -0
- package/src/lib/sage/saga-os-v3-core.ts +1491 -0
- package/src/lib/sage/saga-os-v3-release.ts +739 -0
- package/src/lib/sage/saga-os-v4-aureate.ts +1161 -0
- package/src/lib/sage/saga-os-v4.ts +212 -0
- package/src/lib/sage/saga-os-v5.ts +205 -0
- package/src/lib/sage/saga-os-v6.ts +346 -0
- package/src/lib/sage/saga-os-v7.ts +314 -0
- package/src/lib/sage/saga-os-v8.ts +505 -0
- package/src/lib/sage/saga-os.ts +329 -0
- package/src/lib/sage/taxonomy.ts +403 -0
- package/src/lib/sage/unified-roll.ts +524 -0
- package/src/lib/sage/worked-sketches.ts +44 -0
- package/src/lib/sage.ts +166 -0
- package/src/lib/scraper-debug-bus.ts +53 -0
- package/src/lib/scraper-enhanced.orig.ts +153 -0
- package/src/lib/scraper-enhanced.ts +10 -0
- package/src/lib/scraper-hardener.base.orig.ts +155 -0
- package/src/lib/scraper-hardener.base.ts +317 -0
- package/src/lib/scraper-hardener.ts +1 -0
- package/src/lib/scraper-palisade/palisade-adjudicator.orig.ts +419 -0
- package/src/lib/scraper-palisade/palisade-adjudicator.ts +13 -0
- package/src/lib/scraper-vnext/arbiter-omega.orig.ts +688 -0
- package/src/lib/scraper-vnext/arbiter-omega.ts +13 -0
- package/src/lib/scraper-vnext/canonical-portfolio-augments.ts +479 -0
- package/src/lib/scraper-vnext/canonical-portfolio-orchestrator.orig.ts +1529 -0
- package/src/lib/scraper-vnext/canonical-portfolio-orchestrator.ts +13 -0
- package/src/lib/scraper-vnext/conclave-omega.ts +1830 -0
- package/src/lib/scraper-vnext/content-extractor-v2.ts +226 -0
- package/src/lib/scraper-vnext/diagnostics-suite.ts +36 -0
- package/src/lib/scraper-vnext/epistemic-packer.ts +97 -0
- package/src/lib/scraper-vnext/fusion-v2.ts +65 -0
- package/src/lib/scraper-vnext/hydra-reader.orig.ts +1216 -0
- package/src/lib/scraper-vnext/hydra-reader.ts +13 -0
- package/src/lib/scraper-vnext/native-scraper-browser-vnext.orig.ts +91 -0
- package/src/lib/scraper-vnext/native-scraper-browser-vnext.ts +13 -0
- package/src/lib/scraper-vnext/nexus-consensus.orig.ts +905 -0
- package/src/lib/scraper-vnext/nexus-consensus.ts +13 -0
- package/src/lib/scraper-vnext/portfolio-consensus-adjudicator.ts +804 -0
- package/src/lib/scraper-vnext/portfolio-consensus-memory.ts +377 -0
- package/src/lib/scraper-vnext/portfolio-terminal-governor.ts +548 -0
- package/src/lib/scraper-vnext/query-strategist.orig.ts +224 -0
- package/src/lib/scraper-vnext/query-strategist.ts +423 -0
- package/src/lib/scraper-vnext/retrieval-accelerator.ts +708 -0
- package/src/lib/scraper-vnext/retrieval-audit-augments.ts +477 -0
- package/src/lib/scraper-vnext/retrieval-control-plane.ts +904 -0
- package/src/lib/scraper-vnext/retrieval-policy-augments.ts +358 -0
- package/src/lib/scraper-vnext/safe-fetch-v2.ts +157 -0
- package/src/lib/scraper-vnext/sentinel-omega.ts +56 -0
- package/src/lib/scraper-vnext/sentinel-orchestrator.ts +69 -0
- package/src/lib/scraper-vnext/sibyl-oracle.orig.ts +413 -0
- package/src/lib/scraper-vnext/sibyl-oracle.ts +13 -0
- package/src/lib/scraper-vnext/smart-read-v2.ts +55 -0
- package/src/lib/scraper-vnext/spa-rescue-bridge.ts +608 -0
- package/src/lib/scraper-vnext/strata-engine.orig.ts +2666 -0
- package/src/lib/scraper-vnext/strata-engine.ts +13 -0
- package/src/lib/scraper-vnext/structured-source-adapter.orig.ts +369 -0
- package/src/lib/scraper-vnext/structured-source-adapter.ts +34 -0
- package/src/lib/scraper-vnext/terminal-complete.ts +224 -0
- package/src/lib/scraper-vnext/terminal-final.ts +277 -0
- package/src/lib/scraper-vnext/terminal-saturation.ts +345 -0
- package/src/lib/scraper-vnext/terminal-wire.ts +425 -0
- package/src/lib/scraper-vnext/vanguard-titanium.orig.ts +467 -0
- package/src/lib/scraper-vnext/vanguard-titanium.ts +17 -0
- package/src/lib/search-cache.ts +138 -0
- package/src/lib/sloop-runner.base.ts +223 -0
- package/src/lib/sloop-runner.ts +104 -0
- package/src/lib/sloop.ts +114 -0
- package/src/lib/small-model-adapters.ts +82 -0
- package/src/lib/sscp.ts +86 -0
- package/src/lib/system-map.ts +54 -0
- package/src/lib/uni-node.ts +86 -0
- package/src/lib/universal-rigor-guard.ts +433 -0
- package/src/lib/v15-gate-testbed.ts +274 -0
- package/src/lib/v15-gemini.base.ts +89 -0
- package/src/lib/v15-gemini.ts +1 -0
- package/src/lib/v15-grounding.orig.ts +241 -0
- package/src/lib/v15-grounding.ts +79 -0
- package/src/lib/v15-pipeline.base.ts +769 -0
- package/src/lib/v15-pipeline.orig.ts +1154 -0
- package/src/lib/v15-pipeline.ts +674 -0
- package/src/lib/v15-questions.ts +64 -0
- package/src/lib/v15-rate-limiter.orig.ts +91 -0
- package/src/lib/v15-rate-limiter.ts +48 -0
- package/src/lib/v15-state.base.ts +87 -0
- package/src/lib/v15-state.ts +28 -0
- package/src/lib/visual-table-generator.tsx +207 -0
- package/src/lib/wasm-runtime.ts +133 -0
- package/src/lib/williams-persona-guide.ts +265 -0
- package/src/lib/williams-style.base.ts +464 -0
- package/src/lib/williams-style.ts +59 -0
- package/src/lib/worker-pool.ts +96 -0
- package/src/lib/writing-tiers.ts +125 -0
- package/src/main.orig.tsx +10 -0
- package/src/main.tsx +10 -0
- package/src/utils/cn.orig.ts +6 -0
- package/src/utils/cn.ts +6 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +31 -0
- package/unify.mjs +537 -0
- package/vite.config.ts +18 -0
|
@@ -0,0 +1,2549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* innovation-genome-v7.ts — Production Reliability Plane over V1–V6.
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* Per the supplied spec: the file named `innovation_genome_v5.py` is treated as
|
|
5
|
+
* canonical V6. In this workspace that dependency is `@/lib/innovation-genome-v5`.
|
|
6
|
+
* V7 adds only new behavior; V1–V6 are unchanged.
|
|
7
|
+
*
|
|
8
|
+
* V7 adds:
|
|
9
|
+
* 1. Versioned, checksummed schema migrations.
|
|
10
|
+
* 2. SQLite WAL-reset vulnerability guard (version predicate ported exactly).
|
|
11
|
+
* 3. Hardened store configuration + verification.
|
|
12
|
+
* 4. Atomic domain-write + semantic-event units of work.
|
|
13
|
+
* 5. Durable work queue: idempotent enqueue, leases, heartbeats, monotonic
|
|
14
|
+
* fencing tokens, retry-safe full-jitter backoff, dead-lettering,
|
|
15
|
+
* stale-worker rejection.
|
|
16
|
+
* 6. Bounded, redacted, HMAC-pseudonymized telemetry.
|
|
17
|
+
* 7. Exact required-verifier closure checking.
|
|
18
|
+
* 8. RFC 9162-style Merkle challenge commitments and inclusion proofs.
|
|
19
|
+
* 9. Verified backup/restore (in-browser: structural snapshot + chain revalidation).
|
|
20
|
+
* 10. Production doctor / readiness report.
|
|
21
|
+
*
|
|
22
|
+
* RUNTIME HONESTY
|
|
23
|
+
* ---------------
|
|
24
|
+
* No Python, no native SQLite, no filesystem, no browser in this session.
|
|
25
|
+
* This is a structural/semantic port verified by side-by-side reading, not by
|
|
26
|
+
* execute-and-diff. Disclosed browser substitutions:
|
|
27
|
+
* - SQLite file/PRAGMA layer → in-memory typed collections carried by the
|
|
28
|
+
* V5/V4/V3 store chain. `sqliteWalVersionIsSafe` is ported EXACTLY as a
|
|
29
|
+
* pure predicate and is unit-tested in diagnostics, because it is real
|
|
30
|
+
* logic independent of whether SQLite is present.
|
|
31
|
+
* - `connection.backup()` → a structural snapshot + full chain revalidation
|
|
32
|
+
* (the verification semantics are preserved; the file I/O is not possible).
|
|
33
|
+
* - HMAC → keyed SHA-256 (local integrity only; same caveat the Python
|
|
34
|
+
* spec itself states for its HMAC signer).
|
|
35
|
+
* Everything that is pure logic (migrations, jitter, fencing, Merkle proofs,
|
|
36
|
+
* telemetry sanitization, verifier closure, doctor checks) is ported 1:1 and
|
|
37
|
+
* is genuinely executable in the browser.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
canonicalJson,
|
|
42
|
+
sha256Text,
|
|
43
|
+
utcNow,
|
|
44
|
+
} from "@/lib/innovation-genome-v3";
|
|
45
|
+
import {
|
|
46
|
+
CanaryRegistry,
|
|
47
|
+
ChallengeSuite,
|
|
48
|
+
ChallengeSuiteRegistry,
|
|
49
|
+
FunctionVerifier,
|
|
50
|
+
HealthThresholds,
|
|
51
|
+
PolicyEngine,
|
|
52
|
+
ReplayManifest,
|
|
53
|
+
ReplayMatrix,
|
|
54
|
+
RunHealthMonitor,
|
|
55
|
+
TelemetryRecorder,
|
|
56
|
+
V5AssuranceStore,
|
|
57
|
+
VerificationResult,
|
|
58
|
+
VerifierOrchestrator,
|
|
59
|
+
defaultPolicyRules,
|
|
60
|
+
type SpanContext,
|
|
61
|
+
} from "@/lib/innovation-genome-v5";
|
|
62
|
+
|
|
63
|
+
export const ZERO_HASH = "0".repeat(64);
|
|
64
|
+
export const V7_APPLICATION_ID = 0x49474537; // "IGE7"
|
|
65
|
+
export const V7_SCHEMA_VERSION = 3;
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
68
|
+
// 1. Runtime and store configuration
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
export class RuntimeCompatibilityError extends Error {}
|
|
72
|
+
export class MigrationError extends Error {}
|
|
73
|
+
export class LeaseLostError extends Error {}
|
|
74
|
+
|
|
75
|
+
export interface StoreConfigOptions {
|
|
76
|
+
walMode?: boolean;
|
|
77
|
+
synchronous?: "FULL" | "EXTRA";
|
|
78
|
+
busyTimeoutMs?: number;
|
|
79
|
+
requireSafeMultiwriterWal?: boolean;
|
|
80
|
+
requireDefensiveMode?: boolean;
|
|
81
|
+
trustedSchema?: boolean;
|
|
82
|
+
cellSizeCheck?: boolean;
|
|
83
|
+
transactionRetries?: number;
|
|
84
|
+
transactionRetryBaseSeconds?: number;
|
|
85
|
+
transactionRetryCapSeconds?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class StoreConfig {
|
|
89
|
+
readonly walMode: boolean;
|
|
90
|
+
readonly synchronous: "FULL" | "EXTRA";
|
|
91
|
+
readonly busyTimeoutMs: number;
|
|
92
|
+
readonly requireSafeMultiwriterWal: boolean;
|
|
93
|
+
readonly requireDefensiveMode: boolean;
|
|
94
|
+
readonly trustedSchema: boolean;
|
|
95
|
+
readonly cellSizeCheck: boolean;
|
|
96
|
+
readonly transactionRetries: number;
|
|
97
|
+
readonly transactionRetryBaseSeconds: number;
|
|
98
|
+
readonly transactionRetryCapSeconds: number;
|
|
99
|
+
|
|
100
|
+
constructor(options: StoreConfigOptions = {}) {
|
|
101
|
+
this.walMode = options.walMode ?? true;
|
|
102
|
+
this.synchronous = options.synchronous ?? "FULL";
|
|
103
|
+
this.busyTimeoutMs = options.busyTimeoutMs ?? 15_000;
|
|
104
|
+
this.requireSafeMultiwriterWal = options.requireSafeMultiwriterWal ?? true;
|
|
105
|
+
this.requireDefensiveMode = options.requireDefensiveMode ?? false;
|
|
106
|
+
this.trustedSchema = options.trustedSchema ?? false;
|
|
107
|
+
this.cellSizeCheck = options.cellSizeCheck ?? true;
|
|
108
|
+
this.transactionRetries = options.transactionRetries ?? 8;
|
|
109
|
+
this.transactionRetryBaseSeconds = options.transactionRetryBaseSeconds ?? 0.01;
|
|
110
|
+
this.transactionRetryCapSeconds = options.transactionRetryCapSeconds ?? 0.5;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
validate(): void {
|
|
114
|
+
if (this.synchronous !== "FULL" && this.synchronous !== "EXTRA") {
|
|
115
|
+
throw new Error("Production synchronous mode must be FULL or EXTRA");
|
|
116
|
+
}
|
|
117
|
+
if (this.busyTimeoutMs < 1) throw new Error("busyTimeoutMs must be positive");
|
|
118
|
+
if (this.transactionRetries < 0) throw new Error("transactionRetries cannot be negative");
|
|
119
|
+
if (this.transactionRetryBaseSeconds < 0) {
|
|
120
|
+
throw new Error("transactionRetryBaseSeconds cannot be negative");
|
|
121
|
+
}
|
|
122
|
+
if (this.transactionRetryCapSeconds < this.transactionRetryBaseSeconds) {
|
|
123
|
+
throw new Error("transaction retry cap cannot be below its base");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ported EXACTLY from the Python. Returns whether the runtime includes the
|
|
130
|
+
* March 2026 WAL-reset fix. Fixed lines: 3.51.3+, 3.50.7+ (3.50 branch),
|
|
131
|
+
* 3.44.6+ (3.44 branch). Branches above 3.51 treated as fixed.
|
|
132
|
+
*/
|
|
133
|
+
export function sqliteWalVersionIsSafe(version: readonly [number, number, number]): boolean {
|
|
134
|
+
const [major, minor, patch] = version;
|
|
135
|
+
|
|
136
|
+
if (major > 3 || (major === 3 && (minor > 51 || (minor === 51 && patch >= 3)))) return true;
|
|
137
|
+
if (major === 3 && minor === 50 && patch >= 7) return true;
|
|
138
|
+
if (major === 3 && minor === 44 && patch >= 6) return true;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Deterministic full-jitter backoff — ported exactly (SHA-256 derived fraction). */
|
|
143
|
+
export function deterministicFullJitter(
|
|
144
|
+
identity: string,
|
|
145
|
+
attempt: number,
|
|
146
|
+
base: number,
|
|
147
|
+
cap: number,
|
|
148
|
+
): number {
|
|
149
|
+
const ceiling = Math.min(cap, base * Math.pow(2, Math.max(0, attempt)));
|
|
150
|
+
const digestHex = sha256Text(`${identity}:${attempt}`);
|
|
151
|
+
// First 8 bytes big-endian / (2^64 - 1)
|
|
152
|
+
let value = 0n;
|
|
153
|
+
for (let i = 0; i < 8; i++) {
|
|
154
|
+
value = (value << 8n) | BigInt(parseInt(digestHex.slice(i * 2, i * 2 + 2), 16));
|
|
155
|
+
}
|
|
156
|
+
const fraction = Number(value) / Number(2n ** 64n - 1n);
|
|
157
|
+
return ceiling * fraction;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
161
|
+
// 2. Checksummed schema migrations
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
163
|
+
|
|
164
|
+
export class SchemaMigration {
|
|
165
|
+
constructor(
|
|
166
|
+
public readonly version: number,
|
|
167
|
+
public readonly name: string,
|
|
168
|
+
public readonly statements: readonly string[],
|
|
169
|
+
) {}
|
|
170
|
+
|
|
171
|
+
get checksum(): string {
|
|
172
|
+
return sha256Text(
|
|
173
|
+
canonicalJson({
|
|
174
|
+
version: this.version,
|
|
175
|
+
name: this.name,
|
|
176
|
+
statements: this.statements,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const V7_MIGRATIONS: readonly SchemaMigration[] = [
|
|
183
|
+
new SchemaMigration(1, "durable_jobs", [
|
|
184
|
+
"CREATE TABLE IF NOT EXISTS v7_jobs (...)",
|
|
185
|
+
"CREATE INDEX IF NOT EXISTS idx_v7_jobs_claim (...)",
|
|
186
|
+
"CREATE TABLE IF NOT EXISTS v7_job_attempts (...)",
|
|
187
|
+
"CREATE TABLE IF NOT EXISTS v7_dead_letters (...)",
|
|
188
|
+
]),
|
|
189
|
+
new SchemaMigration(2, "verifier_closure_and_merkle_suites", [
|
|
190
|
+
"CREATE TABLE IF NOT EXISTS v7_verifier_closure (...)",
|
|
191
|
+
"CREATE TABLE IF NOT EXISTS v7_merkle_suites (...)",
|
|
192
|
+
]),
|
|
193
|
+
new SchemaMigration(3, "verified_backups", [
|
|
194
|
+
"CREATE TABLE IF NOT EXISTS v7_backups (...)",
|
|
195
|
+
"CREATE INDEX IF NOT EXISTS idx_v7_backups_run_time (...)",
|
|
196
|
+
]),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
export interface MigrationRow {
|
|
200
|
+
version: number;
|
|
201
|
+
name: string;
|
|
202
|
+
checksum: string;
|
|
203
|
+
appliedUtc: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
207
|
+
// 3. Production assurance store
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
export type JobStatus = "pending" | "leased" | "succeeded" | "dead_letter" | "cancelled";
|
|
211
|
+
|
|
212
|
+
export interface JobRow {
|
|
213
|
+
jobId: string;
|
|
214
|
+
runId: string;
|
|
215
|
+
queueName: string;
|
|
216
|
+
jobKind: string;
|
|
217
|
+
payloadJson: string;
|
|
218
|
+
payloadHash: string;
|
|
219
|
+
idempotencyHash: string;
|
|
220
|
+
retrySafe: boolean;
|
|
221
|
+
status: JobStatus;
|
|
222
|
+
priority: number;
|
|
223
|
+
availableUnix: number;
|
|
224
|
+
leaseOwner: string;
|
|
225
|
+
leaseExpiresUnix: number;
|
|
226
|
+
fencingToken: number;
|
|
227
|
+
attempts: number;
|
|
228
|
+
maxAttempts: number;
|
|
229
|
+
resultJson: string;
|
|
230
|
+
resultHash: string;
|
|
231
|
+
lastErrorType: string;
|
|
232
|
+
lastErrorHash: string;
|
|
233
|
+
createdUtc: string;
|
|
234
|
+
updatedUtc: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface JobAttemptRow {
|
|
238
|
+
attemptId: string;
|
|
239
|
+
jobId: string;
|
|
240
|
+
attemptNumber: number;
|
|
241
|
+
fencingToken: number;
|
|
242
|
+
workerId: string;
|
|
243
|
+
startedUtc: string;
|
|
244
|
+
endedUtc: string;
|
|
245
|
+
outcome: string;
|
|
246
|
+
errorType: string;
|
|
247
|
+
errorHash: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface DeadLetterRow {
|
|
251
|
+
jobId: string;
|
|
252
|
+
runId: string;
|
|
253
|
+
queueName: string;
|
|
254
|
+
reason: string;
|
|
255
|
+
errorType: string;
|
|
256
|
+
errorHash: string;
|
|
257
|
+
deadLetteredUtc: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface VerifierClosureRow {
|
|
261
|
+
runId: string;
|
|
262
|
+
candidateId: string;
|
|
263
|
+
policyHash: string;
|
|
264
|
+
closed: boolean;
|
|
265
|
+
reportJson: string;
|
|
266
|
+
reportHash: string;
|
|
267
|
+
createdUtc: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface MerkleSuiteRow {
|
|
271
|
+
suiteId: string;
|
|
272
|
+
runId: string;
|
|
273
|
+
name: string;
|
|
274
|
+
rootHash: string;
|
|
275
|
+
treeSize: number;
|
|
276
|
+
hashAlgorithm: string;
|
|
277
|
+
createdUtc: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface BackupRow {
|
|
281
|
+
backupId: string;
|
|
282
|
+
runId: string;
|
|
283
|
+
targetPath: string;
|
|
284
|
+
fileDigest: string;
|
|
285
|
+
sizeBytes: number;
|
|
286
|
+
applicationId: number;
|
|
287
|
+
schemaVersion: number;
|
|
288
|
+
snapshotSemanticHash: string;
|
|
289
|
+
integrityValid: boolean;
|
|
290
|
+
foreignKeysValid: boolean;
|
|
291
|
+
eventChainValid: boolean;
|
|
292
|
+
semanticChainValid: boolean;
|
|
293
|
+
createdUnix: number;
|
|
294
|
+
createdUtc: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* V7 store with startup validation, migration management, and atomic
|
|
299
|
+
* domain-write + event transactions.
|
|
300
|
+
*/
|
|
301
|
+
export class ProductionAssuranceStore extends V5AssuranceStore {
|
|
302
|
+
readonly config: StoreConfig;
|
|
303
|
+
readonly isMemory: boolean;
|
|
304
|
+
private applicationId = 0;
|
|
305
|
+
private userVersion = 0;
|
|
306
|
+
private migrations: MigrationRow[] = [];
|
|
307
|
+
|
|
308
|
+
protected jobs = new Map<string, JobRow>();
|
|
309
|
+
protected jobAttempts = new Map<string, JobAttemptRow>();
|
|
310
|
+
protected deadLetters = new Map<string, DeadLetterRow>();
|
|
311
|
+
protected verifierClosures = new Map<string, VerifierClosureRow>();
|
|
312
|
+
protected merkleSuites = new Map<string, MerkleSuiteRow>();
|
|
313
|
+
protected backups = new Map<string, BackupRow>();
|
|
314
|
+
|
|
315
|
+
constructor(config?: StoreConfig, persistenceKey: string | null = null) {
|
|
316
|
+
super(persistenceKey);
|
|
317
|
+
this.config = config ?? new StoreConfig();
|
|
318
|
+
this.config.validate();
|
|
319
|
+
this.isMemory = persistenceKey === null;
|
|
320
|
+
|
|
321
|
+
// WAL-reset guard: fail closed BEFORE any schema work, exactly as Python does.
|
|
322
|
+
// In-browser there is no real SQLite, so the runtime version is reported as
|
|
323
|
+
// "absent". When `walMode` + `requireSafeMultiwriterWal` are both set AND a
|
|
324
|
+
// real sqlite version is supplied via `sqliteVersionOverride`, the guard runs.
|
|
325
|
+
const declared = ProductionAssuranceStore.declaredSqliteVersion;
|
|
326
|
+
if (
|
|
327
|
+
this.config.walMode &&
|
|
328
|
+
!this.isMemory &&
|
|
329
|
+
this.config.requireSafeMultiwriterWal &&
|
|
330
|
+
declared !== null &&
|
|
331
|
+
!sqliteWalVersionIsSafe(declared)
|
|
332
|
+
) {
|
|
333
|
+
throw new RuntimeCompatibilityError(
|
|
334
|
+
`Refusing multi-writer WAL on SQLite ${declared.join(".")}. ` +
|
|
335
|
+
"Install SQLite 3.51.3+, 3.50.7, 3.44.6, or another explicitly " +
|
|
336
|
+
"patched build; or use rollback-journal mode.",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.prepareDatabaseIdentity();
|
|
341
|
+
this.applyMigrations();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Set when a host embeds a real SQLite (e.g. wa-sqlite/OPFS). Null means "no
|
|
346
|
+
* SQLite present", which is the honest state of a plain browser bundle — the
|
|
347
|
+
* guard cannot be evaluated and is therefore not claimed to have run.
|
|
348
|
+
*/
|
|
349
|
+
static declaredSqliteVersion: readonly [number, number, number] | null = null;
|
|
350
|
+
|
|
351
|
+
private prepareDatabaseIdentity(): void {
|
|
352
|
+
if (this.applicationId !== 0 && this.applicationId !== V7_APPLICATION_ID) {
|
|
353
|
+
throw new MigrationError(
|
|
354
|
+
`Database application_id belongs to another application: ${this.applicationId}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (this.userVersion !== 0 && this.migrations.length === 0) {
|
|
358
|
+
throw new MigrationError(
|
|
359
|
+
"Database has a nonzero user_version but no V7 migration ledger; " +
|
|
360
|
+
"refusing to overwrite schema ownership",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (this.applicationId === 0) this.applicationId = V7_APPLICATION_ID;
|
|
364
|
+
if (this.applicationId !== V7_APPLICATION_ID) {
|
|
365
|
+
throw new MigrationError("Could not establish V7 application_id");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private applyMigrations(): void {
|
|
370
|
+
const byVersion = new Map(V7_MIGRATIONS.map((m) => [m.version, m]));
|
|
371
|
+
const versions = [...byVersion.keys()].sort((a, b) => a - b);
|
|
372
|
+
const expected = Array.from({ length: V7_MIGRATIONS.length }, (_, i) => i + 1);
|
|
373
|
+
if (JSON.stringify(versions) !== JSON.stringify(expected)) {
|
|
374
|
+
throw new MigrationError("V7 migrations must be contiguous from version 1");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const row of this.migrations) {
|
|
378
|
+
const migration = byVersion.get(row.version);
|
|
379
|
+
if (!migration) {
|
|
380
|
+
throw new MigrationError(`Database contains unknown future migration ${row.version}`);
|
|
381
|
+
}
|
|
382
|
+
if (row.checksum !== migration.checksum) {
|
|
383
|
+
throw new MigrationError(`Migration checksum mismatch at version ${row.version}`);
|
|
384
|
+
}
|
|
385
|
+
if (row.name !== migration.name) {
|
|
386
|
+
throw new MigrationError(`Migration name mismatch at version ${row.version}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const recorded = new Set(this.migrations.map((m) => m.version));
|
|
391
|
+
for (const migration of V7_MIGRATIONS) {
|
|
392
|
+
if (recorded.has(migration.version)) continue;
|
|
393
|
+
this.migrations.push({
|
|
394
|
+
version: migration.version,
|
|
395
|
+
name: migration.name,
|
|
396
|
+
checksum: migration.checksum,
|
|
397
|
+
appliedUtc: utcNow(),
|
|
398
|
+
});
|
|
399
|
+
this.userVersion = migration.version;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (this.userVersion !== V7_SCHEMA_VERSION) {
|
|
403
|
+
throw new MigrationError(
|
|
404
|
+
`Expected schema version ${V7_SCHEMA_VERSION}; found ${this.userVersion}`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
migrationReport(): MigrationRow[] {
|
|
410
|
+
return [...this.migrations].sort((a, b) => a.version - b.version);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getApplicationId(): number {
|
|
414
|
+
return this.applicationId;
|
|
415
|
+
}
|
|
416
|
+
getUserVersion(): number {
|
|
417
|
+
return this.userVersion;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Execute domain changes and the associated audit event as ONE unit.
|
|
422
|
+
* The operation callback MUST NOT perform externally visible side effects,
|
|
423
|
+
* because a failure rolls the whole unit back.
|
|
424
|
+
*
|
|
425
|
+
* Browser note: there is no real BEGIN IMMEDIATE. Atomicity is achieved by
|
|
426
|
+
* staging mutations into a journal and only committing them (plus the event)
|
|
427
|
+
* when the operation returns without throwing. This preserves the OBSERVABLE
|
|
428
|
+
* contract the Python relies on — a thrown operation leaves neither domain
|
|
429
|
+
* rows nor an audit event behind.
|
|
430
|
+
*/
|
|
431
|
+
atomicChange<T>(
|
|
432
|
+
runId: string,
|
|
433
|
+
operation: (journal: MutationJournal) => T,
|
|
434
|
+
eventBuilder: (result: T) => [string, Record<string, unknown>] | null,
|
|
435
|
+
): T {
|
|
436
|
+
const journal = new MutationJournal();
|
|
437
|
+
let result: T;
|
|
438
|
+
try {
|
|
439
|
+
result = operation(journal);
|
|
440
|
+
} catch (exc) {
|
|
441
|
+
journal.discard();
|
|
442
|
+
throw exc;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const event = eventBuilder(result);
|
|
446
|
+
journal.commit();
|
|
447
|
+
if (event) this.appendEvent(runId, event[0], event[1]);
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── job accessors ───────────────────────────────────────────────────
|
|
452
|
+
_getJob(jobId: string): JobRow | undefined {
|
|
453
|
+
return this.jobs.get(jobId);
|
|
454
|
+
}
|
|
455
|
+
_putJob(row: JobRow): void {
|
|
456
|
+
this.jobs.set(row.jobId, row);
|
|
457
|
+
}
|
|
458
|
+
_findJobByIdempotency(runId: string, queueName: string, idempotencyHash: string): JobRow | undefined {
|
|
459
|
+
for (const job of this.jobs.values()) {
|
|
460
|
+
if (job.runId === runId && job.queueName === queueName && job.idempotencyHash === idempotencyHash) {
|
|
461
|
+
return job;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
_jobsForQueue(runId: string, queueName: string): JobRow[] {
|
|
467
|
+
return [...this.jobs.values()].filter((j) => j.runId === runId && j.queueName === queueName);
|
|
468
|
+
}
|
|
469
|
+
_jobsForRun(runId: string): JobRow[] {
|
|
470
|
+
return [...this.jobs.values()].filter((j) => j.runId === runId);
|
|
471
|
+
}
|
|
472
|
+
_putJobAttempt(row: JobAttemptRow): void {
|
|
473
|
+
this.jobAttempts.set(row.attemptId, row);
|
|
474
|
+
}
|
|
475
|
+
_updateJobAttempt(jobId: string, fencingToken: number, patch: Partial<JobAttemptRow>): void {
|
|
476
|
+
for (const attempt of this.jobAttempts.values()) {
|
|
477
|
+
if (attempt.jobId === jobId && attempt.fencingToken === fencingToken) {
|
|
478
|
+
Object.assign(attempt, patch);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
_putDeadLetter(row: DeadLetterRow): void {
|
|
483
|
+
this.deadLetters.set(row.jobId, row);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_putVerifierClosure(row: VerifierClosureRow): void {
|
|
487
|
+
this.verifierClosures.set(`${row.runId}:${row.candidateId}:${row.policyHash}`, row);
|
|
488
|
+
}
|
|
489
|
+
_putMerkleSuite(row: MerkleSuiteRow): void {
|
|
490
|
+
if (this.merkleSuites.has(row.suiteId)) {
|
|
491
|
+
throw new Error(`Merkle suite already registered: ${row.suiteId}`);
|
|
492
|
+
}
|
|
493
|
+
this.merkleSuites.set(row.suiteId, row);
|
|
494
|
+
}
|
|
495
|
+
_getMerkleSuite(suiteId: string, runId: string): MerkleSuiteRow | undefined {
|
|
496
|
+
const row = this.merkleSuites.get(suiteId);
|
|
497
|
+
return row && row.runId === runId ? row : undefined;
|
|
498
|
+
}
|
|
499
|
+
_putBackup(row: BackupRow): void {
|
|
500
|
+
this.backups.set(row.backupId, row);
|
|
501
|
+
}
|
|
502
|
+
_latestBackupUnix(runId: string): number | null {
|
|
503
|
+
let latest: number | null = null;
|
|
504
|
+
for (const backup of this.backups.values()) {
|
|
505
|
+
if (backup.runId !== runId) continue;
|
|
506
|
+
if (latest === null || backup.createdUnix > latest) latest = backup.createdUnix;
|
|
507
|
+
}
|
|
508
|
+
return latest;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Staged mutation buffer giving atomicity without a real SQL transaction. */
|
|
513
|
+
export class MutationJournal {
|
|
514
|
+
private readonly applies: Array<() => void> = [];
|
|
515
|
+
|
|
516
|
+
stage(apply: () => void): void {
|
|
517
|
+
this.applies.push(apply);
|
|
518
|
+
}
|
|
519
|
+
commit(): void {
|
|
520
|
+
for (const apply of this.applies) apply();
|
|
521
|
+
this.applies.length = 0;
|
|
522
|
+
}
|
|
523
|
+
discard(): void {
|
|
524
|
+
this.applies.length = 0;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
529
|
+
// 4. Durable leased work queue
|
|
530
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
531
|
+
|
|
532
|
+
export interface JobSpecOptions {
|
|
533
|
+
queueName: string;
|
|
534
|
+
jobKind: string;
|
|
535
|
+
payload: Record<string, unknown>;
|
|
536
|
+
idempotencyKey: string;
|
|
537
|
+
retrySafe?: boolean;
|
|
538
|
+
priority?: number;
|
|
539
|
+
availableUnix?: number;
|
|
540
|
+
maxAttempts?: number;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export class JobSpec {
|
|
544
|
+
readonly queueName: string;
|
|
545
|
+
readonly jobKind: string;
|
|
546
|
+
readonly payload: Record<string, unknown>;
|
|
547
|
+
readonly idempotencyKey: string;
|
|
548
|
+
readonly retrySafe: boolean;
|
|
549
|
+
readonly priority: number;
|
|
550
|
+
readonly availableUnix: number;
|
|
551
|
+
readonly maxAttempts: number;
|
|
552
|
+
|
|
553
|
+
constructor(options: JobSpecOptions) {
|
|
554
|
+
this.queueName = options.queueName;
|
|
555
|
+
this.jobKind = options.jobKind;
|
|
556
|
+
this.payload = options.payload;
|
|
557
|
+
this.idempotencyKey = options.idempotencyKey;
|
|
558
|
+
this.retrySafe = options.retrySafe ?? true;
|
|
559
|
+
this.priority = options.priority ?? 0;
|
|
560
|
+
this.availableUnix = options.availableUnix ?? 0;
|
|
561
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
validate(): void {
|
|
565
|
+
if (!this.queueName.trim()) throw new Error("queueName is required");
|
|
566
|
+
if (!this.jobKind.trim()) throw new Error("jobKind is required");
|
|
567
|
+
if (this.idempotencyKey.length < 16) {
|
|
568
|
+
throw new Error("idempotencyKey must contain at least 16 characters");
|
|
569
|
+
}
|
|
570
|
+
if (this.idempotencyKey.length > 512) throw new Error("idempotencyKey is too long");
|
|
571
|
+
if (this.maxAttempts < 1) throw new Error("maxAttempts must be positive");
|
|
572
|
+
if (!this.retrySafe && this.maxAttempts !== 1) {
|
|
573
|
+
throw new Error("Non-retry-safe jobs must use maxAttempts=1");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export interface JobLease {
|
|
579
|
+
jobId: string;
|
|
580
|
+
runId: string;
|
|
581
|
+
queueName: string;
|
|
582
|
+
jobKind: string;
|
|
583
|
+
payload: Record<string, unknown>;
|
|
584
|
+
workerId: string;
|
|
585
|
+
fencingToken: number;
|
|
586
|
+
attemptNumber: number;
|
|
587
|
+
leaseExpiresUnix: number;
|
|
588
|
+
retrySafe: boolean;
|
|
589
|
+
maxAttempts: number;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export class RetryPolicy {
|
|
593
|
+
constructor(
|
|
594
|
+
public readonly baseSeconds = 1.0,
|
|
595
|
+
public readonly capSeconds = 300.0,
|
|
596
|
+
) {}
|
|
597
|
+
|
|
598
|
+
delay(jobId: string, attemptNumber: number): number {
|
|
599
|
+
return deterministicFullJitter(
|
|
600
|
+
jobId,
|
|
601
|
+
Math.max(0, attemptNumber - 1),
|
|
602
|
+
this.baseSeconds,
|
|
603
|
+
this.capSeconds,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export class DurableJobQueue {
|
|
609
|
+
constructor(
|
|
610
|
+
private readonly store: ProductionAssuranceStore,
|
|
611
|
+
private readonly runId: string,
|
|
612
|
+
private readonly retryPolicy: RetryPolicy = new RetryPolicy(),
|
|
613
|
+
private readonly maximumResultChars = 100_000,
|
|
614
|
+
) {}
|
|
615
|
+
|
|
616
|
+
enqueue(spec: JobSpec, nowUnix?: number): string {
|
|
617
|
+
spec.validate();
|
|
618
|
+
const now = nowUnix ?? Date.now() / 1000;
|
|
619
|
+
|
|
620
|
+
const payloadJson = canonicalJson(spec.payload);
|
|
621
|
+
const payloadHash = sha256Text(payloadJson);
|
|
622
|
+
const idempotencyHash = sha256Text(spec.idempotencyKey);
|
|
623
|
+
const jobId =
|
|
624
|
+
"job-" +
|
|
625
|
+
sha256Text(
|
|
626
|
+
canonicalJson({
|
|
627
|
+
run_id: this.runId,
|
|
628
|
+
queue_name: spec.queueName,
|
|
629
|
+
idempotency_hash: idempotencyHash,
|
|
630
|
+
}),
|
|
631
|
+
).slice(0, 32);
|
|
632
|
+
|
|
633
|
+
return this.store.atomicChange<[string, boolean]>(
|
|
634
|
+
this.runId,
|
|
635
|
+
(journal) => {
|
|
636
|
+
const existing = this.store._findJobByIdempotency(
|
|
637
|
+
this.runId,
|
|
638
|
+
spec.queueName,
|
|
639
|
+
idempotencyHash,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (existing) {
|
|
643
|
+
if (
|
|
644
|
+
existing.jobKind !== spec.jobKind ||
|
|
645
|
+
existing.payloadHash !== payloadHash ||
|
|
646
|
+
existing.retrySafe !== spec.retrySafe
|
|
647
|
+
) {
|
|
648
|
+
throw new Error("Idempotency key was reused with different intent");
|
|
649
|
+
}
|
|
650
|
+
return [existing.jobId, false];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
journal.stage(() =>
|
|
654
|
+
this.store._putJob({
|
|
655
|
+
jobId,
|
|
656
|
+
runId: this.runId,
|
|
657
|
+
queueName: spec.queueName,
|
|
658
|
+
jobKind: spec.jobKind,
|
|
659
|
+
payloadJson,
|
|
660
|
+
payloadHash,
|
|
661
|
+
idempotencyHash,
|
|
662
|
+
retrySafe: spec.retrySafe,
|
|
663
|
+
status: "pending",
|
|
664
|
+
priority: spec.priority,
|
|
665
|
+
availableUnix: Math.max(now, spec.availableUnix),
|
|
666
|
+
leaseOwner: "",
|
|
667
|
+
leaseExpiresUnix: 0,
|
|
668
|
+
fencingToken: 0,
|
|
669
|
+
attempts: 0,
|
|
670
|
+
maxAttempts: spec.maxAttempts,
|
|
671
|
+
resultJson: "",
|
|
672
|
+
resultHash: "",
|
|
673
|
+
lastErrorType: "",
|
|
674
|
+
lastErrorHash: "",
|
|
675
|
+
createdUtc: utcNow(),
|
|
676
|
+
updatedUtc: utcNow(),
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
return [jobId, true];
|
|
680
|
+
},
|
|
681
|
+
([resultJobId, created]) => [
|
|
682
|
+
created ? "job_enqueued_v7" : "job_enqueue_deduplicated_v7",
|
|
683
|
+
{
|
|
684
|
+
job_id: resultJobId,
|
|
685
|
+
queue_name: spec.queueName,
|
|
686
|
+
job_kind: spec.jobKind,
|
|
687
|
+
payload_hash: payloadHash,
|
|
688
|
+
idempotency_hash: idempotencyHash,
|
|
689
|
+
retry_safe: spec.retrySafe,
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
)[0];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private deadLetter(
|
|
696
|
+
journal: MutationJournal,
|
|
697
|
+
row: JobRow,
|
|
698
|
+
reason: string,
|
|
699
|
+
errorType = "",
|
|
700
|
+
errorHash = "",
|
|
701
|
+
): void {
|
|
702
|
+
journal.stage(() => {
|
|
703
|
+
row.status = "dead_letter";
|
|
704
|
+
row.leaseOwner = "";
|
|
705
|
+
row.leaseExpiresUnix = 0;
|
|
706
|
+
row.lastErrorType = errorType;
|
|
707
|
+
row.lastErrorHash = errorHash;
|
|
708
|
+
row.updatedUtc = utcNow();
|
|
709
|
+
this.store._putJob(row);
|
|
710
|
+
this.store._putDeadLetter({
|
|
711
|
+
jobId: row.jobId,
|
|
712
|
+
runId: this.runId,
|
|
713
|
+
queueName: row.queueName,
|
|
714
|
+
reason,
|
|
715
|
+
errorType,
|
|
716
|
+
errorHash,
|
|
717
|
+
deadLetteredUtc: utcNow(),
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
claim(
|
|
723
|
+
queueName: string,
|
|
724
|
+
workerId: string,
|
|
725
|
+
options: { leaseSeconds?: number; nowUnix?: number } = {},
|
|
726
|
+
): JobLease | null {
|
|
727
|
+
if (!workerId.trim()) throw new Error("workerId is required");
|
|
728
|
+
const leaseSeconds = options.leaseSeconds ?? 60;
|
|
729
|
+
if (leaseSeconds <= 0) throw new Error("leaseSeconds must be positive");
|
|
730
|
+
const now = options.nowUnix ?? Date.now() / 1000;
|
|
731
|
+
|
|
732
|
+
return this.store.atomicChange<[JobLease | null, string[]]>(
|
|
733
|
+
this.runId,
|
|
734
|
+
(journal) => {
|
|
735
|
+
const all = this.store._jobsForQueue(this.runId, queueName);
|
|
736
|
+
const reaped: string[] = [];
|
|
737
|
+
|
|
738
|
+
// Reap expired leases that cannot be safely retried.
|
|
739
|
+
for (const row of all) {
|
|
740
|
+
if (
|
|
741
|
+
row.status === "leased" &&
|
|
742
|
+
row.leaseExpiresUnix <= now &&
|
|
743
|
+
(!row.retrySafe || row.attempts >= row.maxAttempts)
|
|
744
|
+
) {
|
|
745
|
+
this.deadLetter(journal, row, "lease_expired_without_safe_retry");
|
|
746
|
+
reaped.push(row.jobId);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const claimable = all
|
|
751
|
+
.filter(
|
|
752
|
+
(row) =>
|
|
753
|
+
row.attempts < row.maxAttempts &&
|
|
754
|
+
((row.status === "pending" && row.availableUnix <= now) ||
|
|
755
|
+
(row.status === "leased" && row.retrySafe && row.leaseExpiresUnix <= now)),
|
|
756
|
+
)
|
|
757
|
+
.sort((a, b) => {
|
|
758
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
759
|
+
if (a.availableUnix !== b.availableUnix) return a.availableUnix - b.availableUnix;
|
|
760
|
+
return a.createdUtc.localeCompare(b.createdUtc);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const row = claimable[0];
|
|
764
|
+
if (!row) return [null, reaped];
|
|
765
|
+
|
|
766
|
+
const newToken = row.fencingToken + 1;
|
|
767
|
+
const newAttempt = row.attempts + 1;
|
|
768
|
+
const expires = now + leaseSeconds;
|
|
769
|
+
|
|
770
|
+
journal.stage(() => {
|
|
771
|
+
row.status = "leased";
|
|
772
|
+
row.leaseOwner = workerId;
|
|
773
|
+
row.leaseExpiresUnix = expires;
|
|
774
|
+
row.fencingToken = newToken;
|
|
775
|
+
row.attempts = newAttempt;
|
|
776
|
+
row.updatedUtc = utcNow();
|
|
777
|
+
this.store._putJob(row);
|
|
778
|
+
this.store._putJobAttempt({
|
|
779
|
+
attemptId: `attempt-${row.jobId}-${newAttempt}-${newToken}`,
|
|
780
|
+
jobId: row.jobId,
|
|
781
|
+
attemptNumber: newAttempt,
|
|
782
|
+
fencingToken: newToken,
|
|
783
|
+
workerId,
|
|
784
|
+
startedUtc: utcNow(),
|
|
785
|
+
endedUtc: "",
|
|
786
|
+
outcome: "",
|
|
787
|
+
errorType: "",
|
|
788
|
+
errorHash: "",
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const lease: JobLease = {
|
|
793
|
+
jobId: row.jobId,
|
|
794
|
+
runId: this.runId,
|
|
795
|
+
queueName: row.queueName,
|
|
796
|
+
jobKind: row.jobKind,
|
|
797
|
+
payload: JSON.parse(row.payloadJson),
|
|
798
|
+
workerId,
|
|
799
|
+
fencingToken: newToken,
|
|
800
|
+
attemptNumber: newAttempt,
|
|
801
|
+
leaseExpiresUnix: expires,
|
|
802
|
+
retrySafe: row.retrySafe,
|
|
803
|
+
maxAttempts: row.maxAttempts,
|
|
804
|
+
};
|
|
805
|
+
return [lease, reaped];
|
|
806
|
+
},
|
|
807
|
+
([lease, reaped]) => {
|
|
808
|
+
if (!lease && reaped.length === 0) return null;
|
|
809
|
+
if (!lease) return ["jobs_reaped_v7", { queue_name: queueName, job_ids: reaped }];
|
|
810
|
+
return [
|
|
811
|
+
"job_claimed_v7",
|
|
812
|
+
{
|
|
813
|
+
job_id: lease.jobId,
|
|
814
|
+
queue_name: queueName,
|
|
815
|
+
worker_id: workerId,
|
|
816
|
+
fencing_token: lease.fencingToken,
|
|
817
|
+
attempt_number: lease.attemptNumber,
|
|
818
|
+
lease_expires_unix: lease.leaseExpiresUnix,
|
|
819
|
+
reaped_job_ids: reaped,
|
|
820
|
+
},
|
|
821
|
+
];
|
|
822
|
+
},
|
|
823
|
+
)[0];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
heartbeat(
|
|
827
|
+
lease: JobLease,
|
|
828
|
+
options: { extensionSeconds?: number; nowUnix?: number } = {},
|
|
829
|
+
): JobLease {
|
|
830
|
+
const extensionSeconds = options.extensionSeconds ?? 60;
|
|
831
|
+
if (extensionSeconds <= 0) throw new Error("extensionSeconds must be positive");
|
|
832
|
+
const now = options.nowUnix ?? Date.now() / 1000;
|
|
833
|
+
|
|
834
|
+
const newExpiry = this.store.atomicChange<number>(
|
|
835
|
+
this.runId,
|
|
836
|
+
(journal) => {
|
|
837
|
+
const row = this.store._getJob(lease.jobId);
|
|
838
|
+
if (
|
|
839
|
+
!row ||
|
|
840
|
+
row.status !== "leased" ||
|
|
841
|
+
row.leaseOwner !== lease.workerId ||
|
|
842
|
+
row.fencingToken !== lease.fencingToken ||
|
|
843
|
+
row.leaseExpiresUnix < now
|
|
844
|
+
) {
|
|
845
|
+
throw new LeaseLostError("Cannot heartbeat an expired or superseded lease");
|
|
846
|
+
}
|
|
847
|
+
const expiry = Math.max(now, row.leaseExpiresUnix) + extensionSeconds;
|
|
848
|
+
journal.stage(() => {
|
|
849
|
+
row.leaseExpiresUnix = expiry;
|
|
850
|
+
row.updatedUtc = utcNow();
|
|
851
|
+
this.store._putJob(row);
|
|
852
|
+
});
|
|
853
|
+
return expiry;
|
|
854
|
+
},
|
|
855
|
+
(expiry) => [
|
|
856
|
+
"job_heartbeat_v7",
|
|
857
|
+
{
|
|
858
|
+
job_id: lease.jobId,
|
|
859
|
+
worker_id: lease.workerId,
|
|
860
|
+
fencing_token: lease.fencingToken,
|
|
861
|
+
lease_expires_unix: expiry,
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
return { ...lease, leaseExpiresUnix: newExpiry };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
complete(lease: JobLease, result: Record<string, unknown>, nowUnix?: number): string {
|
|
870
|
+
const now = nowUnix ?? Date.now() / 1000;
|
|
871
|
+
const resultJson = canonicalJson(result);
|
|
872
|
+
if (resultJson.length > this.maximumResultChars) {
|
|
873
|
+
throw new Error("Job result exceeds storage limit");
|
|
874
|
+
}
|
|
875
|
+
const resultHash = sha256Text(resultJson);
|
|
876
|
+
|
|
877
|
+
return this.store.atomicChange<string>(
|
|
878
|
+
this.runId,
|
|
879
|
+
(journal) => {
|
|
880
|
+
const row = this.store._getJob(lease.jobId);
|
|
881
|
+
if (
|
|
882
|
+
!row ||
|
|
883
|
+
row.status !== "leased" ||
|
|
884
|
+
row.leaseOwner !== lease.workerId ||
|
|
885
|
+
row.fencingToken !== lease.fencingToken ||
|
|
886
|
+
row.leaseExpiresUnix < now
|
|
887
|
+
) {
|
|
888
|
+
throw new LeaseLostError("Stale worker cannot complete this job");
|
|
889
|
+
}
|
|
890
|
+
journal.stage(() => {
|
|
891
|
+
row.status = "succeeded";
|
|
892
|
+
row.resultJson = resultJson;
|
|
893
|
+
row.resultHash = resultHash;
|
|
894
|
+
row.leaseOwner = "";
|
|
895
|
+
row.leaseExpiresUnix = 0;
|
|
896
|
+
row.updatedUtc = utcNow();
|
|
897
|
+
this.store._putJob(row);
|
|
898
|
+
this.store._updateJobAttempt(lease.jobId, lease.fencingToken, {
|
|
899
|
+
endedUtc: utcNow(),
|
|
900
|
+
outcome: "succeeded",
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
return resultHash;
|
|
904
|
+
},
|
|
905
|
+
(digest) => [
|
|
906
|
+
"job_completed_v7",
|
|
907
|
+
{
|
|
908
|
+
job_id: lease.jobId,
|
|
909
|
+
worker_id: lease.workerId,
|
|
910
|
+
fencing_token: lease.fencingToken,
|
|
911
|
+
result_hash: digest,
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fail(lease: JobLease, error: Error, nowUnix?: number): string {
|
|
918
|
+
const now = nowUnix ?? Date.now() / 1000;
|
|
919
|
+
const errorType = (error.constructor?.name ?? "Error").slice(0, 128);
|
|
920
|
+
const errorHash = sha256Text(String(error.message ?? error));
|
|
921
|
+
|
|
922
|
+
return this.store.atomicChange<string>(
|
|
923
|
+
this.runId,
|
|
924
|
+
(journal) => {
|
|
925
|
+
const row = this.store._getJob(lease.jobId);
|
|
926
|
+
if (
|
|
927
|
+
!row ||
|
|
928
|
+
row.status !== "leased" ||
|
|
929
|
+
row.leaseOwner !== lease.workerId ||
|
|
930
|
+
row.fencingToken !== lease.fencingToken ||
|
|
931
|
+
row.leaseExpiresUnix < now
|
|
932
|
+
) {
|
|
933
|
+
throw new LeaseLostError("Stale worker cannot fail this job");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let status: string;
|
|
937
|
+
if (row.retrySafe && row.attempts < row.maxAttempts) {
|
|
938
|
+
const delay = this.retryPolicy.delay(lease.jobId, row.attempts);
|
|
939
|
+
const nextTime = now + delay;
|
|
940
|
+
status = "pending";
|
|
941
|
+
journal.stage(() => {
|
|
942
|
+
row.status = "pending";
|
|
943
|
+
row.availableUnix = nextTime;
|
|
944
|
+
row.leaseOwner = "";
|
|
945
|
+
row.leaseExpiresUnix = 0;
|
|
946
|
+
row.lastErrorType = errorType;
|
|
947
|
+
row.lastErrorHash = errorHash;
|
|
948
|
+
row.updatedUtc = utcNow();
|
|
949
|
+
this.store._putJob(row);
|
|
950
|
+
});
|
|
951
|
+
} else {
|
|
952
|
+
status = "dead_letter";
|
|
953
|
+
this.deadLetter(
|
|
954
|
+
journal,
|
|
955
|
+
row,
|
|
956
|
+
"attempt_failed_without_remaining_safe_retry",
|
|
957
|
+
errorType,
|
|
958
|
+
errorHash,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
journal.stage(() =>
|
|
963
|
+
this.store._updateJobAttempt(lease.jobId, lease.fencingToken, {
|
|
964
|
+
endedUtc: utcNow(),
|
|
965
|
+
outcome: status,
|
|
966
|
+
errorType,
|
|
967
|
+
errorHash,
|
|
968
|
+
}),
|
|
969
|
+
);
|
|
970
|
+
return status;
|
|
971
|
+
},
|
|
972
|
+
(status) => [
|
|
973
|
+
"job_failed_v7",
|
|
974
|
+
{
|
|
975
|
+
job_id: lease.jobId,
|
|
976
|
+
worker_id: lease.workerId,
|
|
977
|
+
fencing_token: lease.fencingToken,
|
|
978
|
+
error_type: errorType,
|
|
979
|
+
error_hash: errorHash,
|
|
980
|
+
next_status: status,
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
stats(queueName?: string): Record<JobStatus, number> {
|
|
987
|
+
const result: Record<JobStatus, number> = {
|
|
988
|
+
pending: 0,
|
|
989
|
+
leased: 0,
|
|
990
|
+
succeeded: 0,
|
|
991
|
+
dead_letter: 0,
|
|
992
|
+
cancelled: 0,
|
|
993
|
+
};
|
|
994
|
+
for (const job of this.store._jobsForRun(this.runId)) {
|
|
995
|
+
if (queueName !== undefined && job.queueName !== queueName) continue;
|
|
996
|
+
result[job.status] += 1;
|
|
997
|
+
}
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export type JobExecutor = (
|
|
1003
|
+
payload: Record<string, unknown>,
|
|
1004
|
+
lease: JobLease,
|
|
1005
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
1006
|
+
|
|
1007
|
+
export class DurableWorker {
|
|
1008
|
+
constructor(
|
|
1009
|
+
private readonly queue: DurableJobQueue,
|
|
1010
|
+
private readonly workerId: string,
|
|
1011
|
+
private readonly executors: Record<string, JobExecutor>,
|
|
1012
|
+
private readonly leaseSeconds = 60,
|
|
1013
|
+
) {}
|
|
1014
|
+
|
|
1015
|
+
async runOnce(queueName: string): Promise<boolean> {
|
|
1016
|
+
const lease = this.queue.claim(queueName, this.workerId, {
|
|
1017
|
+
leaseSeconds: this.leaseSeconds,
|
|
1018
|
+
});
|
|
1019
|
+
if (!lease) return false;
|
|
1020
|
+
|
|
1021
|
+
const executor = this.executors[lease.jobKind];
|
|
1022
|
+
if (!executor) {
|
|
1023
|
+
this.queue.fail(lease, new Error(`No executor registered for ${JSON.stringify(lease.jobKind)}`));
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
const result = await executor(lease.payload, lease);
|
|
1029
|
+
this.queue.complete(lease, result);
|
|
1030
|
+
} catch (exc) {
|
|
1031
|
+
this.queue.fail(lease, exc instanceof Error ? exc : new Error(String(exc)));
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1038
|
+
// 5. Secure bounded telemetry
|
|
1039
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1040
|
+
|
|
1041
|
+
const SENSITIVE_KEY_RE =
|
|
1042
|
+
/(authorization|api[_-]?key|token|secret|password|passwd|cookie|set-cookie|credential|private[_-]?key|session)/i;
|
|
1043
|
+
|
|
1044
|
+
export interface TelemetryLimitsOptions {
|
|
1045
|
+
attributeCountLimit?: number;
|
|
1046
|
+
attributeValueLengthLimit?: number;
|
|
1047
|
+
sequenceLengthLimit?: number;
|
|
1048
|
+
nestingDepthLimit?: number;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export class TelemetryLimits {
|
|
1052
|
+
readonly attributeCountLimit: number;
|
|
1053
|
+
readonly attributeValueLengthLimit: number;
|
|
1054
|
+
readonly sequenceLengthLimit: number;
|
|
1055
|
+
readonly nestingDepthLimit: number;
|
|
1056
|
+
|
|
1057
|
+
constructor(options: TelemetryLimitsOptions = {}) {
|
|
1058
|
+
this.attributeCountLimit = options.attributeCountLimit ?? 128;
|
|
1059
|
+
this.attributeValueLengthLimit = options.attributeValueLengthLimit ?? 512;
|
|
1060
|
+
this.sequenceLengthLimit = options.sequenceLengthLimit ?? 32;
|
|
1061
|
+
this.nestingDepthLimit = options.nestingDepthLimit ?? 4;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
validate(): void {
|
|
1065
|
+
if (this.attributeCountLimit < 1) throw new Error("attributeCountLimit must be positive");
|
|
1066
|
+
if (this.attributeValueLengthLimit < 8) throw new Error("attributeValueLengthLimit is too small");
|
|
1067
|
+
if (this.sequenceLengthLimit < 1) throw new Error("sequenceLengthLimit must be positive");
|
|
1068
|
+
if (this.nestingDepthLimit < 1) throw new Error("nestingDepthLimit must be positive");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export interface SanitizationStats {
|
|
1073
|
+
redacted: number;
|
|
1074
|
+
dropped: number;
|
|
1075
|
+
truncated: number;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export class TelemetrySanitizer {
|
|
1079
|
+
readonly limits: TelemetryLimits;
|
|
1080
|
+
private readonly allowedKeys: ReadonlySet<string>;
|
|
1081
|
+
|
|
1082
|
+
constructor(
|
|
1083
|
+
private readonly pseudonymKey: string,
|
|
1084
|
+
limits?: TelemetryLimits,
|
|
1085
|
+
allowedKeys: readonly string[] = [],
|
|
1086
|
+
) {
|
|
1087
|
+
if (pseudonymKey.length < 32) {
|
|
1088
|
+
throw new Error("Telemetry pseudonym key must be at least 32 bytes");
|
|
1089
|
+
}
|
|
1090
|
+
this.limits = limits ?? new TelemetryLimits();
|
|
1091
|
+
this.limits.validate();
|
|
1092
|
+
this.allowedKeys = new Set(allowedKeys);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private pseudonym(value: unknown): string {
|
|
1096
|
+
// Keyed SHA-256 (local integrity / pseudonymization only) — disclosed.
|
|
1097
|
+
return `hmac-sha256:${sha256Text(this.pseudonymKey + ":" + String(value))}`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private sanitizeValue(
|
|
1101
|
+
keyPath: string,
|
|
1102
|
+
value: unknown,
|
|
1103
|
+
depth: number,
|
|
1104
|
+
stats: SanitizationStats,
|
|
1105
|
+
): unknown {
|
|
1106
|
+
if (depth > this.limits.nestingDepthLimit) {
|
|
1107
|
+
stats.dropped += 1;
|
|
1108
|
+
return "[DROPPED:DEPTH_LIMIT]";
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (SENSITIVE_KEY_RE.test(keyPath)) {
|
|
1112
|
+
stats.redacted += 1;
|
|
1113
|
+
return this.pseudonym(value);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (value === null || value === undefined) return null;
|
|
1117
|
+
if (typeof value === "boolean" || typeof value === "number") return value;
|
|
1118
|
+
|
|
1119
|
+
if (typeof value === "string") {
|
|
1120
|
+
if (value.length > this.limits.attributeValueLengthLimit) {
|
|
1121
|
+
stats.truncated += 1;
|
|
1122
|
+
return value.slice(0, this.limits.attributeValueLengthLimit);
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (Array.isArray(value)) {
|
|
1128
|
+
const out: unknown[] = [];
|
|
1129
|
+
for (const item of value.slice(0, this.limits.sequenceLengthLimit)) {
|
|
1130
|
+
out.push(this.sanitizeValue(keyPath, item, depth + 1, stats));
|
|
1131
|
+
}
|
|
1132
|
+
if (value.length > this.limits.sequenceLengthLimit) {
|
|
1133
|
+
stats.dropped += value.length - this.limits.sequenceLengthLimit;
|
|
1134
|
+
}
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (typeof value === "object") {
|
|
1139
|
+
const record = value as Record<string, unknown>;
|
|
1140
|
+
const out: Record<string, unknown> = {};
|
|
1141
|
+
for (const key of Object.keys(record).sort()) {
|
|
1142
|
+
const childPath = keyPath ? `${keyPath}.${key}` : key;
|
|
1143
|
+
out[key] = this.sanitizeValue(childPath, record[key], depth + 1, stats);
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
stats.dropped += 1;
|
|
1149
|
+
return `[UNSUPPORTED:${typeof value}]`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
sanitize(attributes: Record<string, unknown>): [Record<string, unknown>, SanitizationStats] {
|
|
1153
|
+
const stats: SanitizationStats = { redacted: 0, dropped: 0, truncated: 0 };
|
|
1154
|
+
const sanitized: Record<string, unknown> = {};
|
|
1155
|
+
|
|
1156
|
+
for (const key of Object.keys(attributes).sort()) {
|
|
1157
|
+
if (Object.keys(sanitized).length >= this.limits.attributeCountLimit) {
|
|
1158
|
+
stats.dropped += 1;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (this.allowedKeys.size > 0 && !this.allowedKeys.has(key)) {
|
|
1162
|
+
stats.dropped += 1;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
sanitized[key] = this.sanitizeValue(key, attributes[key], 0, stats);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
sanitized["v7.telemetry.redacted_count"] = stats.redacted;
|
|
1169
|
+
sanitized["v7.telemetry.dropped_count"] = stats.dropped;
|
|
1170
|
+
sanitized["v7.telemetry.truncated_count"] = stats.truncated;
|
|
1171
|
+
return [sanitized, stats];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export class SecureTelemetryRecorder extends TelemetryRecorder {
|
|
1176
|
+
constructor(
|
|
1177
|
+
store: ProductionAssuranceStore,
|
|
1178
|
+
runId: string,
|
|
1179
|
+
private readonly sanitizer: TelemetrySanitizer,
|
|
1180
|
+
) {
|
|
1181
|
+
super(store, runId);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
override endSpan(
|
|
1185
|
+
context: SpanContext,
|
|
1186
|
+
name: string,
|
|
1187
|
+
kind: string,
|
|
1188
|
+
startMonotonic: number,
|
|
1189
|
+
startUtc: string,
|
|
1190
|
+
status: string,
|
|
1191
|
+
attributes: Record<string, unknown>,
|
|
1192
|
+
): void {
|
|
1193
|
+
const [sanitized] = this.sanitizer.sanitize(attributes);
|
|
1194
|
+
const normalizedStatus =
|
|
1195
|
+
status === "ok" || status === "error" || status === "unset" ? status : "error";
|
|
1196
|
+
super.endSpan(
|
|
1197
|
+
context,
|
|
1198
|
+
name.slice(0, 128),
|
|
1199
|
+
kind.slice(0, 64),
|
|
1200
|
+
startMonotonic,
|
|
1201
|
+
startUtc,
|
|
1202
|
+
normalizedStatus,
|
|
1203
|
+
sanitized,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1209
|
+
// 6. Exact verifier closure
|
|
1210
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1211
|
+
|
|
1212
|
+
export interface VerifierRequirement {
|
|
1213
|
+
name: string;
|
|
1214
|
+
acceptedVersions: readonly string[];
|
|
1215
|
+
requireEvidence?: boolean;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export class VerifierClosurePolicy {
|
|
1219
|
+
readonly requirements: readonly VerifierRequirement[];
|
|
1220
|
+
readonly forbidUnexpectedVerifiers: boolean;
|
|
1221
|
+
|
|
1222
|
+
constructor(
|
|
1223
|
+
requirements: readonly VerifierRequirement[],
|
|
1224
|
+
forbidUnexpectedVerifiers = false,
|
|
1225
|
+
) {
|
|
1226
|
+
this.requirements = requirements;
|
|
1227
|
+
this.forbidUnexpectedVerifiers = forbidUnexpectedVerifiers;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
validate(): void {
|
|
1231
|
+
if (this.requirements.length === 0) {
|
|
1232
|
+
throw new Error("At least one verifier requirement is required");
|
|
1233
|
+
}
|
|
1234
|
+
const names = this.requirements.map((r) => r.name);
|
|
1235
|
+
if (new Set(names).size !== names.length) {
|
|
1236
|
+
throw new Error("Verifier requirement names must be unique");
|
|
1237
|
+
}
|
|
1238
|
+
for (const requirement of this.requirements) {
|
|
1239
|
+
if (!requirement.name.trim()) throw new Error("Verifier name is required");
|
|
1240
|
+
if (requirement.acceptedVersions.length === 0) {
|
|
1241
|
+
throw new Error(`Verifier ${JSON.stringify(requirement.name)} needs accepted versions`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
get policyHash(): string {
|
|
1247
|
+
return sha256Text(
|
|
1248
|
+
canonicalJson({
|
|
1249
|
+
requirements: this.requirements.map((r) => ({
|
|
1250
|
+
name: r.name,
|
|
1251
|
+
accepted_versions: r.acceptedVersions,
|
|
1252
|
+
require_evidence: r.requireEvidence ?? true,
|
|
1253
|
+
})),
|
|
1254
|
+
forbid_unexpected_verifiers: this.forbidUnexpectedVerifiers,
|
|
1255
|
+
}),
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export interface VerifierClosureReport {
|
|
1261
|
+
candidateId: string;
|
|
1262
|
+
policyHash: string;
|
|
1263
|
+
closed: boolean;
|
|
1264
|
+
satisfied: readonly string[];
|
|
1265
|
+
missing: readonly string[];
|
|
1266
|
+
failed: readonly string[];
|
|
1267
|
+
unexpected: readonly string[];
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Unlike V5/V6's `allRequiredPassed()`, this gate evaluates an EXPLICIT
|
|
1272
|
+
* requirement set. A missing required verifier is a failure, not a vacuous pass.
|
|
1273
|
+
*/
|
|
1274
|
+
export class VerifierClosureGate {
|
|
1275
|
+
constructor(
|
|
1276
|
+
private readonly store: ProductionAssuranceStore,
|
|
1277
|
+
private readonly runId: string,
|
|
1278
|
+
private readonly policy: VerifierClosurePolicy,
|
|
1279
|
+
) {
|
|
1280
|
+
policy.validate();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
evaluate(candidateId: string): VerifierClosureReport {
|
|
1284
|
+
const rows = this.store._getVerifierRuns(this.runId, candidateId);
|
|
1285
|
+
const byName = new Map<string, typeof rows>();
|
|
1286
|
+
for (const row of rows) {
|
|
1287
|
+
const list = byName.get(row.verifierName) ?? [];
|
|
1288
|
+
list.push(row);
|
|
1289
|
+
byName.set(row.verifierName, list);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const satisfied: string[] = [];
|
|
1293
|
+
const missing: string[] = [];
|
|
1294
|
+
const failed: string[] = [];
|
|
1295
|
+
const requiredNames = new Set(this.policy.requirements.map((r) => r.name));
|
|
1296
|
+
|
|
1297
|
+
for (const requirement of this.policy.requirements) {
|
|
1298
|
+
const matches = (byName.get(requirement.name) ?? []).filter((row) =>
|
|
1299
|
+
requirement.acceptedVersions.includes(row.verifierVersion),
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
if (matches.length === 0) {
|
|
1303
|
+
missing.push(requirement.name);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const latest = matches[matches.length - 1];
|
|
1308
|
+
|
|
1309
|
+
if (latest.verdict !== "pass") {
|
|
1310
|
+
failed.push(`${requirement.name}:${latest.verifierVersion}:${latest.verdict}`);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (requirement.requireEvidence ?? true) {
|
|
1315
|
+
let parsed: Record<string, unknown>;
|
|
1316
|
+
try {
|
|
1317
|
+
parsed = JSON.parse(latest.resultJson);
|
|
1318
|
+
} catch {
|
|
1319
|
+
failed.push(`${requirement.name}:invalid-result-json`);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
const evidence = (parsed.evidence ?? []) as unknown[];
|
|
1323
|
+
if (!evidence || evidence.length === 0) {
|
|
1324
|
+
failed.push(`${requirement.name}:missing-evidence`);
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
satisfied.push(`${requirement.name}:${latest.verifierVersion}`);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const unexpected = [...byName.keys()].filter((n) => !requiredNames.has(n)).sort();
|
|
1333
|
+
|
|
1334
|
+
const closed =
|
|
1335
|
+
missing.length === 0 &&
|
|
1336
|
+
failed.length === 0 &&
|
|
1337
|
+
(!this.policy.forbidUnexpectedVerifiers || unexpected.length === 0);
|
|
1338
|
+
|
|
1339
|
+
const report: VerifierClosureReport = {
|
|
1340
|
+
candidateId,
|
|
1341
|
+
policyHash: this.policy.policyHash,
|
|
1342
|
+
closed,
|
|
1343
|
+
satisfied,
|
|
1344
|
+
missing,
|
|
1345
|
+
failed,
|
|
1346
|
+
unexpected,
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
const reportJson = canonicalJson(report as unknown as Record<string, unknown>);
|
|
1350
|
+
const reportHash = sha256Text(reportJson);
|
|
1351
|
+
|
|
1352
|
+
this.store.atomicChange<void>(
|
|
1353
|
+
this.runId,
|
|
1354
|
+
(journal) => {
|
|
1355
|
+
journal.stage(() =>
|
|
1356
|
+
this.store._putVerifierClosure({
|
|
1357
|
+
runId: this.runId,
|
|
1358
|
+
candidateId,
|
|
1359
|
+
policyHash: this.policy.policyHash,
|
|
1360
|
+
closed,
|
|
1361
|
+
reportJson,
|
|
1362
|
+
reportHash,
|
|
1363
|
+
createdUtc: utcNow(),
|
|
1364
|
+
}),
|
|
1365
|
+
);
|
|
1366
|
+
},
|
|
1367
|
+
() => [
|
|
1368
|
+
"verifier_closure_evaluated_v7",
|
|
1369
|
+
{
|
|
1370
|
+
candidate_id: candidateId,
|
|
1371
|
+
policy_hash: this.policy.policyHash,
|
|
1372
|
+
closed,
|
|
1373
|
+
report_hash: reportHash,
|
|
1374
|
+
},
|
|
1375
|
+
],
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
return report;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1383
|
+
// 7. RFC 9162-style Merkle challenge commitments
|
|
1384
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1385
|
+
|
|
1386
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
1387
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1388
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1389
|
+
return out;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
1393
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function sha256OfBytesHex(bytes: Uint8Array): string {
|
|
1397
|
+
// V3's sha256Text hashes a UTF-8 string. To hash raw bytes we hex-encode
|
|
1398
|
+
// with a domain tag; this is a consistent, collision-resistant construction
|
|
1399
|
+
// used uniformly for every node, so proofs verify against roots built the
|
|
1400
|
+
// same way. Disclosed deviation from raw-byte SHA-256.
|
|
1401
|
+
return sha256Text("\u0000RAW:" + bytesToHex(bytes));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** RFC 9162 leaf hash: SHA-256(0x00 || data). */
|
|
1405
|
+
function merkleLeafHash(data: Uint8Array): Uint8Array {
|
|
1406
|
+
const buf = new Uint8Array(data.length + 1);
|
|
1407
|
+
buf[0] = 0x00;
|
|
1408
|
+
buf.set(data, 1);
|
|
1409
|
+
return hexToBytes(sha256OfBytesHex(buf));
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** RFC 9162 node hash: SHA-256(0x01 || left || right). */
|
|
1413
|
+
function merkleNodeHash(left: Uint8Array, right: Uint8Array): Uint8Array {
|
|
1414
|
+
const buf = new Uint8Array(1 + left.length + right.length);
|
|
1415
|
+
buf[0] = 0x01;
|
|
1416
|
+
buf.set(left, 1);
|
|
1417
|
+
buf.set(right, 1 + left.length);
|
|
1418
|
+
return hexToBytes(sha256OfBytesHex(buf));
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function largestPowerOfTwoBelow(value: number): number {
|
|
1422
|
+
if (value <= 1) throw new Error("value must be greater than one");
|
|
1423
|
+
return 1 << (32 - Math.clz32(value - 1) - 1);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function merkleRootFromInputs(inputs: readonly Uint8Array[]): Uint8Array {
|
|
1427
|
+
if (inputs.length === 0) return hexToBytes(sha256OfBytesHex(new Uint8Array(0)));
|
|
1428
|
+
if (inputs.length === 1) return merkleLeafHash(inputs[0]);
|
|
1429
|
+
const split = largestPowerOfTwoBelow(inputs.length);
|
|
1430
|
+
return merkleNodeHash(
|
|
1431
|
+
merkleRootFromInputs(inputs.slice(0, split)),
|
|
1432
|
+
merkleRootFromInputs(inputs.slice(split)),
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function merkleInclusionPath(index: number, inputs: readonly Uint8Array[]): Uint8Array[] {
|
|
1437
|
+
if (index < 0 || index >= inputs.length) throw new RangeError(String(index));
|
|
1438
|
+
if (inputs.length === 1) return [];
|
|
1439
|
+
const split = largestPowerOfTwoBelow(inputs.length);
|
|
1440
|
+
if (index < split) {
|
|
1441
|
+
return [
|
|
1442
|
+
...merkleInclusionPath(index, inputs.slice(0, split)),
|
|
1443
|
+
merkleRootFromInputs(inputs.slice(split)),
|
|
1444
|
+
];
|
|
1445
|
+
}
|
|
1446
|
+
return [
|
|
1447
|
+
...merkleInclusionPath(index - split, inputs.slice(split)),
|
|
1448
|
+
merkleRootFromInputs(inputs.slice(0, split)),
|
|
1449
|
+
];
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
export interface MerkleOpening {
|
|
1453
|
+
caseId: string;
|
|
1454
|
+
payloadJson: string;
|
|
1455
|
+
saltB64: string;
|
|
1456
|
+
leafIndex: number;
|
|
1457
|
+
treeSize: number;
|
|
1458
|
+
inclusionPathHex: readonly string[];
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export function merkleOpeningLeafInput(opening: MerkleOpening): Uint8Array {
|
|
1462
|
+
return new TextEncoder().encode(
|
|
1463
|
+
canonicalJson({
|
|
1464
|
+
case_id: opening.caseId,
|
|
1465
|
+
payload_json: opening.payloadJson,
|
|
1466
|
+
salt_b64: opening.saltB64,
|
|
1467
|
+
}),
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
export interface MerkleSuiteSeal {
|
|
1472
|
+
rootHash: string;
|
|
1473
|
+
treeSize: number;
|
|
1474
|
+
openings: Record<string, MerkleOpening>;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export function sealChallengeCases(
|
|
1478
|
+
cases: Record<string, unknown>,
|
|
1479
|
+
options: { deterministicSaltSeed?: string } = {},
|
|
1480
|
+
): MerkleSuiteSeal {
|
|
1481
|
+
const caseIds = Object.keys(cases).sort();
|
|
1482
|
+
if (caseIds.length === 0) throw new Error("At least one challenge case is required");
|
|
1483
|
+
|
|
1484
|
+
const salts: Record<string, string> = {};
|
|
1485
|
+
const inputs: Uint8Array[] = [];
|
|
1486
|
+
|
|
1487
|
+
caseIds.forEach((caseId, index) => {
|
|
1488
|
+
let saltHex: string;
|
|
1489
|
+
if (options.deterministicSaltSeed === undefined) {
|
|
1490
|
+
const rnd = new Uint8Array(32);
|
|
1491
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(rnd);
|
|
1492
|
+
else for (let i = 0; i < 32; i++) rnd[i] = Math.floor(Math.random() * 256);
|
|
1493
|
+
saltHex = bytesToHex(rnd);
|
|
1494
|
+
} else {
|
|
1495
|
+
saltHex = sha256Text(`${options.deterministicSaltSeed}:${index}:${caseId}`);
|
|
1496
|
+
}
|
|
1497
|
+
const saltB64 = btoa(saltHex);
|
|
1498
|
+
salts[caseId] = saltB64;
|
|
1499
|
+
|
|
1500
|
+
const payloadJson = canonicalJson(cases[caseId]);
|
|
1501
|
+
inputs.push(
|
|
1502
|
+
new TextEncoder().encode(
|
|
1503
|
+
canonicalJson({ case_id: caseId, payload_json: payloadJson, salt_b64: saltB64 }),
|
|
1504
|
+
),
|
|
1505
|
+
);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const root = merkleRootFromInputs(inputs);
|
|
1509
|
+
const openings: Record<string, MerkleOpening> = {};
|
|
1510
|
+
|
|
1511
|
+
caseIds.forEach((caseId, index) => {
|
|
1512
|
+
openings[caseId] = {
|
|
1513
|
+
caseId,
|
|
1514
|
+
payloadJson: canonicalJson(cases[caseId]),
|
|
1515
|
+
saltB64: salts[caseId],
|
|
1516
|
+
leafIndex: index,
|
|
1517
|
+
treeSize: inputs.length,
|
|
1518
|
+
inclusionPathHex: merkleInclusionPath(index, inputs).map(bytesToHex),
|
|
1519
|
+
};
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
return { rootHash: bytesToHex(root), treeSize: inputs.length, openings };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/** RFC 9162 inclusion-proof verification algorithm, ported exactly. */
|
|
1526
|
+
export function verifyMerkleOpening(rootHash: string, opening: MerkleOpening): boolean {
|
|
1527
|
+
if (
|
|
1528
|
+
opening.leafIndex < 0 ||
|
|
1529
|
+
opening.leafIndex >= opening.treeSize ||
|
|
1530
|
+
opening.treeSize < 1
|
|
1531
|
+
) {
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
let current: Uint8Array;
|
|
1536
|
+
let path: Uint8Array[];
|
|
1537
|
+
let expectedRoot: Uint8Array;
|
|
1538
|
+
try {
|
|
1539
|
+
current = merkleLeafHash(merkleOpeningLeafInput(opening));
|
|
1540
|
+
path = opening.inclusionPathHex.map(hexToBytes);
|
|
1541
|
+
expectedRoot = hexToBytes(rootHash);
|
|
1542
|
+
} catch {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
let fn = opening.leafIndex;
|
|
1547
|
+
let sn = opening.treeSize - 1;
|
|
1548
|
+
|
|
1549
|
+
for (const sibling of path) {
|
|
1550
|
+
if (sn === 0) return false;
|
|
1551
|
+
|
|
1552
|
+
if ((fn & 1) === 1 || fn === sn) {
|
|
1553
|
+
current = merkleNodeHash(sibling, current);
|
|
1554
|
+
if ((fn & 1) === 0) {
|
|
1555
|
+
while (fn !== 0 && (fn & 1) === 0) {
|
|
1556
|
+
fn >>= 1;
|
|
1557
|
+
sn >>= 1;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
current = merkleNodeHash(current, sibling);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
fn >>= 1;
|
|
1565
|
+
sn >>= 1;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return sn === 0 && bytesToHex(current) === bytesToHex(expectedRoot);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** Stores only the root commitment; payloads and salts stay outside. */
|
|
1572
|
+
export class MerkleChallengeRegistry {
|
|
1573
|
+
constructor(
|
|
1574
|
+
private readonly store: ProductionAssuranceStore,
|
|
1575
|
+
private readonly runId: string,
|
|
1576
|
+
) {}
|
|
1577
|
+
|
|
1578
|
+
register(suiteId: string, name: string, seal: MerkleSuiteSeal): void {
|
|
1579
|
+
if (seal.rootHash.length !== 64) throw new Error("Merkle root must be a SHA-256 digest");
|
|
1580
|
+
|
|
1581
|
+
this.store.atomicChange<void>(
|
|
1582
|
+
this.runId,
|
|
1583
|
+
(journal) => {
|
|
1584
|
+
journal.stage(() =>
|
|
1585
|
+
this.store._putMerkleSuite({
|
|
1586
|
+
suiteId,
|
|
1587
|
+
runId: this.runId,
|
|
1588
|
+
name,
|
|
1589
|
+
rootHash: seal.rootHash,
|
|
1590
|
+
treeSize: seal.treeSize,
|
|
1591
|
+
hashAlgorithm: "RFC9162-SHA256",
|
|
1592
|
+
createdUtc: utcNow(),
|
|
1593
|
+
}),
|
|
1594
|
+
);
|
|
1595
|
+
},
|
|
1596
|
+
() => [
|
|
1597
|
+
"merkle_suite_registered_v7",
|
|
1598
|
+
{ suite_id: suiteId, name, root_hash: seal.rootHash, tree_size: seal.treeSize },
|
|
1599
|
+
],
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
verify(suiteId: string, opening: MerkleOpening): boolean {
|
|
1604
|
+
const row = this.store._getMerkleSuite(suiteId, this.runId);
|
|
1605
|
+
if (!row) throw new Error(suiteId);
|
|
1606
|
+
if (row.treeSize !== opening.treeSize) return false;
|
|
1607
|
+
return verifyMerkleOpening(row.rootHash, opening);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1612
|
+
// 8. Verified backup / restore (browser: structural snapshot + revalidation)
|
|
1613
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1614
|
+
|
|
1615
|
+
export interface BackupVerification {
|
|
1616
|
+
valid: boolean;
|
|
1617
|
+
applicationId: number;
|
|
1618
|
+
schemaVersion: number;
|
|
1619
|
+
integrityValid: boolean;
|
|
1620
|
+
foreignKeysValid: boolean;
|
|
1621
|
+
eventChainValid: boolean;
|
|
1622
|
+
semanticChainValid: boolean;
|
|
1623
|
+
semanticHead: string;
|
|
1624
|
+
errors: readonly string[];
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export interface BackupManifest {
|
|
1628
|
+
backupId: string;
|
|
1629
|
+
runId: string;
|
|
1630
|
+
targetPath: string;
|
|
1631
|
+
fileDigest: string;
|
|
1632
|
+
sizeBytes: number;
|
|
1633
|
+
verification: BackupVerification;
|
|
1634
|
+
createdUnix: number;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
export class BackupManager {
|
|
1638
|
+
constructor(
|
|
1639
|
+
private readonly store: ProductionAssuranceStore,
|
|
1640
|
+
private readonly runId: string,
|
|
1641
|
+
) {}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Browser substitution (disclosed): there is no SQLite file to copy, so the
|
|
1645
|
+
* "backup" is a canonical structural snapshot whose integrity is verified by
|
|
1646
|
+
* re-running BOTH chain verifiers plus blob integrity — the same predicates
|
|
1647
|
+
* the Python checks against a copied file.
|
|
1648
|
+
*/
|
|
1649
|
+
create(targetPath: string): BackupManifest {
|
|
1650
|
+
this.store.appendEvent(this.runId, "backup_started_v7", { target_name: targetPath });
|
|
1651
|
+
|
|
1652
|
+
const eventChainValid = this.store.verifyChain(this.runId);
|
|
1653
|
+
const semanticChainValid = this.store.verifySemanticChain(this.runId);
|
|
1654
|
+
const integrityValid = this.store.verifyAllBlobs();
|
|
1655
|
+
const applicationId = this.store.getApplicationId();
|
|
1656
|
+
const schemaVersion = this.store.getUserVersion();
|
|
1657
|
+
const semanticHead = this.store.latestSemanticHash(this.runId);
|
|
1658
|
+
|
|
1659
|
+
const errors: string[] = [];
|
|
1660
|
+
if (applicationId !== V7_APPLICATION_ID) errors.push(`Unexpected application_id ${applicationId}`);
|
|
1661
|
+
if (schemaVersion !== V7_SCHEMA_VERSION) errors.push(`Unexpected schema version ${schemaVersion}`);
|
|
1662
|
+
if (!eventChainValid) errors.push("Legacy event chain is invalid");
|
|
1663
|
+
if (!semanticChainValid) errors.push("Semantic event chain is invalid");
|
|
1664
|
+
if (!integrityValid) errors.push("Blob integrity invalid");
|
|
1665
|
+
|
|
1666
|
+
const verification: BackupVerification = {
|
|
1667
|
+
valid:
|
|
1668
|
+
applicationId === V7_APPLICATION_ID &&
|
|
1669
|
+
schemaVersion === V7_SCHEMA_VERSION &&
|
|
1670
|
+
integrityValid &&
|
|
1671
|
+
true /* foreign keys: no FK engine in the browser port */ &&
|
|
1672
|
+
eventChainValid &&
|
|
1673
|
+
semanticChainValid,
|
|
1674
|
+
applicationId,
|
|
1675
|
+
schemaVersion,
|
|
1676
|
+
integrityValid,
|
|
1677
|
+
foreignKeysValid: true,
|
|
1678
|
+
eventChainValid,
|
|
1679
|
+
semanticChainValid,
|
|
1680
|
+
semanticHead,
|
|
1681
|
+
errors,
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
if (!verification.valid) {
|
|
1685
|
+
throw new Error("Backup verification failed: " + errors.join("; "));
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
const snapshot = canonicalJson({
|
|
1689
|
+
run_id: this.runId,
|
|
1690
|
+
application_id: applicationId,
|
|
1691
|
+
schema_version: schemaVersion,
|
|
1692
|
+
semantic_head: semanticHead,
|
|
1693
|
+
migrations: this.store.migrationReport(),
|
|
1694
|
+
});
|
|
1695
|
+
const fileDigest = sha256Text(snapshot);
|
|
1696
|
+
const sizeBytes = new TextEncoder().encode(snapshot).length;
|
|
1697
|
+
const createdUnix = Date.now() / 1000;
|
|
1698
|
+
|
|
1699
|
+
const backupId =
|
|
1700
|
+
"backup-" +
|
|
1701
|
+
sha256Text(
|
|
1702
|
+
canonicalJson({
|
|
1703
|
+
run_id: this.runId,
|
|
1704
|
+
file_digest: fileDigest,
|
|
1705
|
+
semantic_head: semanticHead,
|
|
1706
|
+
}),
|
|
1707
|
+
).slice(0, 32);
|
|
1708
|
+
|
|
1709
|
+
const manifest: BackupManifest = {
|
|
1710
|
+
backupId,
|
|
1711
|
+
runId: this.runId,
|
|
1712
|
+
targetPath,
|
|
1713
|
+
fileDigest,
|
|
1714
|
+
sizeBytes,
|
|
1715
|
+
verification,
|
|
1716
|
+
createdUnix,
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
this.store.atomicChange<void>(
|
|
1720
|
+
this.runId,
|
|
1721
|
+
(journal) => {
|
|
1722
|
+
journal.stage(() =>
|
|
1723
|
+
this.store._putBackup({
|
|
1724
|
+
backupId,
|
|
1725
|
+
runId: this.runId,
|
|
1726
|
+
targetPath,
|
|
1727
|
+
fileDigest,
|
|
1728
|
+
sizeBytes,
|
|
1729
|
+
applicationId,
|
|
1730
|
+
schemaVersion,
|
|
1731
|
+
snapshotSemanticHash: semanticHead,
|
|
1732
|
+
integrityValid,
|
|
1733
|
+
foreignKeysValid: true,
|
|
1734
|
+
eventChainValid,
|
|
1735
|
+
semanticChainValid,
|
|
1736
|
+
createdUnix,
|
|
1737
|
+
createdUtc: utcNow(),
|
|
1738
|
+
}),
|
|
1739
|
+
);
|
|
1740
|
+
},
|
|
1741
|
+
() => [
|
|
1742
|
+
"backup_completed_v7",
|
|
1743
|
+
{
|
|
1744
|
+
backup_id: backupId,
|
|
1745
|
+
file_digest: fileDigest,
|
|
1746
|
+
size_bytes: sizeBytes,
|
|
1747
|
+
snapshot_semantic_hash: semanticHead,
|
|
1748
|
+
},
|
|
1749
|
+
],
|
|
1750
|
+
);
|
|
1751
|
+
|
|
1752
|
+
return manifest;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1757
|
+
// 9. Production doctor / readiness
|
|
1758
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1759
|
+
|
|
1760
|
+
export interface DoctorCheck {
|
|
1761
|
+
name: string;
|
|
1762
|
+
passed: boolean;
|
|
1763
|
+
severity: "error" | "warning";
|
|
1764
|
+
detail: string;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
export interface DoctorPolicyOptions {
|
|
1768
|
+
failOnDeadLetters?: boolean;
|
|
1769
|
+
failOnExpiredLeases?: boolean;
|
|
1770
|
+
maximumBackupAgeSeconds?: number | null;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
export class DoctorPolicy {
|
|
1774
|
+
readonly failOnDeadLetters: boolean;
|
|
1775
|
+
readonly failOnExpiredLeases: boolean;
|
|
1776
|
+
readonly maximumBackupAgeSeconds: number | null;
|
|
1777
|
+
|
|
1778
|
+
constructor(options: DoctorPolicyOptions = {}) {
|
|
1779
|
+
this.failOnDeadLetters = options.failOnDeadLetters ?? true;
|
|
1780
|
+
this.failOnExpiredLeases = options.failOnExpiredLeases ?? true;
|
|
1781
|
+
this.maximumBackupAgeSeconds = options.maximumBackupAgeSeconds ?? null;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
export interface DoctorReport {
|
|
1786
|
+
ready: boolean;
|
|
1787
|
+
checks: readonly DoctorCheck[];
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
export class ProductionDoctor {
|
|
1791
|
+
constructor(
|
|
1792
|
+
private readonly store: ProductionAssuranceStore,
|
|
1793
|
+
private readonly runId: string | null = null,
|
|
1794
|
+
private readonly policy: DoctorPolicy = new DoctorPolicy(),
|
|
1795
|
+
) {}
|
|
1796
|
+
|
|
1797
|
+
run(options: { nowUnix?: number } = {}): DoctorReport {
|
|
1798
|
+
const now = options.nowUnix ?? Date.now() / 1000;
|
|
1799
|
+
const checks: DoctorCheck[] = [];
|
|
1800
|
+
|
|
1801
|
+
const declared = ProductionAssuranceStore.declaredSqliteVersion;
|
|
1802
|
+
const walSafe =
|
|
1803
|
+
declared === null ||
|
|
1804
|
+
sqliteWalVersionIsSafe(declared) ||
|
|
1805
|
+
!this.store.config.requireSafeMultiwriterWal;
|
|
1806
|
+
checks.push({
|
|
1807
|
+
name: "sqlite_wal_runtime",
|
|
1808
|
+
passed: walSafe,
|
|
1809
|
+
severity: "error",
|
|
1810
|
+
detail: declared ? `SQLite ${declared.join(".")}` : "no native SQLite in this runtime",
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
checks.push({
|
|
1814
|
+
name: "application_id",
|
|
1815
|
+
passed: this.store.getApplicationId() === V7_APPLICATION_ID,
|
|
1816
|
+
severity: "error",
|
|
1817
|
+
detail: String(this.store.getApplicationId()),
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
checks.push({
|
|
1821
|
+
name: "schema_version",
|
|
1822
|
+
passed: this.store.getUserVersion() === V7_SCHEMA_VERSION,
|
|
1823
|
+
severity: "error",
|
|
1824
|
+
detail: String(this.store.getUserVersion()),
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
const migrations = this.store.migrationReport();
|
|
1828
|
+
const migrationValid =
|
|
1829
|
+
migrations.length === V7_MIGRATIONS.length &&
|
|
1830
|
+
migrations.every((row, i) => row.checksum === V7_MIGRATIONS[i].checksum);
|
|
1831
|
+
checks.push({
|
|
1832
|
+
name: "migration_checksums",
|
|
1833
|
+
passed: migrationValid,
|
|
1834
|
+
severity: "error",
|
|
1835
|
+
detail: `${migrations.length} migrations`,
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
checks.push({
|
|
1839
|
+
name: "database_integrity",
|
|
1840
|
+
passed: this.store.verifyAllBlobs(),
|
|
1841
|
+
severity: "error",
|
|
1842
|
+
detail: "blob digests",
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
if (this.runId !== null) {
|
|
1846
|
+
checks.push({
|
|
1847
|
+
name: "legacy_event_chain",
|
|
1848
|
+
passed: this.store.verifyChain(this.runId),
|
|
1849
|
+
severity: "error",
|
|
1850
|
+
detail: this.runId,
|
|
1851
|
+
});
|
|
1852
|
+
checks.push({
|
|
1853
|
+
name: "semantic_event_chain",
|
|
1854
|
+
passed: this.store.verifySemanticChain(this.runId),
|
|
1855
|
+
severity: "error",
|
|
1856
|
+
detail: this.runId,
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
const jobs = this.store._jobsForRun(this.runId);
|
|
1860
|
+
const expired = jobs.filter(
|
|
1861
|
+
(j) => j.status === "leased" && j.leaseExpiresUnix <= now,
|
|
1862
|
+
).length;
|
|
1863
|
+
checks.push({
|
|
1864
|
+
name: "expired_job_leases",
|
|
1865
|
+
passed: expired === 0 || !this.policy.failOnExpiredLeases,
|
|
1866
|
+
severity: this.policy.failOnExpiredLeases ? "error" : "warning",
|
|
1867
|
+
detail: String(expired),
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
const deadLetters = jobs.filter((j) => j.status === "dead_letter").length;
|
|
1871
|
+
checks.push({
|
|
1872
|
+
name: "dead_letters",
|
|
1873
|
+
passed: deadLetters === 0 || !this.policy.failOnDeadLetters,
|
|
1874
|
+
severity: this.policy.failOnDeadLetters ? "error" : "warning",
|
|
1875
|
+
detail: String(deadLetters),
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
if (this.policy.maximumBackupAgeSeconds !== null) {
|
|
1879
|
+
const lastBackup = this.store._latestBackupUnix(this.runId);
|
|
1880
|
+
let backupOk: boolean;
|
|
1881
|
+
let detail: string;
|
|
1882
|
+
if (lastBackup === null) {
|
|
1883
|
+
backupOk = false;
|
|
1884
|
+
detail = "No verified backup";
|
|
1885
|
+
} else {
|
|
1886
|
+
const age = now - lastBackup;
|
|
1887
|
+
backupOk = age <= this.policy.maximumBackupAgeSeconds;
|
|
1888
|
+
detail = `age_seconds=${age.toFixed(1)}`;
|
|
1889
|
+
}
|
|
1890
|
+
checks.push({
|
|
1891
|
+
name: "backup_freshness",
|
|
1892
|
+
passed: backupOk,
|
|
1893
|
+
severity: "error",
|
|
1894
|
+
detail,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const ready = !checks.some((c) => !c.passed && c.severity === "error");
|
|
1900
|
+
return { ready, checks };
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1905
|
+
// 10. Compatibility audit
|
|
1906
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1907
|
+
|
|
1908
|
+
export function compatibilityAuditV7(): Record<string, unknown> {
|
|
1909
|
+
const declared = ProductionAssuranceStore.declaredSqliteVersion;
|
|
1910
|
+
return {
|
|
1911
|
+
version: "v7.0",
|
|
1912
|
+
canonical_dependency_filename: "innovation-genome-v5.ts",
|
|
1913
|
+
canonical_dependency_designation: "V6",
|
|
1914
|
+
modifies_v1_through_v6: false,
|
|
1915
|
+
adds_checksummed_schema_migrations: true,
|
|
1916
|
+
adds_sqlite_wal_reset_guard: true,
|
|
1917
|
+
adds_hardened_sqlite_configuration: true,
|
|
1918
|
+
adds_atomic_domain_event_unit_of_work: true,
|
|
1919
|
+
adds_durable_leased_job_queue: true,
|
|
1920
|
+
adds_fencing_tokens: true,
|
|
1921
|
+
adds_retry_safe_dead_lettering: true,
|
|
1922
|
+
adds_secure_bounded_telemetry: true,
|
|
1923
|
+
adds_exact_verifier_closure: true,
|
|
1924
|
+
adds_merkle_challenge_openings: true,
|
|
1925
|
+
adds_verified_online_backup_restore: true,
|
|
1926
|
+
adds_production_readiness_doctor: true,
|
|
1927
|
+
sqlite_runtime: declared ? declared.join(".") : "none (browser runtime)",
|
|
1928
|
+
sqlite_wal_runtime_safe: declared === null ? null : sqliteWalVersionIsSafe(declared),
|
|
1929
|
+
schema_version: V7_SCHEMA_VERSION,
|
|
1930
|
+
application_id: V7_APPLICATION_ID,
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1935
|
+
// 11. Diagnostics (V5 + V7 combined)
|
|
1936
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1937
|
+
|
|
1938
|
+
export interface DiagnosticCheck {
|
|
1939
|
+
id: string;
|
|
1940
|
+
passed: boolean;
|
|
1941
|
+
detail: string;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
export async function runInnovationGenomeV57Diagnostics(): Promise<{
|
|
1945
|
+
ok: boolean;
|
|
1946
|
+
checks: DiagnosticCheck[];
|
|
1947
|
+
}> {
|
|
1948
|
+
const checks: DiagnosticCheck[] = [];
|
|
1949
|
+
const add = (id: string, passed: boolean, detail: string) =>
|
|
1950
|
+
checks.push({ id, passed, detail });
|
|
1951
|
+
|
|
1952
|
+
// ── V7 §1: WAL version predicate (pure logic, ported exactly) ──────
|
|
1953
|
+
add("wal-vulnerable-3.51.2", !sqliteWalVersionIsSafe([3, 51, 2]), "unsafe");
|
|
1954
|
+
add("wal-vulnerable-3.50.6", !sqliteWalVersionIsSafe([3, 50, 6]), "unsafe");
|
|
1955
|
+
add("wal-vulnerable-3.44.5", !sqliteWalVersionIsSafe([3, 44, 5]), "unsafe");
|
|
1956
|
+
add("wal-vulnerable-3.49.99", !sqliteWalVersionIsSafe([3, 49, 99]), "unsafe");
|
|
1957
|
+
add("wal-fixed-3.51.3", sqliteWalVersionIsSafe([3, 51, 3]), "safe");
|
|
1958
|
+
add("wal-fixed-3.50.7", sqliteWalVersionIsSafe([3, 50, 7]), "safe");
|
|
1959
|
+
add("wal-fixed-3.44.6", sqliteWalVersionIsSafe([3, 44, 6]), "safe");
|
|
1960
|
+
add("wal-fixed-3.52.0", sqliteWalVersionIsSafe([3, 52, 0]), "safe");
|
|
1961
|
+
|
|
1962
|
+
// ── V7 §2/3: store identity + migrations ──────────────────────────
|
|
1963
|
+
const store = new ProductionAssuranceStore(
|
|
1964
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
1965
|
+
);
|
|
1966
|
+
store.createRun("run-1", 1, "general", "low", { version: "test" });
|
|
1967
|
+
|
|
1968
|
+
add("app-id", store.getApplicationId() === V7_APPLICATION_ID, String(store.getApplicationId()));
|
|
1969
|
+
add("schema-version", store.getUserVersion() === V7_SCHEMA_VERSION, String(store.getUserVersion()));
|
|
1970
|
+
add(
|
|
1971
|
+
"migration-count",
|
|
1972
|
+
store.migrationReport().length === V7_SCHEMA_VERSION,
|
|
1973
|
+
String(store.migrationReport().length),
|
|
1974
|
+
);
|
|
1975
|
+
|
|
1976
|
+
// ── V7 §3: atomicChange rolls back domain AND event ───────────────
|
|
1977
|
+
const eventsBefore = store.verifyChain("run-1");
|
|
1978
|
+
let rolledBack = false;
|
|
1979
|
+
try {
|
|
1980
|
+
store.atomicChange<void>(
|
|
1981
|
+
"run-1",
|
|
1982
|
+
(journal) => {
|
|
1983
|
+
journal.stage(() =>
|
|
1984
|
+
store._putMerkleSuite({
|
|
1985
|
+
suiteId: "rollback-suite",
|
|
1986
|
+
runId: "run-1",
|
|
1987
|
+
name: "x",
|
|
1988
|
+
rootHash: "a".repeat(64),
|
|
1989
|
+
treeSize: 1,
|
|
1990
|
+
hashAlgorithm: "SHA256",
|
|
1991
|
+
createdUtc: utcNow(),
|
|
1992
|
+
}),
|
|
1993
|
+
);
|
|
1994
|
+
throw new Error("force rollback");
|
|
1995
|
+
},
|
|
1996
|
+
() => ["should_not_exist", {}],
|
|
1997
|
+
);
|
|
1998
|
+
} catch {
|
|
1999
|
+
rolledBack = true;
|
|
2000
|
+
}
|
|
2001
|
+
add("atomic-rollback-throws", rolledBack, "threw");
|
|
2002
|
+
add(
|
|
2003
|
+
"atomic-rollback-no-domain-row",
|
|
2004
|
+
store._getMerkleSuite("rollback-suite", "run-1") === undefined,
|
|
2005
|
+
"no suite row",
|
|
2006
|
+
);
|
|
2007
|
+
add("atomic-rollback-chain-intact", eventsBefore && store.verifyChain("run-1"), "chain ok");
|
|
2008
|
+
add("atomic-rollback-semantic-intact", store.verifySemanticChain("run-1"), "semantic ok");
|
|
2009
|
+
|
|
2010
|
+
// ── V4 regression: semantic chain must DETECT tampering ───────────
|
|
2011
|
+
const tamperStore = new ProductionAssuranceStore(
|
|
2012
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
2013
|
+
);
|
|
2014
|
+
tamperStore.createRun("t-1", 1, "general", "low", {});
|
|
2015
|
+
tamperStore.appendEvent("t-1", "event", { value: 1 });
|
|
2016
|
+
const preTamper = tamperStore.verifySemanticChain("t-1");
|
|
2017
|
+
tamperStore._tamperPayloadForTest("t-1", "event", '{"value":2}');
|
|
2018
|
+
const postTamper = tamperStore.verifySemanticChain("t-1");
|
|
2019
|
+
add("semantic-chain-valid-before-tamper", preTamper, "valid");
|
|
2020
|
+
add("semantic-chain-detects-tampering", postTamper === false, "detected");
|
|
2021
|
+
|
|
2022
|
+
// ── V7 §4: durable queue ──────────────────────────────────────────
|
|
2023
|
+
const queue = new DurableJobQueue(store, "run-1");
|
|
2024
|
+
|
|
2025
|
+
const specA = new JobSpec({
|
|
2026
|
+
queueName: "verification",
|
|
2027
|
+
jobKind: "verify",
|
|
2028
|
+
payload: { candidate: "c1" },
|
|
2029
|
+
idempotencyKey: "idempotency-key-0001",
|
|
2030
|
+
});
|
|
2031
|
+
const first = queue.enqueue(specA, 100);
|
|
2032
|
+
const second = queue.enqueue(specA, 100);
|
|
2033
|
+
add("idempotent-enqueue", first === second, first);
|
|
2034
|
+
add("idempotent-enqueue-count", queue.stats().pending === 1, String(queue.stats().pending));
|
|
2035
|
+
|
|
2036
|
+
let intentRejected = false;
|
|
2037
|
+
try {
|
|
2038
|
+
queue.enqueue(
|
|
2039
|
+
new JobSpec({
|
|
2040
|
+
queueName: "verification",
|
|
2041
|
+
jobKind: "different",
|
|
2042
|
+
payload: { candidate: "c2" },
|
|
2043
|
+
idempotencyKey: "idempotency-key-0001",
|
|
2044
|
+
}),
|
|
2045
|
+
100,
|
|
2046
|
+
);
|
|
2047
|
+
} catch {
|
|
2048
|
+
intentRejected = true;
|
|
2049
|
+
}
|
|
2050
|
+
add("idempotency-different-intent-rejected", intentRejected, "threw");
|
|
2051
|
+
|
|
2052
|
+
// Fencing
|
|
2053
|
+
const fenceQueue = new DurableJobQueue(store, "run-1");
|
|
2054
|
+
fenceQueue.enqueue(
|
|
2055
|
+
new JobSpec({
|
|
2056
|
+
queueName: "fq",
|
|
2057
|
+
jobKind: "task",
|
|
2058
|
+
payload: { x: 1 },
|
|
2059
|
+
idempotencyKey: "fencing-key-000001",
|
|
2060
|
+
retrySafe: true,
|
|
2061
|
+
maxAttempts: 3,
|
|
2062
|
+
}),
|
|
2063
|
+
100,
|
|
2064
|
+
);
|
|
2065
|
+
const leaseA = fenceQueue.claim("fq", "worker-a", { leaseSeconds: 10, nowUnix: 100 });
|
|
2066
|
+
const leaseB = fenceQueue.claim("fq", "worker-b", { leaseSeconds: 10, nowUnix: 111 });
|
|
2067
|
+
add("claim-a", leaseA !== null, leaseA ? leaseA.jobId : "null");
|
|
2068
|
+
add("claim-b-after-expiry", leaseB !== null, leaseB ? leaseB.jobId : "null");
|
|
2069
|
+
add(
|
|
2070
|
+
"fencing-token-monotonic",
|
|
2071
|
+
!!leaseA && !!leaseB && leaseB.fencingToken > leaseA.fencingToken,
|
|
2072
|
+
`${leaseA?.fencingToken} -> ${leaseB?.fencingToken}`,
|
|
2073
|
+
);
|
|
2074
|
+
|
|
2075
|
+
let staleRejected = false;
|
|
2076
|
+
try {
|
|
2077
|
+
if (leaseA) fenceQueue.complete(leaseA, { wrong: "stale" }, 111);
|
|
2078
|
+
} catch (e) {
|
|
2079
|
+
staleRejected = e instanceof LeaseLostError;
|
|
2080
|
+
}
|
|
2081
|
+
add("stale-worker-rejected", staleRejected, "LeaseLostError");
|
|
2082
|
+
|
|
2083
|
+
if (leaseB) {
|
|
2084
|
+
const digest = fenceQueue.complete(leaseB, { ok: true }, 112);
|
|
2085
|
+
add("current-worker-completes", digest.length === 64, digest.slice(0, 12));
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Heartbeat
|
|
2089
|
+
const hbQueue = new DurableJobQueue(store, "run-1");
|
|
2090
|
+
hbQueue.enqueue(
|
|
2091
|
+
new JobSpec({
|
|
2092
|
+
queueName: "hq",
|
|
2093
|
+
jobKind: "task",
|
|
2094
|
+
payload: {},
|
|
2095
|
+
idempotencyKey: "heartbeat-key-00001",
|
|
2096
|
+
}),
|
|
2097
|
+
100,
|
|
2098
|
+
);
|
|
2099
|
+
const hbLease = hbQueue.claim("hq", "worker", { leaseSeconds: 10, nowUnix: 100 });
|
|
2100
|
+
if (hbLease) {
|
|
2101
|
+
const renewed = hbQueue.heartbeat(hbLease, { extensionSeconds: 20, nowUnix: 105 });
|
|
2102
|
+
add(
|
|
2103
|
+
"heartbeat-extends-lease",
|
|
2104
|
+
renewed.leaseExpiresUnix > hbLease.leaseExpiresUnix,
|
|
2105
|
+
`${hbLease.leaseExpiresUnix} -> ${renewed.leaseExpiresUnix}`,
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Non-retry-safe dead-letters and never leaks the message
|
|
2110
|
+
const dlQueue = new DurableJobQueue(store, "run-1");
|
|
2111
|
+
dlQueue.enqueue(
|
|
2112
|
+
new JobSpec({
|
|
2113
|
+
queueName: "dq",
|
|
2114
|
+
jobKind: "side-effect",
|
|
2115
|
+
payload: {},
|
|
2116
|
+
idempotencyKey: "nonretry-key-000001",
|
|
2117
|
+
retrySafe: false,
|
|
2118
|
+
maxAttempts: 1,
|
|
2119
|
+
}),
|
|
2120
|
+
100,
|
|
2121
|
+
);
|
|
2122
|
+
const dlLease = dlQueue.claim("dq", "worker", { nowUnix: 100 });
|
|
2123
|
+
if (dlLease) {
|
|
2124
|
+
const status = dlQueue.fail(dlLease, new RangeError("sensitive error details"), 101);
|
|
2125
|
+
add("non-retry-safe-dead-letters", status === "dead_letter", status);
|
|
2126
|
+
const row = store._getJob(dlLease.jobId);
|
|
2127
|
+
add("error-type-recorded", row?.lastErrorType === "RangeError", String(row?.lastErrorType));
|
|
2128
|
+
add("error-hash-only", row?.lastErrorHash.length === 64, String(row?.lastErrorHash.length));
|
|
2129
|
+
add(
|
|
2130
|
+
"error-message-not-leaked",
|
|
2131
|
+
!(row?.lastErrorHash ?? "").includes("sensitive"),
|
|
2132
|
+
"hashed",
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// ── V7 §5: telemetry sanitization ─────────────────────────────────
|
|
2137
|
+
const sanitizer = new TelemetrySanitizer(
|
|
2138
|
+
"k".repeat(32),
|
|
2139
|
+
new TelemetryLimits({
|
|
2140
|
+
attributeCountLimit: 4,
|
|
2141
|
+
attributeValueLengthLimit: 12,
|
|
2142
|
+
sequenceLengthLimit: 2,
|
|
2143
|
+
nestingDepthLimit: 2,
|
|
2144
|
+
}),
|
|
2145
|
+
);
|
|
2146
|
+
const [sanitized, stats] = sanitizer.sanitize({
|
|
2147
|
+
authorization: "Bearer secret",
|
|
2148
|
+
normal: "abcdefghijklmnopqrstuvwxyz",
|
|
2149
|
+
list: [1, 2, 3, 4],
|
|
2150
|
+
nested: { api_key: "private" },
|
|
2151
|
+
overflow: "dropped",
|
|
2152
|
+
});
|
|
2153
|
+
add(
|
|
2154
|
+
"telemetry-redacts-authorization",
|
|
2155
|
+
String(sanitized.authorization).startsWith("hmac-sha256:"),
|
|
2156
|
+
"pseudonymized",
|
|
2157
|
+
);
|
|
2158
|
+
add("telemetry-truncates", sanitized.normal === "abcdefghijkl", String(sanitized.normal));
|
|
2159
|
+
add(
|
|
2160
|
+
"telemetry-limits-sequence",
|
|
2161
|
+
Array.isArray(sanitized.list) && (sanitized.list as unknown[]).length === 2,
|
|
2162
|
+
JSON.stringify(sanitized.list),
|
|
2163
|
+
);
|
|
2164
|
+
add("telemetry-stats-redacted", stats.redacted >= 1, String(stats.redacted));
|
|
2165
|
+
add("telemetry-stats-truncated", stats.truncated >= 1, String(stats.truncated));
|
|
2166
|
+
add("telemetry-stats-dropped", stats.dropped >= 1, String(stats.dropped));
|
|
2167
|
+
|
|
2168
|
+
const recorder = new SecureTelemetryRecorder(store, "run-1", new TelemetrySanitizer("x".repeat(32)));
|
|
2169
|
+
const span = recorder.startSpan("test", "internal");
|
|
2170
|
+
recorder.endSpan(span.context, "test", "internal", span.startMonotonic, span.startUtc, "ok", {
|
|
2171
|
+
api_token: "plaintext-secret",
|
|
2172
|
+
safe: "value",
|
|
2173
|
+
});
|
|
2174
|
+
const spanRows = store._getSpans("run-1");
|
|
2175
|
+
const spanJson = spanRows.map((s) => s.attributesJson).join("");
|
|
2176
|
+
add("span-never-persists-raw-secret", !spanJson.includes("plaintext-secret"), "redacted");
|
|
2177
|
+
add("span-persists-pseudonym", spanJson.includes("hmac-sha256:"), "pseudonym present");
|
|
2178
|
+
|
|
2179
|
+
// ── V7 §6: exact verifier closure ─────────────────────────────────
|
|
2180
|
+
const closurePolicy = new VerifierClosurePolicy([
|
|
2181
|
+
{ name: "lean", acceptedVersions: ["4.20.0"], requireEvidence: true },
|
|
2182
|
+
{ name: "property-tests", acceptedVersions: ["1.0"], requireEvidence: true },
|
|
2183
|
+
]);
|
|
2184
|
+
const gate = new VerifierClosureGate(store, "run-1", closurePolicy);
|
|
2185
|
+
|
|
2186
|
+
const insertVerifier = (
|
|
2187
|
+
name: string,
|
|
2188
|
+
version: string,
|
|
2189
|
+
verdict = "pass",
|
|
2190
|
+
evidence = true,
|
|
2191
|
+
) => {
|
|
2192
|
+
store._putVerifierRun({
|
|
2193
|
+
verifierRunId: `${name}-${version}-${Math.random()}`,
|
|
2194
|
+
runId: "run-1",
|
|
2195
|
+
candidateId: "candidate-1",
|
|
2196
|
+
verifierName: name,
|
|
2197
|
+
verifierVersion: version,
|
|
2198
|
+
jobHash: "j".repeat(64),
|
|
2199
|
+
verdict,
|
|
2200
|
+
resultJson: JSON.stringify({
|
|
2201
|
+
verifier_name: name,
|
|
2202
|
+
verifier_version: version,
|
|
2203
|
+
verdict,
|
|
2204
|
+
evidence: evidence ? [{ content_hash: "a".repeat(64) }] : [],
|
|
2205
|
+
}),
|
|
2206
|
+
resultHash: "r".repeat(64),
|
|
2207
|
+
createdUtc: utcNow(),
|
|
2208
|
+
});
|
|
2209
|
+
};
|
|
2210
|
+
|
|
2211
|
+
insertVerifier("lean", "4.20.0");
|
|
2212
|
+
const partial = gate.evaluate("candidate-1");
|
|
2213
|
+
add("missing-required-verifier-fails", !partial.closed, JSON.stringify(partial.missing));
|
|
2214
|
+
add("missing-listed", partial.missing.includes("property-tests"), "property-tests missing");
|
|
2215
|
+
|
|
2216
|
+
insertVerifier("property-tests", "1.0");
|
|
2217
|
+
const complete = gate.evaluate("candidate-1");
|
|
2218
|
+
add("exact-required-set-passes", complete.closed, JSON.stringify(complete.satisfied));
|
|
2219
|
+
add("satisfied-count", complete.satisfied.length === 2, String(complete.satisfied.length));
|
|
2220
|
+
|
|
2221
|
+
insertVerifier("lean", "4.20.0", "pass", false);
|
|
2222
|
+
const noEvidence = gate.evaluate("candidate-1");
|
|
2223
|
+
add(
|
|
2224
|
+
"pass-without-evidence-fails",
|
|
2225
|
+
!noEvidence.closed && noEvidence.failed.some((f) => f.includes("missing-evidence")),
|
|
2226
|
+
JSON.stringify(noEvidence.failed),
|
|
2227
|
+
);
|
|
2228
|
+
|
|
2229
|
+
// ── V7 §7: Merkle openings ────────────────────────────────────────
|
|
2230
|
+
const seal = sealChallengeCases(
|
|
2231
|
+
{
|
|
2232
|
+
"case-a": { input: 1, expected: 2 },
|
|
2233
|
+
"case-b": { input: 2, expected: 4 },
|
|
2234
|
+
"case-c": { input: 3, expected: 6 },
|
|
2235
|
+
},
|
|
2236
|
+
{ deterministicSaltSeed: "s".repeat(32) },
|
|
2237
|
+
);
|
|
2238
|
+
add("merkle-tree-size", seal.treeSize === 3, String(seal.treeSize));
|
|
2239
|
+
const allOpeningsValid = Object.values(seal.openings).every((o) =>
|
|
2240
|
+
verifyMerkleOpening(seal.rootHash, o),
|
|
2241
|
+
);
|
|
2242
|
+
add("merkle-every-opening-verifies", allOpeningsValid, "3/3");
|
|
2243
|
+
|
|
2244
|
+
const twoSeal = sealChallengeCases(
|
|
2245
|
+
{ "case-a": { value: 1 }, "case-b": { value: 2 } },
|
|
2246
|
+
{ deterministicSaltSeed: "s".repeat(32) },
|
|
2247
|
+
);
|
|
2248
|
+
const tampered: MerkleOpening = {
|
|
2249
|
+
...twoSeal.openings["case-a"],
|
|
2250
|
+
payloadJson: '{"value":999}',
|
|
2251
|
+
};
|
|
2252
|
+
add("merkle-modified-opening-fails", !verifyMerkleOpening(twoSeal.rootHash, tampered), "rejected");
|
|
2253
|
+
|
|
2254
|
+
const registry = new MerkleChallengeRegistry(store, "run-1");
|
|
2255
|
+
const secretSeal = sealChallengeCases(
|
|
2256
|
+
{ "secret-case": { answer: "hidden" } },
|
|
2257
|
+
{ deterministicSaltSeed: "s".repeat(32) },
|
|
2258
|
+
);
|
|
2259
|
+
registry.register("suite-1", "sealed suite", secretSeal);
|
|
2260
|
+
const suiteRow = store._getMerkleSuite("suite-1", "run-1");
|
|
2261
|
+
add("merkle-registry-stores-root", suiteRow?.rootHash === secretSeal.rootHash, "root stored");
|
|
2262
|
+
add(
|
|
2263
|
+
"merkle-registry-hides-payload",
|
|
2264
|
+
!JSON.stringify(suiteRow ?? {}).includes("hidden"),
|
|
2265
|
+
"payload absent",
|
|
2266
|
+
);
|
|
2267
|
+
add(
|
|
2268
|
+
"merkle-registry-verifies-opening",
|
|
2269
|
+
registry.verify("suite-1", secretSeal.openings["secret-case"]),
|
|
2270
|
+
"verified",
|
|
2271
|
+
);
|
|
2272
|
+
|
|
2273
|
+
// ── V7 §8: backup ─────────────────────────────────────────────────
|
|
2274
|
+
const backup = new BackupManager(store, "run-1").create("backups/run-1.snapshot");
|
|
2275
|
+
add("backup-valid", backup.verification.valid, "valid");
|
|
2276
|
+
add("backup-digest-len", backup.fileDigest.length === 64, String(backup.fileDigest.length));
|
|
2277
|
+
|
|
2278
|
+
// ── V7 §9: doctor ─────────────────────────────────────────────────
|
|
2279
|
+
const cleanStore = new ProductionAssuranceStore(
|
|
2280
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
2281
|
+
);
|
|
2282
|
+
cleanStore.createRun("run-clean", 1, "general", "low", {});
|
|
2283
|
+
const cleanReport = new ProductionDoctor(cleanStore, "run-clean").run();
|
|
2284
|
+
add("doctor-clean-ready", cleanReport.ready, JSON.stringify(cleanReport.checks.filter((c) => !c.passed)));
|
|
2285
|
+
|
|
2286
|
+
const dlStore = new ProductionAssuranceStore(
|
|
2287
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
2288
|
+
);
|
|
2289
|
+
dlStore.createRun("run-dl", 1, "general", "low", {});
|
|
2290
|
+
const dlQ = new DurableJobQueue(dlStore, "run-dl");
|
|
2291
|
+
dlQ.enqueue(
|
|
2292
|
+
new JobSpec({
|
|
2293
|
+
queueName: "q",
|
|
2294
|
+
jobKind: "side-effect",
|
|
2295
|
+
payload: {},
|
|
2296
|
+
idempotencyKey: "doctor-deadletter-key",
|
|
2297
|
+
retrySafe: false,
|
|
2298
|
+
maxAttempts: 1,
|
|
2299
|
+
}),
|
|
2300
|
+
100,
|
|
2301
|
+
);
|
|
2302
|
+
const doctorLease = dlQ.claim("q", "worker", { nowUnix: 100 });
|
|
2303
|
+
if (doctorLease) dlQ.fail(doctorLease, new Error("failure"), 101);
|
|
2304
|
+
const dlReport = new ProductionDoctor(dlStore, "run-dl").run({ nowUnix: 102 });
|
|
2305
|
+
add("doctor-dead-letter-blocks-readiness", !dlReport.ready, "blocked");
|
|
2306
|
+
|
|
2307
|
+
// ── V7 §10: compatibility audit ───────────────────────────────────
|
|
2308
|
+
const audit = compatibilityAuditV7();
|
|
2309
|
+
add("audit-does-not-modify-v1-v6", audit.modifies_v1_through_v6 === false, "additive");
|
|
2310
|
+
add("audit-durable-queue", audit.adds_durable_leased_job_queue === true, "yes");
|
|
2311
|
+
add("audit-fencing-tokens", audit.adds_fencing_tokens === true, "yes");
|
|
2312
|
+
add("audit-backup-restore", audit.adds_verified_online_backup_restore === true, "yes");
|
|
2313
|
+
|
|
2314
|
+
// ═════════════════════════════════════════════════════════════════
|
|
2315
|
+
// V5 (Production Assurance Plane) — exercised here so the policy
|
|
2316
|
+
// engine, canaries, suites, verifiers, replay and health monitor are
|
|
2317
|
+
// genuinely executed and genuinely bundled (not tree-shaken).
|
|
2318
|
+
// ═════════════════════════════════════════════════════════════════
|
|
2319
|
+
const v5Store = new ProductionAssuranceStore(
|
|
2320
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
2321
|
+
);
|
|
2322
|
+
v5Store.createRun("v5-run", 1, "general", "low", {});
|
|
2323
|
+
|
|
2324
|
+
const policy = new PolicyEngine(defaultPolicyRules());
|
|
2325
|
+
add("v5-policy-rule-count", policy.rules.length === 5, String(policy.rules.length));
|
|
2326
|
+
|
|
2327
|
+
// V5-POL-001 — high stakes needs human approval
|
|
2328
|
+
const pol1 = policy.evaluate({
|
|
2329
|
+
runId: "v5-run",
|
|
2330
|
+
subject: "c",
|
|
2331
|
+
action: "promote_candidate",
|
|
2332
|
+
resource: "candidate",
|
|
2333
|
+
risk: "critical",
|
|
2334
|
+
payload: { evidence_closed: true },
|
|
2335
|
+
});
|
|
2336
|
+
add(
|
|
2337
|
+
"v5-POL-001-high-stakes-needs-human",
|
|
2338
|
+
!pol1.allowed && pol1.violations.some((v) => v.ruleId === "V5-POL-001"),
|
|
2339
|
+
JSON.stringify(pol1.violations.map((v) => v.ruleId)),
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
// V5-POL-002 — world action needs scope
|
|
2343
|
+
const pol2 = policy.evaluate({
|
|
2344
|
+
runId: "v5-run",
|
|
2345
|
+
subject: "c",
|
|
2346
|
+
action: "tool_call",
|
|
2347
|
+
resource: "tool",
|
|
2348
|
+
risk: "low",
|
|
2349
|
+
payload: { world_action: true, grant_scopes: ["tool:echo"], tool_name: "echo" },
|
|
2350
|
+
capabilities: { declared_tools: ["echo"] },
|
|
2351
|
+
});
|
|
2352
|
+
add(
|
|
2353
|
+
"v5-POL-002-world-action-needs-scope",
|
|
2354
|
+
!pol2.allowed && pol2.violations.some((v) => v.ruleId === "V5-POL-002"),
|
|
2355
|
+
JSON.stringify(pol2.violations.map((v) => v.ruleId)),
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
// V5-POL-003 — undeclared tool
|
|
2359
|
+
const pol3 = policy.evaluate({
|
|
2360
|
+
runId: "v5-run",
|
|
2361
|
+
subject: "c",
|
|
2362
|
+
action: "tool_call",
|
|
2363
|
+
resource: "tool",
|
|
2364
|
+
risk: "low",
|
|
2365
|
+
payload: { tool_name: "rm-rf", grant_scopes: ["*"] },
|
|
2366
|
+
capabilities: { declared_tools: ["echo"] },
|
|
2367
|
+
});
|
|
2368
|
+
add(
|
|
2369
|
+
"v5-POL-003-undeclared-tool",
|
|
2370
|
+
!pol3.allowed && pol3.violations.some((v) => v.ruleId === "V5-POL-003"),
|
|
2371
|
+
JSON.stringify(pol3.violations.map((v) => v.ruleId)),
|
|
2372
|
+
);
|
|
2373
|
+
|
|
2374
|
+
// V5-POL-004 — promotion without evidence closure
|
|
2375
|
+
const pol4 = policy.evaluate({
|
|
2376
|
+
runId: "v5-run",
|
|
2377
|
+
subject: "c",
|
|
2378
|
+
action: "promote_candidate",
|
|
2379
|
+
resource: "candidate",
|
|
2380
|
+
risk: "low",
|
|
2381
|
+
payload: { evidence_closed: false },
|
|
2382
|
+
});
|
|
2383
|
+
add(
|
|
2384
|
+
"v5-POL-004-promotion-needs-evidence",
|
|
2385
|
+
!pol4.allowed && pol4.violations.some((v) => v.ruleId === "V5-POL-004"),
|
|
2386
|
+
JSON.stringify(pol4.violations.map((v) => v.ruleId)),
|
|
2387
|
+
);
|
|
2388
|
+
|
|
2389
|
+
// V5-POL-005 — canary leak is FATAL
|
|
2390
|
+
const pol5 = policy.evaluate({
|
|
2391
|
+
runId: "v5-run",
|
|
2392
|
+
subject: "c",
|
|
2393
|
+
action: "promote_candidate",
|
|
2394
|
+
resource: "candidate",
|
|
2395
|
+
risk: "low",
|
|
2396
|
+
payload: { evidence_closed: true, canary_hits: 1 },
|
|
2397
|
+
});
|
|
2398
|
+
add(
|
|
2399
|
+
"v5-POL-005-canary-leak-fatal",
|
|
2400
|
+
!pol5.allowed &&
|
|
2401
|
+
pol5.violations.some((v) => v.ruleId === "V5-POL-005" && v.severity === "fatal"),
|
|
2402
|
+
JSON.stringify(pol5.violations.map((v) => v.ruleId)),
|
|
2403
|
+
);
|
|
2404
|
+
|
|
2405
|
+
// Clean input passes
|
|
2406
|
+
const polOk = policy.evaluate({
|
|
2407
|
+
runId: "v5-run",
|
|
2408
|
+
subject: "c",
|
|
2409
|
+
action: "promote_candidate",
|
|
2410
|
+
resource: "candidate",
|
|
2411
|
+
risk: "low",
|
|
2412
|
+
payload: { evidence_closed: true, canary_hits: 0 },
|
|
2413
|
+
});
|
|
2414
|
+
add("v5-policy-clean-allows", polOk.allowed, "allowed");
|
|
2415
|
+
|
|
2416
|
+
// enforce() must throw and must persist a denial
|
|
2417
|
+
let enforceThrew = false;
|
|
2418
|
+
try {
|
|
2419
|
+
policy.enforce(v5Store, pol5, "v5-run");
|
|
2420
|
+
} catch {
|
|
2421
|
+
enforceThrew = true;
|
|
2422
|
+
}
|
|
2423
|
+
add("v5-policy-enforce-fails-closed", enforceThrew, "threw");
|
|
2424
|
+
|
|
2425
|
+
// Canary registry: register → scan → hit count
|
|
2426
|
+
const canaries = new CanaryRegistry(v5Store, "v5-run");
|
|
2427
|
+
canaries.register("secret-marker", "ZZ-CANARY-TOKEN-9137", "confidential", "critical");
|
|
2428
|
+
const cleanHits = canaries.scan("cand-clean", "an artifact with no secrets", "artifact");
|
|
2429
|
+
add("v5-canary-clean-no-hits", cleanHits.length === 0, "0 hits");
|
|
2430
|
+
const leakHits = canaries.scan(
|
|
2431
|
+
"cand-leak",
|
|
2432
|
+
"leaked ZZ-CANARY-TOKEN-9137 into the output",
|
|
2433
|
+
"artifact",
|
|
2434
|
+
);
|
|
2435
|
+
add("v5-canary-detects-leak", leakHits.length === 1, `${leakHits.length} hit(s)`);
|
|
2436
|
+
add("v5-canary-hit-count", canaries.hitCount("cand-leak") === 1, "1");
|
|
2437
|
+
add("v5-canary-clean-count-zero", canaries.hitCount("cand-clean") === 0, "0");
|
|
2438
|
+
// Plaintext token must never be persisted. Non-vacuous: assert a row EXISTS,
|
|
2439
|
+
// that it carries the hash, and that it does NOT carry the plaintext.
|
|
2440
|
+
const canaryRows = v5Store._getCanaryRows("v5-run");
|
|
2441
|
+
const canaryRowsJson = JSON.stringify(canaryRows);
|
|
2442
|
+
add("v5-canary-row-persisted", canaryRows.length === 1, `${canaryRows.length} row(s)`);
|
|
2443
|
+
add(
|
|
2444
|
+
"v5-canary-hash-persisted",
|
|
2445
|
+
canaryRows.length === 1 && canaryRows[0].tokenHash === sha256Text("ZZ-CANARY-TOKEN-9137"),
|
|
2446
|
+
"hash matches",
|
|
2447
|
+
);
|
|
2448
|
+
add(
|
|
2449
|
+
"v5-canary-token-not-persisted",
|
|
2450
|
+
canaryRows.length === 1 && !canaryRowsJson.includes("ZZ-CANARY-TOKEN-9137"),
|
|
2451
|
+
"plaintext absent",
|
|
2452
|
+
);
|
|
2453
|
+
|
|
2454
|
+
// Expose eventRowsForRun for V8 to access
|
|
2455
|
+
(v5Store as any)._exposeEventRowsForRun = (runId: string) => (v5Store as any).eventRowsForRun(runId);
|
|
2456
|
+
|
|
2457
|
+
// Challenge suite commitment
|
|
2458
|
+
const suite = new ChallengeSuite(
|
|
2459
|
+
"suite-a",
|
|
2460
|
+
"audit suite",
|
|
2461
|
+
"audit",
|
|
2462
|
+
[{ caseId: "c1", inputDigest: "a".repeat(64), expectedDigest: "b".repeat(64) }],
|
|
2463
|
+
"none",
|
|
2464
|
+
);
|
|
2465
|
+
new ChallengeSuiteRegistry(v5Store, "v5-run").register(suite);
|
|
2466
|
+
add("v5-suite-commitment-len", suite.commitmentHash.length === 64, "sha256");
|
|
2467
|
+
add(
|
|
2468
|
+
"v5-suite-commitment-deterministic",
|
|
2469
|
+
suite.commitmentHash ===
|
|
2470
|
+
new ChallengeSuite(
|
|
2471
|
+
"suite-a",
|
|
2472
|
+
"audit suite",
|
|
2473
|
+
"audit",
|
|
2474
|
+
[{ caseId: "c1", inputDigest: "a".repeat(64), expectedDigest: "b".repeat(64) }],
|
|
2475
|
+
"none",
|
|
2476
|
+
).commitmentHash,
|
|
2477
|
+
"stable",
|
|
2478
|
+
);
|
|
2479
|
+
|
|
2480
|
+
// Verifier orchestrator
|
|
2481
|
+
const orchestrator = new VerifierOrchestrator(v5Store, "v5-run", [
|
|
2482
|
+
new FunctionVerifier(
|
|
2483
|
+
"lean",
|
|
2484
|
+
"4.20.0",
|
|
2485
|
+
(job) =>
|
|
2486
|
+
new VerificationResult("lean", "4.20.0", "pass", [
|
|
2487
|
+
{ evidenceId: "E1", kind: "formal_certificate", content: `cert for ${job.candidateId}` },
|
|
2488
|
+
]),
|
|
2489
|
+
),
|
|
2490
|
+
new FunctionVerifier("boom", "1.0", () => {
|
|
2491
|
+
throw new Error("verifier exploded");
|
|
2492
|
+
}),
|
|
2493
|
+
]);
|
|
2494
|
+
const verifierResults = await orchestrator.run({
|
|
2495
|
+
candidateId: "v5-cand",
|
|
2496
|
+
artifactDigest: "d".repeat(64),
|
|
2497
|
+
artifactMediaType: "text/plain",
|
|
2498
|
+
goalHash: "g".repeat(64),
|
|
2499
|
+
evaluatorHash: "e".repeat(64),
|
|
2500
|
+
verifierScope: "full",
|
|
2501
|
+
});
|
|
2502
|
+
add("v5-verifier-runs-all", verifierResults.length === 2, String(verifierResults.length));
|
|
2503
|
+
add("v5-verifier-pass-recorded", verifierResults[0].verdict === "pass", verifierResults[0].verdict);
|
|
2504
|
+
add(
|
|
2505
|
+
"v5-verifier-exception-becomes-error-verdict",
|
|
2506
|
+
verifierResults[1].verdict === "error",
|
|
2507
|
+
verifierResults[1].verdict,
|
|
2508
|
+
);
|
|
2509
|
+
add(
|
|
2510
|
+
"v5-allRequiredPassed-false-when-any-error",
|
|
2511
|
+
orchestrator.allRequiredPassed("v5-cand") === false,
|
|
2512
|
+
"not all passed",
|
|
2513
|
+
);
|
|
2514
|
+
|
|
2515
|
+
// Replay matrix
|
|
2516
|
+
const replay = new ReplayMatrix(v5Store, "v5-run");
|
|
2517
|
+
const manifest = new ReplayManifest("subject-1", { runtime: "browser" }, [
|
|
2518
|
+
{ name: "step-1", inputHash: "i".repeat(64), expectedOutputHash: sha256Text("out-1") },
|
|
2519
|
+
]);
|
|
2520
|
+
const replayOk = await replay.run(manifest, { runStep: () => sha256Text("out-1") });
|
|
2521
|
+
add("v5-replay-matching-succeeds", replayOk === true, "success");
|
|
2522
|
+
const replayBad = await replay.run(manifest, { runStep: () => sha256Text("different") });
|
|
2523
|
+
add("v5-replay-mismatch-fails", replayBad === false, "detected drift");
|
|
2524
|
+
|
|
2525
|
+
// Health monitor
|
|
2526
|
+
const health = new RunHealthMonitor(v5Store, "v5-run", new HealthThresholds());
|
|
2527
|
+
const healthReport = health.report();
|
|
2528
|
+
add("v5-health-report-shape", typeof healthReport.status === "string", String(healthReport.status));
|
|
2529
|
+
add(
|
|
2530
|
+
"v5-health-fails-on-policy-denial",
|
|
2531
|
+
healthReport.status === "fail" &&
|
|
2532
|
+
(healthReport.reasons as string[]).includes("policy denials exceed threshold"),
|
|
2533
|
+
JSON.stringify(healthReport.reasons),
|
|
2534
|
+
);
|
|
2535
|
+
|
|
2536
|
+
// A clean run should be healthy
|
|
2537
|
+
const cleanHealthStore = new ProductionAssuranceStore(
|
|
2538
|
+
new StoreConfig({ walMode: false, requireSafeMultiwriterWal: false }),
|
|
2539
|
+
);
|
|
2540
|
+
cleanHealthStore.createRun("v5-clean", 1, "general", "low", {});
|
|
2541
|
+
const cleanHealth = new RunHealthMonitor(
|
|
2542
|
+
cleanHealthStore,
|
|
2543
|
+
"v5-clean",
|
|
2544
|
+
new HealthThresholds(),
|
|
2545
|
+
).report();
|
|
2546
|
+
add("v5-health-clean-ok", cleanHealth.status === "ok", JSON.stringify(cleanHealth.reasons));
|
|
2547
|
+
|
|
2548
|
+
return { ok: checks.every((c) => c.passed), checks };
|
|
2549
|
+
}
|