-
Notifications
You must be signed in to change notification settings - Fork 0
Add reproducible content-addressed scenario catalog #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Scenario catalog data model | ||
|
|
||
| The simulator remains a pure deterministic core. Storage is an optional adapter enabled by CAUSAL_LAB_DB, so a database failure cannot alter CRDT or virtual-time semantics and removing the setting restores the original stateless service. | ||
|
|
||
| ## Identity | ||
|
|
||
| Validated JSON is encoded with causal-lab-json-v1: object keys are sorted, arrays retain order, and only finite JSON values are admitted. The scenario ID is SHA-256 over the version label, a separator, and those canonical bytes. Key order and whitespace therefore do not change identity, while any semantic field does. | ||
|
|
||
| Run IDs bind a scenario ID to the complete canonical report. trace_sha256 separately makes trace verification and indexing cheap. Repeating one deterministic scenario produces the same immutable receipt rather than another timestamp-based row. | ||
|
|
||
| ## Tables | ||
|
|
||
| - scenarios stores scenario_id, canonicalization, canonical_json, and definition_bytes. | ||
| - runs stores run_id, scenario_id, report_json, trace_sha256, processed_events, and converged. | ||
| - schema_migrations records the applied schema version and owner. | ||
|
|
||
| All tables use STRICT typing. Digest shape, JSON validity, byte and event bounds, booleans, and the scenario foreign key are database constraints. Triggers reject updates and deletes from content-addressed rows. On every open the adapter runs integrity_check and foreign_key_check. | ||
|
|
||
| ## Operations and recovery | ||
|
|
||
| POST /v1/scenarios stores a validated definition. GET /v1/scenarios/:id returns it. POST /v1/scenarios/:id/runs executes and stores the deterministic receipt, and GET /v1/runs/:id reads it. These routes exist only when the catalog is configured; POST /v1/run remains stateless and backward compatible. | ||
|
|
||
| For a live backup, use the SQLite backup API or briefly stop the single process and copy the database together with its WAL and shared-memory files. A portable rollback is an export of canonical scenario and report JSON, verified again by their IDs before import. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Reproducible Scenario Catalog Implementation Plan | ||
|
|
||
| **Goal:** Add a content-addressed SQLite catalog for scenarios and deterministic run receipts while keeping CRDT semantics storage-independent. | ||
| **Architecture:** Parsing and execution become pure reusable functions. A catalog adapter stores canonical scenario JSON and immutable reports; the simulator never imports storage. | ||
| **Tech Stack:** TypeScript 5.9, Node.js 24 node:sqlite, Hono, Vitest, SQLite STRICT tables | ||
| **Verification:** pnpm lint; pnpm typecheck; pnpm test; pnpm build; PRAGMA foreign_key_check and integrity_check. | ||
|
|
||
| --- | ||
|
|
||
| ## Three-pass review | ||
|
|
||
| 1. Repository evidence: validation and execution live inside one HTTP handler, preventing reusable scenario identity. | ||
| 2. External standard: CRDTs converge from the same update set; SQLite offers strict typing and atomic single-file storage. | ||
| 3. Adversarial review: timestamps, random IDs, mutable reports, arbitrary invariant code, and user-controlled SQL were rejected. | ||
|
|
||
| ## Decision | ||
|
|
||
| Canonicalize validated scenarios with sorted keys and SHA-256. The digest is the scenario ID. Store one immutable report and trace digest per scenario. Existing POST /v1/run stays compatible; catalog routes exist only when configured. | ||
|
|
||
| ## SQLite schema | ||
|
|
||
| - scenarios(scenario_id, canonical_json, definition_bytes) | ||
| - runs(run_id, scenario_id, report_json, trace_sha256, processed_events, converged) | ||
| - schema_migrations(version, applied_by) | ||
|
|
||
| IDs are 64 lowercase hex characters, JSON is checked, and reports reference scenarios. | ||
|
|
||
| ## Implementation tasks | ||
|
|
||
| ### Task 1: Extract pure scenario execution | ||
|
|
||
| **Files:** Create src/scenario.ts and test/scenario.test.ts; modify src/app.ts. | ||
|
|
||
| - [x] Add failing tests for identity across key order and byte-identical execution. | ||
|
|
||
| ### Task 2: Implement the catalog | ||
|
|
||
| **Files:** Create schema/sqlite/001_catalog.sql, src/catalog.ts, and test/catalog.test.ts. | ||
|
|
||
| - [x] Test migration idempotence, immutable replay, hash rejection, foreign keys, reopen, and integrity checks. | ||
| - [x] Use prepared statements and disable extensions. | ||
|
|
||
| ### Task 3: Add catalog routes | ||
|
|
||
| **Files:** Modify src/app.ts, src/server.ts, and test/app.test.ts. | ||
|
|
||
| - [x] Add create, fetch, and run receipt routes without changing stateless limits. | ||
|
|
||
| ### Task 4: Document recovery | ||
|
|
||
| **Files:** Modify README.md, docs/test-contract.md, and docs/threat-model.md; create docs/data-model.md. | ||
|
|
||
| - [x] Document WAL-aware backup and canonical JSON export rollback. | ||
|
|
||
| ## Risk and rollback | ||
|
|
||
| - Medium: isolate the active-development Node SQLite API in one adapter. | ||
| - Medium: version canonicalization and pin it with fixtures. | ||
| - Low: fail startup on integrity or foreign-key errors. | ||
| - Rollback: omit CAUSAL_LAB_DB; stateless execution remains unchanged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| CREATE TABLE IF NOT EXISTS schema_migrations ( | ||
| version INTEGER PRIMARY KEY CHECK (version > 0), | ||
| applied_by TEXT NOT NULL CHECK (applied_by = 'causal-lab') | ||
| ) STRICT; | ||
|
|
||
| CREATE TABLE IF NOT EXISTS scenarios ( | ||
| scenario_id TEXT PRIMARY KEY CHECK ( | ||
| length(scenario_id) = 64 AND scenario_id NOT GLOB '*[^0-9a-f]*' | ||
| ), | ||
| canonicalization TEXT NOT NULL CHECK (canonicalization = 'causal-lab-json-v1'), | ||
| canonical_json TEXT NOT NULL CHECK (json_valid(canonical_json)), | ||
| definition_bytes INTEGER NOT NULL CHECK (definition_bytes > 0 AND definition_bytes <= 2097152) | ||
| ) STRICT, WITHOUT ROWID; | ||
|
|
||
| CREATE TABLE IF NOT EXISTS runs ( | ||
| run_id TEXT PRIMARY KEY CHECK ( | ||
| length(run_id) = 64 AND run_id NOT GLOB '*[^0-9a-f]*' | ||
| ), | ||
| scenario_id TEXT NOT NULL, | ||
| report_json TEXT NOT NULL CHECK (json_valid(report_json)), | ||
| trace_sha256 TEXT NOT NULL CHECK ( | ||
| length(trace_sha256) = 64 AND trace_sha256 NOT GLOB '*[^0-9a-f]*' | ||
| ), | ||
| processed_events INTEGER NOT NULL CHECK (processed_events >= 0), | ||
| converged INTEGER NOT NULL CHECK (converged IN (0, 1)), | ||
| FOREIGN KEY (scenario_id) REFERENCES scenarios(scenario_id) ON DELETE RESTRICT | ||
| ) STRICT, WITHOUT ROWID; | ||
|
|
||
| CREATE INDEX IF NOT EXISTS runs_by_scenario ON runs(scenario_id, run_id); | ||
|
|
||
| CREATE TRIGGER IF NOT EXISTS scenarios_immutable_update | ||
| BEFORE UPDATE ON scenarios BEGIN | ||
| SELECT RAISE(ABORT, 'scenarios are immutable'); | ||
| END; | ||
|
|
||
| CREATE TRIGGER IF NOT EXISTS scenarios_immutable_delete | ||
| BEFORE DELETE ON scenarios BEGIN | ||
| SELECT RAISE(ABORT, 'scenarios are immutable'); | ||
| END; | ||
|
|
||
| CREATE TRIGGER IF NOT EXISTS runs_immutable_update | ||
| BEFORE UPDATE ON runs BEGIN | ||
| SELECT RAISE(ABORT, 'runs are immutable'); | ||
| END; | ||
|
|
||
| CREATE TRIGGER IF NOT EXISTS runs_immutable_delete | ||
| BEFORE DELETE ON runs BEGIN | ||
| SELECT RAISE(ABORT, 'runs are immutable'); | ||
| END; | ||
|
|
||
| INSERT OR IGNORE INTO schema_migrations(version, applied_by) VALUES (1, 'causal-lab'); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These immutable triggers only cover explicit UPDATE and DELETE statements. With SQLite's default
recursive_triggerssetting, a maintenance/import path that usesINSERT OR REPLACEon an existing primary key can delete and reinsert the row without firing this trigger, so catalog rows are still mutable at the database layer despite the schema contract; add a duplicate-keyBEFORE INSERTguard or enable recursive triggers before relying on these triggers.Useful? React with 👍 / 👎.