feat: compile revisions from area config snapshots
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
|
||||
```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, nativeRoadOverrides, directEdits, trafficSignals }
|
||||
readRevision(workspace, revisionId) // -> { manifest, osm, areaConfig, nativeRoadOverrides, directEdits, trafficSignals }
|
||||
```
|
||||
|
||||
## 3. Contracts
|
||||
@@ -17,14 +19,17 @@ readRevision(workspace, revisionId) // -> { manifest, osm, nativeRoadOverrides,
|
||||
- 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/<sha256>.osm`.
|
||||
- `active/native-road-edits.json` starts as a validated v2 document at version `0`; `active/state.json` points to the active revision.
|
||||
- A revision is immutable at `revisions/rev-NNNN/`. Its `manifest.json` uses `road-workbench-revision/v1`, references the content-addressed OSM, lists frozen JSON documents, and records SHA-256 digests.
|
||||
- Area-config snapshots are intentionally owned by the subsequent snapshot step. Do not silently fall back to the external config when that step is introduced.
|
||||
- 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: <path>` |
|
||||
| Snapshot file is missing | `Area config snapshot is missing: <path>` |
|
||||
| Snapshot schema or options are invalid | `Invalid area config snapshot: <path>` |
|
||||
| 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` |
|
||||
@@ -36,7 +41,7 @@ readRevision(workspace, revisionId) // -> { manifest, osm, nativeRoadOverrides,
|
||||
|
||||
- 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 or its referenced OSM must make `readRevision()` fail rather than returning altered input.
|
||||
- Bad: mutating a frozen revision document, config snapshot, or referenced OSM must make `readRevision()` fail rather than returning altered input.
|
||||
|
||||
## 6. Tests Required
|
||||
|
||||
@@ -45,9 +50,13 @@ readRevision(workspace, revisionId) // -> { manifest, osm, nativeRoadOverrides,
|
||||
- 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;
|
||||
- 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
|
||||
@@ -66,3 +75,19 @@ manifest.source = { osmFile: path.relative(paths.workspace, file), osmSha256: di
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/fixtures.js",
|
||||
"test:fixtures:update-baseline": "node test/update-fixture-baselines.js",
|
||||
"road:workbench": "node bin/road-workbench.js",
|
||||
"road:export": "node bin/road-compiler.js",
|
||||
"build": "vite build --config workbench/client/vite.config.ts",
|
||||
|
||||
@@ -12,13 +12,16 @@ const {
|
||||
} = require('./native-road');
|
||||
const { LAYER_REGISTRY, manifestForArea, validatePublishedLayers } = require('./layer-manifest');
|
||||
const { loadOrGenerate, runtime } = require('../native-traffic-signals');
|
||||
const { readAreaConfigSnapshot } = require('./road-revisions');
|
||||
|
||||
function compileInput(input) {
|
||||
validateInput(input);
|
||||
const options = input.areaConfigSnapshotFile ? readAreaConfigSnapshot(input.areaConfigSnapshotFile) : input.options;
|
||||
validateOptions(options);
|
||||
const area = {
|
||||
id: input.areaId,
|
||||
input: input.osmFile,
|
||||
nativeRoad: input.options,
|
||||
nativeRoad: options,
|
||||
outputs: {
|
||||
nativeRoadOverrides: input.overridesFile,
|
||||
nativeTrafficSignals: input.trafficSignalsFile,
|
||||
@@ -161,12 +164,18 @@ function validateInput(input) {
|
||||
if (typeof input[key] !== 'string' || input[key].trim() === '')
|
||||
throw new Error(`RoadCompilerInput.${key} must be a non-empty string`);
|
||||
}
|
||||
if (!input.options || typeof input.options !== 'object')
|
||||
throw new Error('RoadCompilerInput.options must be an object');
|
||||
if (typeof input.options.edgeLines !== 'boolean')
|
||||
throw new Error('RoadCompilerInput.options.edgeLines must be a boolean');
|
||||
if (!input.options.junctionTemplates || typeof input.options.junctionTemplates !== 'object')
|
||||
if (
|
||||
input.areaConfigSnapshotFile !== undefined &&
|
||||
(typeof input.areaConfigSnapshotFile !== 'string' || !input.areaConfigSnapshotFile)
|
||||
)
|
||||
throw new Error('RoadCompilerInput.areaConfigSnapshotFile must be a non-empty string when present');
|
||||
}
|
||||
|
||||
function validateOptions(options) {
|
||||
if (!options || typeof options !== 'object') throw new Error('RoadCompilerInput.options must be an object');
|
||||
if (typeof options.edgeLines !== 'boolean') throw new Error('RoadCompilerInput.options.edgeLines must be a boolean');
|
||||
if (!options.junctionTemplates || typeof options.junctionTemplates !== 'object')
|
||||
throw new Error('RoadCompilerInput.options.junctionTemplates must be an object');
|
||||
}
|
||||
|
||||
module.exports = { compileInput, validateInput };
|
||||
module.exports = { compileInput, validateInput, validateOptions };
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { emptyEditDocument, validateEditDocument } = require('./native-road-edits');
|
||||
const { emptyEditDocument, loadEditDocument, saveEditDocument, validateEditDocument } = require('./native-road-edits');
|
||||
|
||||
const REVISION_SCHEMA = 'road-workbench-revision/v1';
|
||||
const ACTIVE_STATE_SCHEMA = 'road-workbench-active/v1';
|
||||
const AREA_CONFIG_SNAPSHOT_SCHEMA = 'road-workbench-area-config/v1';
|
||||
const PACKAGE_VERSION = require('../../package.json').version;
|
||||
const GEOMETRY_SCHEMA = 'native-road-package/v1.1';
|
||||
|
||||
@@ -39,6 +40,7 @@ function pathsFor(workspace) {
|
||||
osmDirectory: path.join(workspace, 'osm'),
|
||||
activeDirectory: path.join(workspace, 'active'),
|
||||
activeEdits: path.join(workspace, 'active', 'native-road-edits.json'),
|
||||
activeAreaConfig: path.join(workspace, 'active', 'area-config.snapshot.json'),
|
||||
activeState: path.join(workspace, 'active', 'state.json'),
|
||||
revisionsDirectory: path.join(workspace, 'revisions'),
|
||||
};
|
||||
@@ -68,18 +70,71 @@ function revisionId(revisionsDirectory) {
|
||||
return `rev-${String(Math.max(0, ...numbers) + 1).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
function activeDocument(paths, osmSha256) {
|
||||
if (fs.existsSync(paths.activeEdits)) return validateEditDocument(readJson(paths.activeEdits));
|
||||
const document = emptyEditDocument({ osmSha256, compilerGeometryVersion: GEOMETRY_SCHEMA });
|
||||
function activeDocument(paths, osmSha256, areaConfigSha256) {
|
||||
if (fs.existsSync(paths.activeEdits)) {
|
||||
const document = validateEditDocument(readJson(paths.activeEdits));
|
||||
if (areaConfigSha256 && document.base.areaConfigSha256 !== areaConfigSha256) {
|
||||
document.base.areaConfigSha256 = areaConfigSha256;
|
||||
writeJsonAtomic(paths.activeEdits, document);
|
||||
}
|
||||
return document;
|
||||
}
|
||||
const document = emptyEditDocument({ osmSha256, areaConfigSha256, compilerGeometryVersion: GEOMETRY_SCHEMA });
|
||||
writeJsonAtomic(paths.activeEdits, document);
|
||||
return document;
|
||||
}
|
||||
|
||||
function ensureRevisionStore(workspace) {
|
||||
function writeActiveAreaConfig(paths, options) {
|
||||
if (!options || typeof options !== 'object') throw new Error('Area config options must be an object.');
|
||||
const snapshot = { schema: AREA_CONFIG_SNAPSHOT_SCHEMA, options };
|
||||
const bytes = `${JSON.stringify(snapshot, null, 2)}\n`;
|
||||
writeFileAtomic(paths.activeAreaConfig, bytes);
|
||||
return { bytes, options, digest: sha256(bytes) };
|
||||
}
|
||||
|
||||
function setActiveAreaConfig(workspace, options) {
|
||||
const paths = pathsFor(workspace);
|
||||
const areaConfig = writeActiveAreaConfig(paths, options);
|
||||
if (fs.existsSync(paths.activeEdits)) {
|
||||
const document = loadEditDocument(paths.activeEdits);
|
||||
const saved = saveEditDocument(paths.activeEdits, {
|
||||
...document,
|
||||
base: { ...document.base, areaConfigSha256: areaConfig.digest },
|
||||
});
|
||||
if (fs.existsSync(paths.activeState)) {
|
||||
writeJsonAtomic(paths.activeState, { ...readJson(paths.activeState), documentVersion: saved.documentVersion });
|
||||
}
|
||||
}
|
||||
return { paths, areaConfig };
|
||||
}
|
||||
|
||||
function readAreaConfigSnapshot(file) {
|
||||
if (!fs.existsSync(file)) throw new Error(`Area config snapshot is missing: ${file}`);
|
||||
const snapshot = readJson(file);
|
||||
if (
|
||||
!snapshot ||
|
||||
snapshot.schema !== AREA_CONFIG_SNAPSHOT_SCHEMA ||
|
||||
!snapshot.options ||
|
||||
typeof snapshot.options !== 'object'
|
||||
)
|
||||
throw new Error(`Invalid area config snapshot: ${file}`);
|
||||
return snapshot.options;
|
||||
}
|
||||
|
||||
function activeAreaConfig(paths) {
|
||||
const bytes = readFile(paths.activeAreaConfig);
|
||||
return { bytes, options: readAreaConfigSnapshot(paths.activeAreaConfig), digest: sha256(bytes) };
|
||||
}
|
||||
|
||||
function ensureRevisionStore(workspace, options) {
|
||||
const paths = pathsFor(workspace);
|
||||
const osm = storeOsm(paths);
|
||||
const edits = activeDocument(paths, osm.digest);
|
||||
const snapshot = fs.existsSync(paths.activeAreaConfig)
|
||||
? activeAreaConfig(paths)
|
||||
: writeActiveAreaConfig(paths, options);
|
||||
const edits = activeDocument(paths, osm.digest, snapshot.digest);
|
||||
if (fs.existsSync(path.join(paths.revisionsDirectory, 'rev-0001', 'manifest.json'))) {
|
||||
backfillAreaConfigSnapshots(paths, snapshot);
|
||||
const baseline = readRevision(workspace, 'rev-0001');
|
||||
const active = fs.existsSync(paths.activeState)
|
||||
? readJson(paths.activeState)
|
||||
@@ -94,6 +149,7 @@ function ensureRevisionStore(workspace) {
|
||||
const baseline = createRevision(paths, {
|
||||
id: 'rev-0001',
|
||||
directEdits: edits,
|
||||
areaConfig: snapshot,
|
||||
label: 'Import baseline',
|
||||
});
|
||||
writeJsonAtomic(paths.activeState, {
|
||||
@@ -104,14 +160,44 @@ function ensureRevisionStore(workspace) {
|
||||
return { paths, active: readJson(paths.activeState), baseline };
|
||||
}
|
||||
|
||||
function backfillAreaConfigSnapshots(paths, areaConfig) {
|
||||
if (!fs.existsSync(paths.revisionsDirectory)) return;
|
||||
for (const entry of fs.readdirSync(paths.revisionsDirectory, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || !/^rev-\d{4}$/.test(entry.name)) continue;
|
||||
const directory = path.join(paths.revisionsDirectory, entry.name);
|
||||
const manifestFile = path.join(directory, 'manifest.json');
|
||||
if (!fs.existsSync(manifestFile)) continue;
|
||||
const manifest = readJson(manifestFile);
|
||||
if (manifest.source?.areaConfigFile && manifest.source?.areaConfigSha256) continue;
|
||||
const directEditsFile = path.join(directory, 'native-road-edits.json');
|
||||
const directEdits = validateEditDocument(readJson(directEditsFile));
|
||||
directEdits.base.areaConfigSha256 = areaConfig.digest;
|
||||
writeJsonAtomic(directEditsFile, directEdits);
|
||||
writeFileAtomic(path.join(directory, 'area-config.snapshot.json'), areaConfig.bytes);
|
||||
manifest.source = {
|
||||
...manifest.source,
|
||||
areaConfigFile: 'area-config.snapshot.json',
|
||||
areaConfigSha256: areaConfig.digest,
|
||||
};
|
||||
manifest.digests = {
|
||||
...manifest.digests,
|
||||
directEdits: sha256(`${JSON.stringify(directEdits, null, 2)}\n`),
|
||||
areaConfigSnapshot: areaConfig.digest,
|
||||
};
|
||||
writeJsonAtomic(manifestFile, manifest);
|
||||
}
|
||||
}
|
||||
|
||||
function createCheckpoint(workspace, label) {
|
||||
if (typeof label !== 'string' || !label.trim())
|
||||
throw new Error('Revision checkpoint label must be a non-empty string.');
|
||||
const initialized = ensureRevisionStore(workspace);
|
||||
const directEdits = validateEditDocument(readJson(initialized.paths.activeEdits));
|
||||
const areaConfig = activeAreaConfig(initialized.paths);
|
||||
const revision = createRevision(initialized.paths, {
|
||||
id: revisionId(initialized.paths.revisionsDirectory),
|
||||
directEdits,
|
||||
areaConfig,
|
||||
label: label.trim(),
|
||||
parentRevisionId: initialized.active.activeRevisionId,
|
||||
});
|
||||
@@ -123,7 +209,7 @@ function createCheckpoint(workspace, label) {
|
||||
return revision;
|
||||
}
|
||||
|
||||
function createRevision(paths, { id, directEdits, label, parentRevisionId = undefined }) {
|
||||
function createRevision(paths, { id, directEdits, areaConfig, label, parentRevisionId = undefined }) {
|
||||
assertRevisionId(id);
|
||||
const osm = storeOsm(paths);
|
||||
const overrides = readFile(paths.overrides);
|
||||
@@ -142,6 +228,7 @@ function createRevision(paths, { id, directEdits, label, parentRevisionId = unde
|
||||
writeFileAtomic(path.join(staging, files.nativeRoadOverrides), overrides);
|
||||
writeJsonAtomic(path.join(staging, files.directEdits), directEdits);
|
||||
writeFileAtomic(path.join(staging, files.trafficSignals), trafficSignals);
|
||||
writeFileAtomic(path.join(staging, 'area-config.snapshot.json'), areaConfig.bytes);
|
||||
const manifest = {
|
||||
schema: REVISION_SCHEMA,
|
||||
id,
|
||||
@@ -151,12 +238,15 @@ function createRevision(paths, { id, directEdits, label, parentRevisionId = unde
|
||||
source: {
|
||||
osmFile: path.relative(paths.workspace, osm.file),
|
||||
osmSha256: osm.digest,
|
||||
areaConfigFile: 'area-config.snapshot.json',
|
||||
areaConfigSha256: areaConfig.digest,
|
||||
},
|
||||
documents: files,
|
||||
digests: {
|
||||
nativeRoadOverrides: sha256(overrides),
|
||||
directEdits: sha256(`${JSON.stringify(directEdits, null, 2)}\n`),
|
||||
trafficSignals: sha256(trafficSignals),
|
||||
areaConfigSnapshot: areaConfig.digest,
|
||||
},
|
||||
compiler: { packageVersion: PACKAGE_VERSION, geometrySchema: GEOMETRY_SCHEMA },
|
||||
};
|
||||
@@ -180,6 +270,11 @@ function readRevision(workspace, id) {
|
||||
const osm = readFile(osmFile);
|
||||
if (sha256(osm) !== manifest.source.osmSha256)
|
||||
throw new Error(`Revision ${id} OSM digest does not match its manifest.`);
|
||||
const areaConfigFile = path.resolve(directory, manifest.source.areaConfigFile);
|
||||
if (!isWithin(directory, areaConfigFile)) throw new Error(`Revision ${id} area config path escapes the revision.`);
|
||||
const areaConfigBytes = readFile(areaConfigFile);
|
||||
if (sha256(areaConfigBytes) !== manifest.source.areaConfigSha256)
|
||||
throw new Error(`Revision ${id} area config digest does not match its manifest.`);
|
||||
const documents = Object.fromEntries(
|
||||
Object.entries(manifest.documents).map(([name, file]) => {
|
||||
const document = path.resolve(directory, file);
|
||||
@@ -190,7 +285,7 @@ function readRevision(workspace, id) {
|
||||
return [name, JSON.parse(bytes.toString('utf8'))];
|
||||
}),
|
||||
);
|
||||
return { manifest, osm: osm.toString('utf8'), ...documents };
|
||||
return { manifest, osm: osm.toString('utf8'), areaConfig: readAreaConfigSnapshot(areaConfigFile), ...documents };
|
||||
}
|
||||
|
||||
function assertRevisionId(id) {
|
||||
@@ -209,8 +304,11 @@ function readJson(file) {
|
||||
module.exports = {
|
||||
REVISION_SCHEMA,
|
||||
ACTIVE_STATE_SCHEMA,
|
||||
AREA_CONFIG_SNAPSHOT_SCHEMA,
|
||||
sha256,
|
||||
ensureRevisionStore,
|
||||
setActiveAreaConfig,
|
||||
readAreaConfigSnapshot,
|
||||
createCheckpoint,
|
||||
readRevision,
|
||||
};
|
||||
|
||||
@@ -1,105 +1,78 @@
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"contract": "native-road-package/v1.1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"files": {
|
||||
"../native-traffic-signals.json": {
|
||||
"contentHash": "88e7f0ef4fc7bdb9ef202af018b3fb3179035ffc8ed5eeba03276c263e1f0476",
|
||||
"bytes": 5886
|
||||
"contentHash": "ad4a30a53be21381c7d203093f2ab1e55b444f01c1bf83388321f130cf6702d6",
|
||||
"bytes": 3819
|
||||
},
|
||||
"comparison.json": {
|
||||
"contentHash": "651eb4000a44a7521a79c3e1429795b83d20f8899e0c67c4bb25b56cf4a6f75d",
|
||||
"bytes": 1295
|
||||
"contentHash": "a08cdd67cf9a0a2fde8892dccc35f1a5c6b63e8bdda2aaec1774d36370fc6300",
|
||||
"bytes": 1045
|
||||
},
|
||||
"compiled.json": {
|
||||
"contentHash": "2ab82eafcdab70155078fae3559692ef1fde0645a52c6decbaea762a9ffeb24a",
|
||||
"bytes": 159230
|
||||
"contentHash": "3f1bc0dd16fd3cd566716ba034095200518f2ada5f02619667520fd3024e572a",
|
||||
"bytes": 126674
|
||||
},
|
||||
"diagnostics.json": {
|
||||
"contentHash": "1d9fdae06ddcbc19b5262b797a4a35c494fc30a98a66dbfa111533b12c5fdfc7",
|
||||
"bytes": 6862
|
||||
"contentHash": "9ecc0aceaceb65c5c0cdb04a1389169f147c2ff193584838acbb1717abf06912",
|
||||
"bytes": 6261
|
||||
},
|
||||
"layers/center_lines.geojson": {
|
||||
"contentHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
|
||||
"orderHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
|
||||
"features": 5,
|
||||
"bytes": 6909
|
||||
"contentHash": "dd2d659415c0d2c0a0872bb658153c74cf3578384cbce03055abbd16c5c22249",
|
||||
"bytes": 4152
|
||||
},
|
||||
"layers/connectors.geojson": {
|
||||
"contentHash": "e7979d4642c7c10f33fb23e92721f22533a851ae1a2b4d35f224d50927bf2fe9",
|
||||
"orderHash": "c327b7b154d4d9ca04cd895523eb0d8ad67c37051ef1d2135b2b46c51e12df58",
|
||||
"features": 90,
|
||||
"bytes": 197885
|
||||
"contentHash": "e829361a9223676dc27bda1719910fdcf82e125f4eaec53c3d348a2d328812ce",
|
||||
"bytes": 124749
|
||||
},
|
||||
"layers/crosswalks.geojson": {
|
||||
"contentHash": "354cd4d7ed105a11b95904b44848dce567b3a3383f76755936d8c00691fe29af",
|
||||
"orderHash": "d24376438ac956bde52fcbfa955dd72c2ba31a97995608aaed1c79f6fd3633ab",
|
||||
"features": 152,
|
||||
"bytes": 219286
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"layers/direction_arrows.geojson": {
|
||||
"contentHash": "7b8362ad4912c997a7661befde55ddfe58757c3c8d139c15867e1938ea8f6918",
|
||||
"orderHash": "5274e8b433946410a7cdffdaa452c1750e04f53e73a86bf81d034cfb26d4c254",
|
||||
"features": 686,
|
||||
"bytes": 1053308
|
||||
"contentHash": "2baaf72199630df794c2551c900ff3c478b294554c1f31e7f14f8131d43df7d3",
|
||||
"bytes": 567748
|
||||
},
|
||||
"layers/edge_lines.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"layers/intersection_surface.geojson": {
|
||||
"contentHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
|
||||
"orderHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
|
||||
"features": 5,
|
||||
"bytes": 13048
|
||||
"contentHash": "8e46c1f1594022010a123fc69632100f7bd7d09e33e9decad5d53e9b5736fd2d",
|
||||
"bytes": 14887
|
||||
},
|
||||
"layers/lane_centerlines.geojson": {
|
||||
"contentHash": "19c550c794810c1071de55dee158295235106a56e3877a2ba10da920ee214593",
|
||||
"orderHash": "67f8e7d33ec578644416e4837dd4aafef62a55aaeb4d438d07ab07eddfbb1df1",
|
||||
"features": 84,
|
||||
"bytes": 73294
|
||||
"contentHash": "a768fc2382a5eb2c35b19a34836a5f74d9957b035a3c33bb21aa1ee4899c7191",
|
||||
"bytes": 32242
|
||||
},
|
||||
"layers/lane_separators.geojson": {
|
||||
"contentHash": "02fdf2c7b6e89c2850a60ea2ac72b9f62910259bdb2949681fcaa7e67d2b51f0",
|
||||
"orderHash": "81782a059c4ff9f69fdf7de9107d20a8d6cf55d34bb0620ec28a9a8c5106c982",
|
||||
"features": 3596,
|
||||
"bytes": 4164106
|
||||
"contentHash": "32fd4ff71195e48e5b087fba388b5caf9f51de40ecebbabc556d04858e2c912e",
|
||||
"bytes": 2367951
|
||||
},
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "5a2936a705e64338d0d79b395a13d761f1893dd72330a9b9ee4696ae75eab295",
|
||||
"orderHash": "d8402ae6e772e0778b862b190abfa291de1b02a15537d455f7dcd8596245dfd8",
|
||||
"features": 30,
|
||||
"bytes": 49554
|
||||
"contentHash": "880f4c338eceb112364e25738f94572c0c3b4aff68a53da319ef76bbda2cba61",
|
||||
"bytes": 17464
|
||||
},
|
||||
"layers/sidewalk_surface.geojson": {
|
||||
"contentHash": "b6f8f876e9f18fd2ef6749125a09d654c73d986e916bd262ceb9967bca1a54c6",
|
||||
"orderHash": "3fd42a67e18e3b8743fa538428e039ef66a1f44150a6881a7aaa18326607b5cb",
|
||||
"features": 26,
|
||||
"bytes": 54997
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"layers/turn_arrows.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"layers/vehicle_stop_lines.geojson": {
|
||||
"contentHash": "cb904550ab8eec011cf33e5edfa81044de6dfca0280dbfe488e77229055ed590",
|
||||
"orderHash": "8a4b877a17a5af60adc8656465afad6756944736291664139e878d46c5a4f363",
|
||||
"features": 4,
|
||||
"bytes": 4191
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"traffic-signal-assemblies.json": {
|
||||
"contentHash": "1788026a0c8c531b2fc7dd49628f7fbadd809c7b2f7f8819cffb3ac7d30689ca",
|
||||
"orderHash": "b409ea6b77b6c8639f63c33a156581f9b184a84fde34d265b5deb05705828251",
|
||||
"features": 7,
|
||||
"bytes": 5411
|
||||
"contentHash": "51ce257c59336d4901b478cba30facb55a284d61695ceedb22fb3a8ea881dde3",
|
||||
"bytes": 3729
|
||||
},
|
||||
"traffic-signals.json": {
|
||||
"contentHash": "625b79b6eb82b41f2099ba51960717c7f794fdfbd87310eb12e99da966540d89",
|
||||
"bytes": 15491
|
||||
"contentHash": "8f5a6007bce6effa49bdc613faaecbe6be2bc9f4a1bbdd5f68f8934da53587ca",
|
||||
"bytes": 10068
|
||||
}
|
||||
}
|
||||
},
|
||||
"volatileExcluded": ["absolute paths -> <repo> or <external>", "native-road-* staging directory -> <staging>"]
|
||||
}
|
||||
|
||||
@@ -1,105 +1,78 @@
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"contract": "native-road-package/v1.1",
|
||||
"areaId": "nantaizi-lake-innovation-valley",
|
||||
"files": {
|
||||
"../native-traffic-signals.json": {
|
||||
"contentHash": "36cb9f069fb44a6e24f0af0d130ef8cb5b4a93250dc9fb0891191d1ba039a5b2",
|
||||
"bytes": 28329
|
||||
"contentHash": "35a57a62f211a6f6a3b38e0a378eddd63cb38fa9807c409d1d3c5e52c70c0d6f",
|
||||
"bytes": 18111
|
||||
},
|
||||
"comparison.json": {
|
||||
"contentHash": "5aa11da443221a48288641ac6950c6e4d75d834713a93110bbf265859202a265",
|
||||
"bytes": 1153
|
||||
"contentHash": "d8d4fc7d72527fce08c766f1619777a69813080a2def35652c402edd55a64a2e",
|
||||
"bytes": 1015
|
||||
},
|
||||
"compiled.json": {
|
||||
"contentHash": "ea5ae4a04f1259b1ccec9fc1cca7ef8ba81426a77584a447480b6047b37f5e01",
|
||||
"bytes": 184985
|
||||
"contentHash": "ba7c97c7117b882e798185e3f023b8358ff0330e9ce8fe38ee6c8d88dbbda0b6",
|
||||
"bytes": 135855
|
||||
},
|
||||
"diagnostics.json": {
|
||||
"contentHash": "7e9327f0424dd612f389c560c114a4cafee1349bbfa36f49b2b4eab846e5ca91",
|
||||
"bytes": 9755
|
||||
"contentHash": "05b09f50df213c162978941b5f771c970e9928d73d4f9041c4209db8ffd5d579",
|
||||
"bytes": 7037
|
||||
},
|
||||
"layers/center_lines.geojson": {
|
||||
"contentHash": "2b2710fce06e75b0a3e4941c2da6119cea7b8bb7834345607bbee384de20ee6b",
|
||||
"orderHash": "ea71a8841370689d6fc2b75d0b30ec7104638fb2904da6d707dc82ff3131e065",
|
||||
"features": 511,
|
||||
"bytes": 694102
|
||||
"contentHash": "04a2de8ac58ae27f10be9e2ee0c817b27f41e990caa65e626391cb1f3e1a18c8",
|
||||
"bytes": 413551
|
||||
},
|
||||
"layers/connectors.geojson": {
|
||||
"contentHash": "901a86bc6c536d38bb73ae4654ec1ce7d1c0790c2d6eba92543c48f584cd15ac",
|
||||
"orderHash": "4df882ad59b65a97d892df64de0156b35beaf071b04eed671ef7b535e9e2a7b9",
|
||||
"features": 76,
|
||||
"bytes": 160866
|
||||
"contentHash": "3647dfb768370d66b9b316a7a69356f6217233627804bea7e2566c27023eec05",
|
||||
"bytes": 99978
|
||||
},
|
||||
"layers/crosswalks.geojson": {
|
||||
"contentHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
|
||||
"orderHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
|
||||
"features": 48,
|
||||
"bytes": 50536
|
||||
"contentHash": "72079521c1f6891b77b226b87ee3656c113f3a72c4fb51981dfdaa9dafc4eeb2",
|
||||
"bytes": 28012
|
||||
},
|
||||
"layers/direction_arrows.geojson": {
|
||||
"contentHash": "38e80bbfe02d4f12ce40e7d0c8342e3bef6b602c64f81dd320b366b6a6b3d205",
|
||||
"orderHash": "579aa6c37585bf4edf9d6b6b1e120f976b93ec2d0c303d82a154b75c114a45f5",
|
||||
"features": 324,
|
||||
"bytes": 493431
|
||||
"contentHash": "5e60d48520fa09aa75a8aa9bb89c4701c7f5dabc4e5ce57c9dc87bdefbbf3d1c",
|
||||
"bytes": 270831
|
||||
},
|
||||
"layers/edge_lines.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
"contentHash": "6d18edfe75b3149104038dcbca4d0096bbcb923f0f9fa4b49f4815344e75bb90",
|
||||
"bytes": 43
|
||||
},
|
||||
"layers/intersection_surface.geojson": {
|
||||
"contentHash": "7f79e57215e3fd07c5c0f703c71e516873db770cb869efdf18925a3184059ca3",
|
||||
"orderHash": "d69df8d92d0fce8f7ee172959b978d941fd011e094857e9ab7304b3d1e98bf92",
|
||||
"features": 12,
|
||||
"bytes": 38528
|
||||
"contentHash": "d0620e5c1cda3fd46ca2a3c6484d1e51aa97e717b7326d118f3f2659e5d0dd95",
|
||||
"bytes": 18960
|
||||
},
|
||||
"layers/lane_centerlines.geojson": {
|
||||
"contentHash": "3fea4ebfe62e0d720c6977e2289f4e6517ed7b3c5ab361270d03378971aec780",
|
||||
"orderHash": "69a8ebaa42a74da4f7d5940c5a81bd82ceb104efd3f858723dccfadbae12121e",
|
||||
"features": 50,
|
||||
"bytes": 43582
|
||||
"contentHash": "6589ee8a94f106b05b426b320e78b86b86d390ab6c38fbc7b5bf8e28332509ed",
|
||||
"bytes": 26292
|
||||
},
|
||||
"layers/lane_separators.geojson": {
|
||||
"contentHash": "dc0b50b53da191e65f3dba01162acb7561966d1796a9c0789527cebfd6ed06a4",
|
||||
"orderHash": "0bfd873e5698869fb4855783fe30bb6db29456958e4d154b7264374a5fdb4a10",
|
||||
"features": 731,
|
||||
"bytes": 835933
|
||||
"contentHash": "545a1ed21674d7e6ead04c70eeb511ae59760f49b0a734b79d3ad01ef4f39382",
|
||||
"bytes": 471152
|
||||
},
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "81af19c71cf6fc8a3884488c00cfb590e1a7fe9ed9e4351fce65825ceadd50fa",
|
||||
"orderHash": "f2ddec5258411fbdbc050b7ecefbb157b59e00b8226afcc30d2b772e1859e357",
|
||||
"features": 25,
|
||||
"bytes": 38275
|
||||
"contentHash": "176e2dd1e7bc73687283eece17827d7aeaa0e3b0840a31583dd88b821a93f987",
|
||||
"bytes": 20244
|
||||
},
|
||||
"layers/sidewalk_surface.geojson": {
|
||||
"contentHash": "bb8f4516e27b16238d753b112d18b8acfa5cfad2cea522a65b7972a609be9563",
|
||||
"orderHash": "4fa773a6f39c5b6d23551e1338c6d99ef8a5ecd9ebfcafa16154c9765fef8c6e",
|
||||
"features": 73,
|
||||
"bytes": 122432
|
||||
"contentHash": "7ce873294a163973cfd4a4a439e3c55fb003e22985943165210268288fe8f690",
|
||||
"bytes": 32236
|
||||
},
|
||||
"layers/turn_arrows.geojson": {
|
||||
"contentHash": "a65bb8fe079f27e00bf2e6208148fc6b73d2d07e76d48b1cd1bf8a610d7e2d66",
|
||||
"orderHash": "66966ac2c65464f9c6715ca9cda5a3620520bf1d066cba2dee36114dc46af936",
|
||||
"features": 108,
|
||||
"bytes": 132536
|
||||
"contentHash": "599820effb9db9e497e8415f3191870baf1f1273aeb81cb4f0034f76c5ca6b40",
|
||||
"bytes": 75592
|
||||
},
|
||||
"layers/vehicle_stop_lines.geojson": {
|
||||
"contentHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
|
||||
"orderHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
|
||||
"features": 8,
|
||||
"bytes": 8475
|
||||
"contentHash": "c4cb8c3235fae7e2fd64306fe1b853d604ee1811286162f79f9ca8286b6a922f",
|
||||
"bytes": 4711
|
||||
},
|
||||
"traffic-signal-assemblies.json": {
|
||||
"contentHash": "9f8d032c00784bb72bb1322238cd7b0a80e621d435b43c728da86f1e77ca352a",
|
||||
"orderHash": "0b9cae13094dba1994faca4bf1d4d80e8bc5497daa46df5899bf5e6fc7f26d61",
|
||||
"features": 35,
|
||||
"bytes": 26398
|
||||
"contentHash": "14a66bd60cae387891dc6e29d8bdcbe4a021dc0c7f75dc3629cae6f25195959e",
|
||||
"bytes": 18021
|
||||
},
|
||||
"traffic-signals.json": {
|
||||
"contentHash": "c4b68cc1cefeb0d393a18eef6c7eb79933fba28630826745165ed433c25779d0",
|
||||
"bytes": 74151
|
||||
"contentHash": "f405a47d61c69ebd76f19339b54d468dfab39d7c7115417c6f790c26fc661c79",
|
||||
"bytes": 47678
|
||||
}
|
||||
}
|
||||
},
|
||||
"volatileExcluded": ["absolute paths -> <repo> or <external>", "native-road-* staging directory -> <staging>"]
|
||||
}
|
||||
|
||||
71
test/fixture-baseline.js
Normal file
71
test/fixture-baseline.js
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
function fixtureInputs() {
|
||||
return ['fengshu-er-road', 'nantaizi-lake-innovation-valley'].map((areaId) => {
|
||||
const isFengshu = areaId === 'fengshu-er-road';
|
||||
const outputRoot = path.join(root, 'outputs', areaId);
|
||||
return {
|
||||
areaId,
|
||||
osmFile: path.join(root, 'inputs', 'osm', isFengshu ? '枫树二路.osm' : '南台子湖创新谷OSM.osm'),
|
||||
outDir: path.join(outputRoot, 'native-road'),
|
||||
stagingDir: path.join(outputRoot, '_pipeline'),
|
||||
overridesFile: path.join(outputRoot, 'native-road-overrides.json'),
|
||||
trafficSignalsFile: path.join(outputRoot, 'native-traffic-signals.json'),
|
||||
comparisonDir: path.join(outputRoot, 'osm2streets_web_out'),
|
||||
options: { edgeLines: false, junctionTemplates: { enabled: false, references: [] } },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function digest(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function canonicalContent(file) {
|
||||
const raw = fs.readFileSync(file, 'utf8');
|
||||
if (!file.endsWith('.json') && !file.endsWith('.geojson')) return raw;
|
||||
return `${JSON.stringify(normalize(JSON.parse(raw)))}\n`;
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
if (Array.isArray(value)) return value.map(normalize);
|
||||
if (value && typeof value === 'object')
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalize(item)]));
|
||||
return typeof value === 'string' ? value.split(root).join('<repo>') : value;
|
||||
}
|
||||
|
||||
function snapshot(input) {
|
||||
const files = {};
|
||||
const candidates = [
|
||||
['../native-traffic-signals.json', input.trafficSignalsFile],
|
||||
...walk(input.outDir).filter(([relative]) => relative !== 'manifest.json'),
|
||||
];
|
||||
for (const [relative, file] of candidates) {
|
||||
const content = canonicalContent(file);
|
||||
files[relative] = { contentHash: digest(content), bytes: Buffer.byteLength(content) };
|
||||
}
|
||||
return { contract: 'native-road-package/v1.1', areaId: input.areaId, files };
|
||||
}
|
||||
|
||||
function walk(directory, prefix = '') {
|
||||
return fs
|
||||
.readdirSync(directory, { withFileTypes: true })
|
||||
.sort((first, second) => first.name.localeCompare(second.name))
|
||||
.flatMap((entry) => {
|
||||
const relative = path.join(prefix, entry.name);
|
||||
const file = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? walk(file, relative) : [[relative, file]];
|
||||
});
|
||||
}
|
||||
|
||||
function baselineFile(areaId) {
|
||||
return path.join(root, 'test', 'baseline', `${areaId}.json`);
|
||||
}
|
||||
|
||||
module.exports = { root, fixtureInputs, snapshot, baselineFile };
|
||||
@@ -8,25 +8,14 @@ const path = require('path');
|
||||
const { unzipSync, strFromU8 } = require('fflate');
|
||||
const { compiler } = require('../src');
|
||||
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
||||
const { root, fixtureInputs, snapshot, baselineFile } = require('./fixture-baseline');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
for (const areaId of ['fengshu-er-road', 'nantaizi-lake-innovation-valley']) {
|
||||
const isFengshu = areaId === 'fengshu-er-road';
|
||||
const outputRoot = path.join(root, 'outputs', areaId);
|
||||
const input = {
|
||||
areaId,
|
||||
osmFile: path.join(root, 'inputs', 'osm', isFengshu ? '枫树二路.osm' : '南台子湖创新谷OSM.osm'),
|
||||
outDir: path.join(outputRoot, 'native-road'),
|
||||
stagingDir: path.join(outputRoot, '_pipeline'),
|
||||
overridesFile: path.join(outputRoot, 'native-road-overrides.json'),
|
||||
trafficSignalsFile: path.join(outputRoot, 'native-traffic-signals.json'),
|
||||
comparisonDir: path.join(outputRoot, 'osm2streets_web_out'),
|
||||
options: { edgeLines: false, junctionTemplates: { enabled: false, references: [] } },
|
||||
};
|
||||
assert.ok(fs.existsSync(path.join(root, 'test', 'baseline', `${areaId}.json`)));
|
||||
for (const input of fixtureInputs()) {
|
||||
assert.ok(fs.existsSync(baselineFile(input.areaId)));
|
||||
const { result } = compiler.compileInput(input);
|
||||
assert.equal(result.areaId, areaId);
|
||||
assert.equal(result.areaId, input.areaId);
|
||||
assert.ok(fs.existsSync(path.join(input.outDir, 'compiled.json')));
|
||||
assert.deepEqual(snapshot(input), JSON.parse(fs.readFileSync(baselineFile(input.areaId), 'utf8')));
|
||||
const first = exportNativeRoadPackage(input.outDir);
|
||||
const second = exportNativeRoadPackage(input.outDir);
|
||||
assert.equal(
|
||||
@@ -35,7 +24,7 @@ for (const areaId of ['fengshu-er-road', 'nantaizi-lake-innovation-valley']) {
|
||||
);
|
||||
const entries = unzipSync(first.bytes);
|
||||
const manifest = JSON.parse(strFromU8(entries['manifest.json']));
|
||||
assert.equal(manifest.areaId, areaId);
|
||||
assert.equal(manifest.areaId, input.areaId);
|
||||
assert.equal(manifest.contract, 'native-road-package/v1.1');
|
||||
assert.deepEqual(manifest.generator, { name: 'road-compiler', version: '0.3.0' });
|
||||
assert.equal('source' in JSON.parse(strFromU8(entries['compiled.json'])), false);
|
||||
@@ -44,9 +33,9 @@ for (const areaId of ['fengshu-er-road', 'nantaizi-lake-innovation-valley']) {
|
||||
false,
|
||||
);
|
||||
assert.equal(Object.keys(entries).length, 18);
|
||||
if (isFengshu) {
|
||||
const inputFile = path.join(outputRoot, 'road-compiler-input.json');
|
||||
const archive = path.join(outputRoot, 'export.zip');
|
||||
if (input.areaId === 'fengshu-er-road') {
|
||||
const inputFile = path.join(path.dirname(input.outDir), 'road-compiler-input.json');
|
||||
const archive = path.join(path.dirname(input.outDir), 'export.zip');
|
||||
fs.writeFileSync(inputFile, `${JSON.stringify(input)}\n`);
|
||||
execFileSync(process.execPath, [
|
||||
path.join(root, 'bin', 'road-compiler.js'),
|
||||
|
||||
@@ -6,6 +6,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { nativeRoadEdits, roadRevisions } = require('../src');
|
||||
const { compileInput } = require('../src/compile/compiler');
|
||||
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'road-revisions-'));
|
||||
const source = '<osm version="0.6"><node id="1" lon="114.1" lat="22.5"/></osm>\n';
|
||||
@@ -15,6 +16,7 @@ const signals = {
|
||||
provenance: 'empty',
|
||||
assemblies: { type: 'FeatureCollection', features: [] },
|
||||
};
|
||||
const options = { edgeLines: false, junctionTemplates: { enabled: false, references: [] } };
|
||||
|
||||
fs.writeFileSync(path.join(directory, 'source.osm'), source);
|
||||
fs.writeFileSync(path.join(directory, 'native-road-overrides.json'), `${JSON.stringify(overrides, null, 2)}\n`);
|
||||
@@ -26,19 +28,21 @@ const legacyBytes = new Map(
|
||||
]),
|
||||
);
|
||||
|
||||
const first = roadRevisions.ensureRevisionStore(directory);
|
||||
const first = roadRevisions.ensureRevisionStore(directory, options);
|
||||
assert.equal(first.baseline.manifest.id, 'rev-0001');
|
||||
assert.equal(first.baseline.manifest.schema, roadRevisions.REVISION_SCHEMA);
|
||||
assert.equal(first.active.activeRevisionId, 'rev-0001');
|
||||
assert.equal(first.baseline.directEdits.schema, nativeRoadEdits.EDITS_SCHEMA);
|
||||
assert.equal(first.baseline.directEdits.documentVersion, 0);
|
||||
assert.equal(first.baseline.directEdits.base.areaConfigSha256, first.baseline.manifest.source.areaConfigSha256);
|
||||
assert.deepEqual(first.baseline.areaConfig, options);
|
||||
for (const [file, bytes] of legacyBytes)
|
||||
assert.deepEqual(fs.readFileSync(path.join(directory, file)), bytes, `${file} remains byte-for-byte unchanged`);
|
||||
|
||||
const digest = crypto.createHash('sha256').update(source).digest('hex');
|
||||
assert.equal(first.baseline.manifest.source.osmSha256, digest);
|
||||
assert.equal(fs.readdirSync(path.join(directory, 'osm')).filter((file) => file.endsWith('.osm')).length, 1);
|
||||
roadRevisions.ensureRevisionStore(directory);
|
||||
roadRevisions.ensureRevisionStore(directory, options);
|
||||
assert.equal(
|
||||
fs.readdirSync(path.join(directory, 'osm')).filter((file) => file.endsWith('.osm')).length,
|
||||
1,
|
||||
@@ -47,6 +51,7 @@ assert.equal(
|
||||
|
||||
const activeEdits = nativeRoadEdits.emptyEditDocument({
|
||||
osmSha256: digest,
|
||||
areaConfigSha256: first.baseline.manifest.source.areaConfigSha256,
|
||||
compilerGeometryVersion: 'native-road-package/v1.1',
|
||||
});
|
||||
activeEdits.operations = [{ id: 'op-1', createdAt: '2026-08-26T12:00:00.000Z', constraintIds: [] }];
|
||||
@@ -58,6 +63,7 @@ assert.equal(checkpoint.manifest.parentRevisionId, 'rev-0001');
|
||||
assert.deepEqual(checkpoint.nativeRoadOverrides, overrides);
|
||||
assert.deepEqual(checkpoint.trafficSignals, signals);
|
||||
assert.deepEqual(checkpoint.directEdits, activeEdits);
|
||||
assert.deepEqual(checkpoint.areaConfig, options);
|
||||
assert.equal(
|
||||
checkpoint.manifest.digests.nativeRoadOverrides,
|
||||
crypto
|
||||
@@ -72,6 +78,54 @@ assert.deepEqual(
|
||||
);
|
||||
assert.throws(() => roadRevisions.createCheckpoint(directory, ''), /label must be a non-empty string/);
|
||||
assert.throws(() => roadRevisions.readRevision(directory, '../rev-0002'), /Invalid revision id/);
|
||||
const changedOptions = { edgeLines: true, junctionTemplates: { enabled: false, references: [] } };
|
||||
const changedSnapshot = roadRevisions.setActiveAreaConfig(directory, changedOptions);
|
||||
assert.equal(
|
||||
nativeRoadEdits.loadEditDocument(path.join(directory, 'active', 'native-road-edits.json')).base.areaConfigSha256,
|
||||
changedSnapshot.areaConfig.digest,
|
||||
'the active document points to the current snapshot',
|
||||
);
|
||||
assert.equal(
|
||||
nativeRoadEdits.loadEditDocument(path.join(directory, 'active', 'native-road-edits.json')).documentVersion,
|
||||
1,
|
||||
'updating the active snapshot is a versioned document write',
|
||||
);
|
||||
assert.deepEqual(
|
||||
roadRevisions.readRevision(directory, 'rev-0002').areaConfig,
|
||||
options,
|
||||
'old revisions retain their snapshot',
|
||||
);
|
||||
roadRevisions.setActiveAreaConfig(directory, options);
|
||||
|
||||
const fixture = path.join(__dirname, 'fixtures', 'fengshu-er-road.osm');
|
||||
fs.copyFileSync(fixture, path.join(directory, 'source.osm'));
|
||||
const frozen = roadRevisions.createCheckpoint(directory, 'Fixture baseline');
|
||||
const snapshot = path.join(directory, 'active', 'area-config.snapshot.json');
|
||||
const compileBase = {
|
||||
areaId: 'snapshot-test',
|
||||
osmFile: path.join(directory, 'source.osm'),
|
||||
outDir: path.join(directory, 'snapshot-output'),
|
||||
stagingDir: path.join(directory, 'snapshot-pipeline'),
|
||||
overridesFile: path.join(directory, 'native-road-overrides.json'),
|
||||
trafficSignalsFile: path.join(directory, 'native-traffic-signals.json'),
|
||||
options: { edgeLines: true, junctionTemplates: { enabled: false, references: [] } },
|
||||
areaConfigSnapshotFile: snapshot,
|
||||
};
|
||||
const fromSnapshot = compileInput(compileBase);
|
||||
assert.equal(fromSnapshot.area.nativeRoad.edgeLines, false, 'the snapshot wins over changed external options');
|
||||
const fromRevision = compileInput({
|
||||
...compileBase,
|
||||
areaId: 'frozen-revision-test',
|
||||
outDir: path.join(directory, 'revision-output'),
|
||||
stagingDir: path.join(directory, 'revision-pipeline'),
|
||||
osmFile: path.join(directory, frozen.manifest.source.osmFile),
|
||||
areaConfigSnapshotFile: path.join(directory, 'revisions', frozen.manifest.id, 'area-config.snapshot.json'),
|
||||
});
|
||||
assert.equal(fromRevision.area.nativeRoad.edgeLines, false, 'a frozen revision ignores later external config changes');
|
||||
assert.throws(
|
||||
() => compileInput({ ...compileBase, areaConfigSnapshotFile: path.join(directory, 'missing.snapshot.json') }),
|
||||
/Area config snapshot is missing/,
|
||||
);
|
||||
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
console.log('road revision storage tests passed');
|
||||
|
||||
12
test/update-fixture-baselines.js
Normal file
12
test/update-fixture-baselines.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const { compiler } = require('../src');
|
||||
const { fixtureInputs, snapshot, baselineFile } = require('./fixture-baseline');
|
||||
|
||||
for (const input of fixtureInputs()) {
|
||||
compiler.compileInput(input);
|
||||
fs.writeFileSync(baselineFile(input.areaId), `${JSON.stringify(snapshot(input), null, 2)}\n`);
|
||||
}
|
||||
|
||||
console.log('road compiler fixture baselines updated');
|
||||
@@ -9,7 +9,7 @@ const { generate, validateDocument, runtime } = require('../src/native-traffic-s
|
||||
const { convertGeoJson } = require('../src/reference/gaode');
|
||||
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
|
||||
const { compileInput } = require('../src/compile/compiler');
|
||||
const { ensureRevisionStore } = require('../src/compile/road-revisions');
|
||||
const { ensureRevisionStore, setActiveAreaConfig } = require('../src/compile/road-revisions');
|
||||
|
||||
function startWorkbench({
|
||||
area = null,
|
||||
@@ -39,7 +39,7 @@ function startWorkbench({
|
||||
};
|
||||
if (input) {
|
||||
session.context.compileFresh = () => {
|
||||
const compiled = compileInput(input);
|
||||
const compiled = compileWorkbenchInput(input);
|
||||
session.area = compiled.area;
|
||||
return compiled;
|
||||
};
|
||||
@@ -143,16 +143,15 @@ function handle(request, response, session) {
|
||||
.then((body) => {
|
||||
if (!area) throw new Error('请先导入 OSM 文件。');
|
||||
if (!debug) throw new Error('该接口仅在 --debug 模式下可用。');
|
||||
const added = addJunctionCluster(
|
||||
context.configPath,
|
||||
body,
|
||||
readCompiled(area),
|
||||
context.readAreaConfig,
|
||||
context.repoRoot,
|
||||
);
|
||||
const added = addJunctionCluster(body, readCompiled(area), area.nativeRoad);
|
||||
setActiveAreaConfig(path.dirname(area.input), added.options);
|
||||
context.compileFresh();
|
||||
const refreshed = context.readAreaConfig(context.configPath, { repoRoot: context.repoRoot });
|
||||
sendJson(response, 200, { ok: true, added, ...state(refreshed, junctionReference, debug) });
|
||||
sendJson(response, 200, {
|
||||
ok: true,
|
||||
added: added.cluster,
|
||||
configSnippet: { nativeRoad: { junctionTemplates: added.options.junctionTemplates } },
|
||||
...state(session.area, junctionReference, debug),
|
||||
});
|
||||
})
|
||||
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
|
||||
if (request.method === 'POST' && url.pathname === '/api/compile')
|
||||
@@ -170,7 +169,7 @@ function handle(request, response, session) {
|
||||
function state(area, junctionReference = null, debug = false) {
|
||||
// This is the read entry point for both a fresh import and a reopened legacy
|
||||
// workspace. Initialization only adds the v2 layout; it never rewrites v1 files.
|
||||
ensureRevisionStore(path.dirname(area.input));
|
||||
ensureRevisionStore(path.dirname(area.input), area.nativeRoad);
|
||||
const nativeDir = area.outputs.nativeRoadDir;
|
||||
const osm2streetsRoadSurface = area.outputs.geojsonDir
|
||||
? path.join(area.outputs.geojsonDir, 'road_surface.geojson')
|
||||
@@ -214,18 +213,16 @@ function state(area, junctionReference = null, debug = false) {
|
||||
// The compiler reports candidates as advisory diagnostics. Lift them into their
|
||||
// own payload with a stable index so the map can label them "#1, #2, ..." and
|
||||
// the inspector can offer a ready-to-paste cluster配置.
|
||||
// Append one detected cluster to the hand-authored area config. The candidate
|
||||
// Append one detected cluster to the active area-config snapshot. The candidate
|
||||
// must still be present in the latest compile, so a stale browser tab cannot
|
||||
// write a cluster that no longer exists. The edited config is validated by the
|
||||
// real loader before it replaces the file: an invalid write would break every
|
||||
// later command, and the file is git-tracked so a bad accept stays revertible.
|
||||
function addJunctionCluster(configPath, body, compiled, readAreaConfig, repoRoot) {
|
||||
// write a cluster that no longer exists. The external config remains untouched;
|
||||
// the response carries a copyable snippet while future compiles use the snapshot.
|
||||
function addJunctionCluster(body, compiled, options) {
|
||||
const index = Number(body?.index);
|
||||
if (!Number.isInteger(index)) throw new Error('请求缺少候选编号 index。');
|
||||
const candidate = junctionCandidates(compiled).find((item) => item.index === index);
|
||||
if (!candidate) throw new Error(`候选 #${index} 不在最新一次编译结果里,请刷新页面后重试。`);
|
||||
const raw = readJson(configPath);
|
||||
const templates = raw.nativeRoad?.junctionTemplates;
|
||||
const templates = options?.junctionTemplates;
|
||||
if (!templates) throw new Error('区域配置缺少 nativeRoad.junctionTemplates,请先手工建立该节点。');
|
||||
const clusters = Array.isArray(templates.clusters) ? templates.clusters : [];
|
||||
const taken = new Set(clusters.flatMap((cluster) => (cluster.nodeIds || []).map(String)));
|
||||
@@ -240,24 +237,11 @@ function addJunctionCluster(configPath, body, compiled, readAreaConfig, repoRoot
|
||||
outerRadiusExtraMeters: 18,
|
||||
nodeIds: candidate.nodeIds.map(String),
|
||||
};
|
||||
const next = {
|
||||
...raw,
|
||||
nativeRoad: {
|
||||
...raw.nativeRoad,
|
||||
const nextOptions = {
|
||||
...options,
|
||||
junctionTemplates: { ...templates, enabled: true, clusters: [...clusters, cluster] },
|
||||
},
|
||||
};
|
||||
const staging = `${configPath}.candidate-${process.pid}.json`;
|
||||
fs.writeFileSync(staging, `${JSON.stringify(next, null, 2)}\n`);
|
||||
try {
|
||||
readAreaConfig(staging, { repoRoot });
|
||||
} catch (error) {
|
||||
fs.unlinkSync(staging);
|
||||
throw new Error(`写入后的配置无法通过校验,已放弃:${error.message}`);
|
||||
}
|
||||
fs.unlinkSync(staging);
|
||||
writeJsonAtomic(configPath, next);
|
||||
return cluster;
|
||||
return { cluster, options: nextOptions };
|
||||
}
|
||||
function uniqueClusterId(base, taken) {
|
||||
if (!taken.has(base)) return base;
|
||||
@@ -328,8 +312,7 @@ function readUpload(request, session) {
|
||||
),
|
||||
);
|
||||
try {
|
||||
const compiled = compileInput(input);
|
||||
ensureRevisionStore(root);
|
||||
const compiled = compileWorkbenchInput(input);
|
||||
session.area = compiled.area;
|
||||
return compiled;
|
||||
} catch (error) {
|
||||
@@ -338,6 +321,11 @@ function readUpload(request, session) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function compileWorkbenchInput(input) {
|
||||
const workspace = path.dirname(input.osmFile);
|
||||
const initialized = ensureRevisionStore(workspace, input.options);
|
||||
return compileInput({ ...input, areaConfigSnapshotFile: initialized.paths.activeAreaConfig });
|
||||
}
|
||||
function readMultipart(request, limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const type = request.headers['content-type'] || '';
|
||||
|
||||
Reference in New Issue
Block a user