# Road Revision Storage ## 1. Scope / Trigger `src/compile/road-revisions.js` owns the persistent v2 workbench layout. It is called after a successful import and when an existing workbench area is read, so legacy `import-*` directories migrate lazily without an administrative command. ## 2. Signatures ```js ensureRevisionStore(workspace) // -> { paths, active, baseline } setActiveAreaConfig(workspace, options) // -> { paths, areaConfig } readAreaConfigSnapshot(file) // -> compiler options createCheckpoint(workspace, label) // -> restored revision readRevision(workspace, revisionId) // -> { manifest, osm, areaConfig, nativeRoadOverrides, directEdits, trafficSignals } ``` ## 3. Contracts - Legacy inputs remain at `source.osm`, `native-road-overrides.json`, and `native-traffic-signals.json`; migration must never rewrite them. - OSM bytes are stored once under `osm/.osm`. - `active/native-road-edits.json` starts as a validated v2 document at version `0`; `active/state.json` points to the active revision. - The active config is `{ schema: 'road-workbench-area-config/v1', options }` at `active/area-config.snapshot.json`. Updating it must also update `base.areaConfigSha256` through a versioned v2 document write. - A revision is immutable at `revisions/rev-NNNN/`. Its `manifest.json` uses `road-workbench-revision/v1`, references the content-addressed OSM and frozen `area-config.snapshot.json`, lists frozen JSON documents, and records SHA-256 digests. - `compileInput()` uses `areaConfigSnapshotFile` when present. Its external `options` are ignored in that mode; a missing or invalid snapshot is an error, never a fallback. ## 4. Validation And Error Matrix | Condition | Error / behavior | | --- | --- | | Required legacy input missing | `Revision source is missing: ` | | Snapshot file is missing | `Area config snapshot is missing: ` | | Snapshot schema or options are invalid | `Invalid area config snapshot: ` | | Content-addressed OSM differs from its filename digest | `Content-addressed OSM is corrupt` | | Revision manifest schema or ID mismatch | `Invalid revision manifest` | | Revision ID is not `rev-NNNN` | Reject with `Invalid revision id` | | Manifest path leaves its OSM or revision directory | Reject the read; never resolve an external path | | Referenced frozen OSM or JSON digest mismatch | Reject the read with a digest mismatch error | | Empty checkpoint label | Reject with `label must be a non-empty string` | ## 5. Good / Base / Bad Cases - Good: first open of a legacy directory adds `osm/`, `active/`, and `revisions/rev-0001/`, leaving old files byte-identical. - Base: opening an already migrated directory is idempotent; an interrupted migration with a baseline but no active state repairs only the missing state file. - Bad: mutating a frozen revision document, config snapshot, or referenced OSM must make `readRevision()` fail rather than returning altered input. ## 6. Tests Required `test/road-revisions.js` must cover: - legacy-byte preservation and baseline creation; - equal OSM bytes yielding one content-addressed copy; - named checkpoint creation, parent linkage, and complete restore; - manifest digest equality with the actual frozen files, including area config; - active config writes incrementing `documentVersion` and preserving old revision snapshots; - compiler snapshot precedence and an explicit missing-snapshot error; - invalid checkpoint labels. `test/fixtures.js` compares normalized current compiler outputs with the checked-in `native-road-package/v1.1` fixture baselines. Regenerate them only through `npm run test:fixtures:update-baseline` after an intentional compiler output change. ## 7. Wrong Vs Correct ### Wrong ```js fs.copyFileSync(sourceOsm, path.join(revisionDirectory, 'source.osm')); ``` This duplicates equal OSM files and provides no integrity check. ### Correct ```js const { file, digest } = storeOsm(paths); manifest.source = { osmFile: path.relative(paths.workspace, file), osmSha256: digest }; ``` The content address is both the deduplication key and the integrity contract. ### Wrong ```js compileInput({ ...input, options: externalOptions }); ``` This allows later external config changes to alter a frozen revision. ### Correct ```js compileInput({ ...input, areaConfigSnapshotFile: revisionSnapshot }); ``` The compiler validates and consumes the frozen snapshot exclusively.