Adds resolveDirectEditConstraints between compileRoadModel and compileGeometry, per the parent design's data flow. The stage is an identity transform until the individual constraint kinds land: it returns empty road profiles, junction plans and handle manifest, so compileGeometry's geometry code is untouched and output stays byte-identical. Constraint planning is separated from solving up front. Only `exact` and `recheck` constraints may reach the solver; `disabled`, `pending`, `conflicted` and `stale` are reported as unapplied with a reason. SOLVED_KINDS starts empty and grows one entry per kind, so a partially delivered solver reports what it skipped instead of publishing half-solved geometry. diagnostic() moves to src/compile/diagnostics.js so the solver can share the compiler's diagnostic shape without depending on fs, which would break the pure-function boundary that lets preview, the full compile and the CLI export share one implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
7.6 KiB
JavaScript
167 lines
7.6 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert/strict');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { nativeRoadEdits, roadRevisions } = require('../src');
|
|
const { compileInput } = require('../src/compile/compiler');
|
|
const { snapshot: outputSnapshot } = require('./fixture-baseline');
|
|
|
|
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';
|
|
const overrides = { schema: 'native-road-overrides/v1', overrides: [] };
|
|
const signals = {
|
|
schema: 'native-traffic-signals/v1',
|
|
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`);
|
|
fs.writeFileSync(path.join(directory, 'native-traffic-signals.json'), `${JSON.stringify(signals, null, 2)}\n`);
|
|
const legacyBytes = new Map(
|
|
['source.osm', 'native-road-overrides.json', 'native-traffic-signals.json'].map((file) => [
|
|
file,
|
|
fs.readFileSync(path.join(directory, file)),
|
|
]),
|
|
);
|
|
|
|
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, options);
|
|
assert.equal(
|
|
fs.readdirSync(path.join(directory, 'osm')).filter((file) => file.endsWith('.osm')).length,
|
|
1,
|
|
'same OSM is stored once',
|
|
);
|
|
|
|
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: [] }];
|
|
fs.writeFileSync(path.join(directory, 'active', 'native-road-edits.json'), `${JSON.stringify(activeEdits, null, 2)}\n`);
|
|
const checkpoint = roadRevisions.createCheckpoint(directory, 'Before junction changes');
|
|
assert.equal(checkpoint.manifest.id, 'rev-0002');
|
|
assert.equal(checkpoint.manifest.label, 'Before junction changes');
|
|
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
|
|
.createHash('sha256')
|
|
.update(fs.readFileSync(path.join(directory, 'revisions', 'rev-0002', 'native-road-overrides.json')))
|
|
.digest('hex'),
|
|
);
|
|
assert.deepEqual(
|
|
roadRevisions.readRevision(directory, 'rev-0002'),
|
|
checkpoint,
|
|
'a checkpoint can be completely restored',
|
|
);
|
|
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/,
|
|
);
|
|
|
|
// The workbench compile is the one caller that passes editsFile. With no v2
|
|
// constraints the direct-edit solver stage must be an identity transform, so
|
|
// both compiles have to publish byte-identical outputs.
|
|
const activeEditsFile = path.join(directory, 'active', 'native-road-edits.json');
|
|
assert.ok(fs.existsSync(activeEditsFile));
|
|
assert.deepEqual(nativeRoadEdits.loadEditDocument(activeEditsFile).constraints, []);
|
|
const identityBase = { ...compileBase, areaId: 'edits-identity-test' };
|
|
const withoutEdits = {
|
|
...identityBase,
|
|
outDir: path.join(directory, 'no-edits-output'),
|
|
stagingDir: path.join(directory, 'no-edits-pipeline'),
|
|
};
|
|
const withEdits = {
|
|
...identityBase,
|
|
outDir: path.join(directory, 'with-edits-output'),
|
|
stagingDir: path.join(directory, 'with-edits-pipeline'),
|
|
editsFile: activeEditsFile,
|
|
};
|
|
compileInput(withoutEdits);
|
|
compileInput(withEdits);
|
|
assert.deepEqual(
|
|
outputSnapshot(withEdits).files,
|
|
outputSnapshot(withoutEdits).files,
|
|
'an empty v2 document must not change compiled output',
|
|
);
|
|
assert.throws(
|
|
() => compileInput({ ...identityBase, editsFile: '' }),
|
|
/editsFile must be a non-empty string when present/,
|
|
);
|
|
assert.throws(
|
|
() => compileInput({ ...identityBase, editsFile: '' }),
|
|
/editsFile must be a non-empty string when present/,
|
|
);
|
|
|
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
console.log('road revision storage tests passed');
|