@ifc-lite/cli 0.33.1 → 0.34.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/README.md +8 -1
- package/dist/commands/diff-content-io.d.ts +50 -0
- package/dist/commands/diff-content-io.d.ts.map +1 -0
- package/dist/commands/diff-content-io.js +104 -0
- package/dist/commands/diff-content-io.js.map +1 -0
- package/dist/commands/diff-content-report.d.ts +23 -0
- package/dist/commands/diff-content-report.d.ts.map +1 -0
- package/dist/commands/diff-content-report.js +67 -0
- package/dist/commands/diff-content-report.js.map +1 -0
- package/dist/commands/diff-content.d.ts +19 -0
- package/dist/commands/diff-content.d.ts.map +1 -1
- package/dist/commands/diff-content.js +120 -129
- package/dist/commands/diff-content.js.map +1 -1
- package/dist/commands/diff-engine.d.ts +32 -1
- package/dist/commands/diff-engine.d.ts.map +1 -1
- package/dist/commands/diff-engine.js +75 -15
- package/dist/commands/diff-engine.js.map +1 -1
- package/dist/commands/diff-geometry.d.ts +107 -0
- package/dist/commands/diff-geometry.d.ts.map +1 -0
- package/dist/commands/diff-geometry.js +262 -0
- package/dist/commands/diff-geometry.js.map +1 -0
- package/dist/commands/diff-lineage-io.d.ts +23 -0
- package/dist/commands/diff-lineage-io.d.ts.map +1 -0
- package/dist/commands/diff-lineage-io.js +119 -0
- package/dist/commands/diff-lineage-io.js.map +1 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +34 -3
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/rekey.d.ts +27 -0
- package/dist/commands/rekey.d.ts.map +1 -0
- package/dist/commands/rekey.js +192 -0
- package/dist/commands/rekey.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +2 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/headless-backend.d.ts.map +1 -1
- package/dist/headless-backend.js +31 -23
- package/dist/headless-backend.js.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/query-overlay-relations.d.ts +8 -28
- package/dist/query-overlay-relations.d.ts.map +1 -1
- package/dist/query-overlay-relations.js +68 -78
- package/dist/query-overlay-relations.js.map +1 -1
- package/dist/query-overlay.d.ts +6 -2
- package/dist/query-overlay.d.ts.map +1 -1
- package/dist/query-overlay.js +61 -5
- package/dist/query-overlay.js.map +1 -1
- package/package.json +18 -18
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ pending generic mutation overlays are not included until reload.
|
|
|
32
32
|
- `info` - model summary: schema, entities, storeys
|
|
33
33
|
- `query` - query entities by type, properties, quantities; supports `--sum`, `--group-by`, `--spatial`
|
|
34
34
|
- `props` - all properties for a single entity (`--id N`)
|
|
35
|
-
- `export` - export to `csv`, `json`, `ifc`,
|
|
35
|
+
- `export` - export to `csv`, `json`, `ifc`, `obj`, `gltf`, `glb`, `jsonld`, `ifcx`, `usd`, `step`, `hbjson`, or `dfjson`
|
|
36
36
|
- `ids` - validate against buildingSMART IDS rules
|
|
37
37
|
- `validate` - structural validation checks
|
|
38
38
|
- `stats` - auto-calculated model KPIs and health check
|
|
@@ -43,11 +43,18 @@ pending generic mutation overlays are not included until reload.
|
|
|
43
43
|
- `merge` - merge multiple IFC files into one federated file
|
|
44
44
|
- `convert` - convert between IFC schema versions (`--schema IFC4`)
|
|
45
45
|
- `diff` - compare two IFC files
|
|
46
|
+
- `rekey` - carry a table keyed on old element keys across a revision
|
|
46
47
|
- `eval` / `run` - run SDK expressions or scripts against a model
|
|
47
48
|
- `ask` - natural language BIM queries
|
|
48
49
|
- `view` - interactive 3D viewer in the browser, controllable via REST (`/api/command`)
|
|
49
50
|
- `analyze` - query plus colorize/isolate/heatmap results in the running viewer
|
|
51
|
+
- `simplify` - demesher: simplify meshes, write a lighter IFC
|
|
50
52
|
- `mcp` - start an MCP server bound to one or more IFC files (stdio or http)
|
|
53
|
+
- `layer` - layered change tracking over a local store (`.ifc-lite/`)
|
|
54
|
+
- `ref` - manage named refs in the layer store
|
|
55
|
+
- `anonymize` - export selected objects + context as an anonymized IFC
|
|
56
|
+
- `schedule` - tabular schedule of one class (csv/json/md/html)
|
|
57
|
+
- `delivery` - repeatable delivery check (structural + IDS) from a saved recipe
|
|
51
58
|
- `gym` - reset/step/reward environment loop over the existing schema/clash/ids checks (see below)
|
|
52
59
|
- `schema`, `bsdd`, `diagnose-geometry`, `extract-entities`, `generate-spaces`, `lod`, `ext` - see `ifc-lite --help`
|
|
53
60
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse to run at all if `--identity-out` names one of the two input models.
|
|
3
|
+
*
|
|
4
|
+
* The sidecar is a JSON document. Writing it over an IFC file destroys the
|
|
5
|
+
* user's model, and a mistyped or shell-completed path is all it takes — the
|
|
6
|
+
* two arguments right before it are IFC paths. So this is checked first, and
|
|
7
|
+
* the command exits without reading, comparing, or writing anything.
|
|
8
|
+
*
|
|
9
|
+
* Two independent tests, because a path string is not a file:
|
|
10
|
+
*
|
|
11
|
+
* 1. Resolved paths are equal. Catches `./v1.ifc` vs `v1.ifc` vs `sub/../v1.ifc`
|
|
12
|
+
* with no filesystem access, and is the whole answer on a platform where
|
|
13
|
+
* `stat` reports no usable inode.
|
|
14
|
+
* 2. The output already exists AND is the same file as an input, by device +
|
|
15
|
+
* inode. This is the exhaustive test: it catches a symlink, a hard link, a
|
|
16
|
+
* bind mount, and `V1.IFC` on a case-insensitive filesystem — every way two
|
|
17
|
+
* different strings can name one file. It is also sufficient on its own for
|
|
18
|
+
* the destructive case, because a path that does not resolve to an existing
|
|
19
|
+
* file cannot be overwriting an input: the inputs must exist to be read.
|
|
20
|
+
*
|
|
21
|
+
* `--identity-in` and `--identity-out` naming the SAME sidecar is not checked
|
|
22
|
+
* here, because that is the carry-forward workflow this feature is built around
|
|
23
|
+
* (read the accepted claims, write back the ones that still held).
|
|
24
|
+
*/
|
|
25
|
+
export declare function refuseOverwritingAnInput(options: {
|
|
26
|
+
basePath: string;
|
|
27
|
+
headPath: string;
|
|
28
|
+
identityOut?: string;
|
|
29
|
+
lineageOut?: string;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Read one of the two input models, reporting a missing or unreadable path the
|
|
33
|
+
* way the rest of the command reports problems rather than throwing a raw
|
|
34
|
+
* `ENOENT` stack at the user — the same treatment `readVerifiedSidecar` gives
|
|
35
|
+
* the sidecar.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readModel(path: string): Promise<Uint8Array>;
|
|
38
|
+
/**
|
|
39
|
+
* Unwrap a possible `.ifcZIP` container into its STEP bytes, reporting a
|
|
40
|
+
* corrupt zip the way the rest of the command reports problems (see
|
|
41
|
+
* {@link readModel}) rather than throwing a raw error. Ordinary `.ifc` bytes
|
|
42
|
+
* pass through unchanged (cheap magic-byte check — `unwrapIfcZipView`).
|
|
43
|
+
*
|
|
44
|
+
* Called once per file and the result reused by both the parser and (when
|
|
45
|
+
* `--geometry` runs) the wasm mesh pass — passing the ORIGINAL, possibly
|
|
46
|
+
* zipped bytes to the mesh pass would feed it a zip container instead of STEP
|
|
47
|
+
* text (issue #4956 review).
|
|
48
|
+
*/
|
|
49
|
+
export declare function unwrapModel(bytes: Uint8Array, label: string): Promise<Uint8Array>;
|
|
50
|
+
//# sourceMappingURL=diff-content-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-content-io.d.ts","sourceRoot":"","sources":["../../src/commands/diff-content-io.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhB;AAgBD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMjE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAMvF"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* File-level I/O for `ifc-lite diff --by-content`: reading the two input
|
|
6
|
+
* models (and unwrapping a possible `.ifcZIP` container), and refusing to run
|
|
7
|
+
* at all when an output path would overwrite one of them. Split out of
|
|
8
|
+
* `diff-content.ts` for size.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { unwrapIfcZipView } from '@ifc-lite/parser';
|
|
13
|
+
import { fatal } from '../output.js';
|
|
14
|
+
/**
|
|
15
|
+
* Refuse to run at all if `--identity-out` names one of the two input models.
|
|
16
|
+
*
|
|
17
|
+
* The sidecar is a JSON document. Writing it over an IFC file destroys the
|
|
18
|
+
* user's model, and a mistyped or shell-completed path is all it takes — the
|
|
19
|
+
* two arguments right before it are IFC paths. So this is checked first, and
|
|
20
|
+
* the command exits without reading, comparing, or writing anything.
|
|
21
|
+
*
|
|
22
|
+
* Two independent tests, because a path string is not a file:
|
|
23
|
+
*
|
|
24
|
+
* 1. Resolved paths are equal. Catches `./v1.ifc` vs `v1.ifc` vs `sub/../v1.ifc`
|
|
25
|
+
* with no filesystem access, and is the whole answer on a platform where
|
|
26
|
+
* `stat` reports no usable inode.
|
|
27
|
+
* 2. The output already exists AND is the same file as an input, by device +
|
|
28
|
+
* inode. This is the exhaustive test: it catches a symlink, a hard link, a
|
|
29
|
+
* bind mount, and `V1.IFC` on a case-insensitive filesystem — every way two
|
|
30
|
+
* different strings can name one file. It is also sufficient on its own for
|
|
31
|
+
* the destructive case, because a path that does not resolve to an existing
|
|
32
|
+
* file cannot be overwriting an input: the inputs must exist to be read.
|
|
33
|
+
*
|
|
34
|
+
* `--identity-in` and `--identity-out` naming the SAME sidecar is not checked
|
|
35
|
+
* here, because that is the carry-forward workflow this feature is built around
|
|
36
|
+
* (read the accepted claims, write back the ones that still held).
|
|
37
|
+
*/
|
|
38
|
+
export async function refuseOverwritingAnInput(options) {
|
|
39
|
+
for (const [flag, target] of [
|
|
40
|
+
['--identity-out', options.identityOut],
|
|
41
|
+
['--lineage-out', options.lineageOut],
|
|
42
|
+
]) {
|
|
43
|
+
if (target === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
const out = resolve(target);
|
|
46
|
+
const outStat = await statOrUndefined(target);
|
|
47
|
+
for (const [label, input] of [
|
|
48
|
+
['base model', options.basePath],
|
|
49
|
+
['head model', options.headPath],
|
|
50
|
+
]) {
|
|
51
|
+
if (resolve(input) !== out && !isSameFile(outStat, await statOrUndefined(input)))
|
|
52
|
+
continue;
|
|
53
|
+
fatal(`${flag} ${target} is the ${label} (${input}). ` +
|
|
54
|
+
'Writing there would overwrite the input file.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function statOrUndefined(path) {
|
|
59
|
+
// A missing or unreadable path cannot be an input file being overwritten;
|
|
60
|
+
// reading the models is what reports it, with its own message.
|
|
61
|
+
try {
|
|
62
|
+
return await stat(path);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isSameFile(a, b) {
|
|
69
|
+
return a !== undefined && b !== undefined && a.dev === b.dev && a.ino === b.ino;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read one of the two input models, reporting a missing or unreadable path the
|
|
73
|
+
* way the rest of the command reports problems rather than throwing a raw
|
|
74
|
+
* `ENOENT` stack at the user — the same treatment `readVerifiedSidecar` gives
|
|
75
|
+
* the sidecar.
|
|
76
|
+
*/
|
|
77
|
+
export async function readModel(path) {
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return fatal(`Cannot read ${path}: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Unwrap a possible `.ifcZIP` container into its STEP bytes, reporting a
|
|
87
|
+
* corrupt zip the way the rest of the command reports problems (see
|
|
88
|
+
* {@link readModel}) rather than throwing a raw error. Ordinary `.ifc` bytes
|
|
89
|
+
* pass through unchanged (cheap magic-byte check — `unwrapIfcZipView`).
|
|
90
|
+
*
|
|
91
|
+
* Called once per file and the result reused by both the parser and (when
|
|
92
|
+
* `--geometry` runs) the wasm mesh pass — passing the ORIGINAL, possibly
|
|
93
|
+
* zipped bytes to the mesh pass would feed it a zip container instead of STEP
|
|
94
|
+
* text (issue #4956 review).
|
|
95
|
+
*/
|
|
96
|
+
export async function unwrapModel(bytes, label) {
|
|
97
|
+
try {
|
|
98
|
+
return new Uint8Array(await unwrapIfcZipView(bytes));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return fatal(`Cannot read ${label}: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=diff-content-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-content-io.js","sourceRoot":"","sources":["../../src/commands/diff-content-io.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,OAK9C;IACC,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI;QAC3B,CAAC,gBAAgB,EAAE,OAAO,CAAC,WAAW,CAAC;QACvC,CAAC,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC;KAC7B,EAAE,CAAC;QACX,IAAI,MAAM,KAAK,SAAS;YAAE,SAAS;QACnC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;QAC9C,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;YAC3B,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC;YAChC,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC;SACxB,EAAE,CAAC;YACX,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;gBAAE,SAAS;YAC3F,KAAK,CACH,GAAG,IAAI,IAAI,MAAM,WAAW,KAAK,KAAK,KAAK,KAAK;gBAC9C,+CAA+C,CAClD,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY;IACzC,0EAA0E;IAC1E,+DAA+D;IAC/D,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,CAAoB,EAAE,CAAoB;IAC5D,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC;AAClF,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,KAAK,CAAC,eAAe,IAAI,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAiB,EAAE,KAAa;IAChE,IAAI,CAAC;QACH,OAAO,IAAI,UAAU,CAAC,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,KAAK,CAAC,eAAe,KAAK,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The human-readable report of `ifc-lite diff --by-content`; the JSON form
|
|
2
|
+
* lives with the command in `diff-content.ts`. Split out for size. */
|
|
3
|
+
import type { IdentityMapEntry, LineageEntry, ModelDiff } from '@ifc-lite/diff';
|
|
4
|
+
import type { DiffRef } from './diff-engine.js';
|
|
5
|
+
export declare function printReport(report: {
|
|
6
|
+
basePath: string;
|
|
7
|
+
headPath: string;
|
|
8
|
+
diff: ModelDiff<DiffRef>;
|
|
9
|
+
appliedCount: number;
|
|
10
|
+
ignoredCount: number;
|
|
11
|
+
identityIn?: string;
|
|
12
|
+
written?: {
|
|
13
|
+
path: string;
|
|
14
|
+
entries: IdentityMapEntry[];
|
|
15
|
+
};
|
|
16
|
+
lineageIn?: string;
|
|
17
|
+
lineageWritten?: {
|
|
18
|
+
path: string;
|
|
19
|
+
entries: LineageEntry[];
|
|
20
|
+
};
|
|
21
|
+
keyProperty?: string;
|
|
22
|
+
}): void;
|
|
23
|
+
//# sourceMappingURL=diff-content-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-content-report.d.ts","sourceRoot":"","sources":["../../src/commands/diff-content-report.ts"],"names":[],"mappings":"AAIA;uEACuE;AAEvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAC;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAsEP"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
export function printReport(report) {
|
|
5
|
+
const { diff } = report;
|
|
6
|
+
const out = (line) => {
|
|
7
|
+
process.stdout.write(`${line}\n`);
|
|
8
|
+
};
|
|
9
|
+
out('');
|
|
10
|
+
out(` Base: ${report.basePath}`);
|
|
11
|
+
out(` Head: ${report.headPath}`);
|
|
12
|
+
out(` Scope: ${diff.scope}` +
|
|
13
|
+
(diff.scope === 'data' ? ' (pass --geometry for world geometry hashes + boxes)' : ''));
|
|
14
|
+
if (report.keyProperty)
|
|
15
|
+
out(` Key: ${report.keyProperty} (GlobalId where absent)`);
|
|
16
|
+
out('');
|
|
17
|
+
out(` Unchanged: ${diff.counts.unchanged}`);
|
|
18
|
+
out(` Modified: ${diff.counts.modified}`);
|
|
19
|
+
out(` Added: ${diff.counts.added}`);
|
|
20
|
+
out(` Deleted: ${diff.counts.deleted}`);
|
|
21
|
+
const byKind = new Map();
|
|
22
|
+
for (const match of diff.contentMatches ?? []) {
|
|
23
|
+
byKind.set(match.kind, (byKind.get(match.kind) ?? 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
out('');
|
|
26
|
+
if (byKind.size === 0) {
|
|
27
|
+
out(' Content matches: none');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
out(' Content matches:');
|
|
31
|
+
for (const [kind, count] of [...byKind].sort()) {
|
|
32
|
+
out(` ${kind.padEnd(13)} ${count}`);
|
|
33
|
+
}
|
|
34
|
+
out(' (renamed / moved / reshaped / respecified are resolved; the rest need a human)');
|
|
35
|
+
}
|
|
36
|
+
if (diff.splitMerges) {
|
|
37
|
+
out('');
|
|
38
|
+
out(diff.splitMerges.length === 0
|
|
39
|
+
? ' Split/merge claims: none'
|
|
40
|
+
: ` Split/merge claims: ${diff.splitMerges.length}`);
|
|
41
|
+
}
|
|
42
|
+
if (diff.successors) {
|
|
43
|
+
out('');
|
|
44
|
+
out(diff.successors.length === 0
|
|
45
|
+
? ' Successor claims: none'
|
|
46
|
+
: ` Successor claims: ${diff.successors.length} (suggestions — review before accepting)`);
|
|
47
|
+
}
|
|
48
|
+
if (report.identityIn) {
|
|
49
|
+
out('');
|
|
50
|
+
out(` Identity map in: ${report.identityIn}`);
|
|
51
|
+
out(` applied: ${report.appliedCount}, ignored: ${report.ignoredCount}`);
|
|
52
|
+
}
|
|
53
|
+
if (report.written) {
|
|
54
|
+
out('');
|
|
55
|
+
out(` Identity map out: ${report.written.path} (${report.written.entries.length} claims)`);
|
|
56
|
+
}
|
|
57
|
+
if (report.lineageIn) {
|
|
58
|
+
out('');
|
|
59
|
+
out(` Lineage in: ${report.lineageIn}`);
|
|
60
|
+
}
|
|
61
|
+
if (report.lineageWritten) {
|
|
62
|
+
out('');
|
|
63
|
+
out(` Lineage out: ${report.lineageWritten.path} (${report.lineageWritten.entries.length} entries)`);
|
|
64
|
+
}
|
|
65
|
+
out('');
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=diff-content-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-content-report.js","sourceRoot":"","sources":["../../src/commands/diff-content-report.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,25 @@ export interface ContentDiffOptions {
|
|
|
5
5
|
identityIn?: string;
|
|
6
6
|
/** `--identity-out`: where to write the sidecar this run establishes. */
|
|
7
7
|
identityOut?: string;
|
|
8
|
+
/** `--lineage-in`: a lineage whose 1:1 entries are replayed as key aliases. */
|
|
9
|
+
lineageIn?: string;
|
|
10
|
+
/** `--lineage-out`: where to write the lineage this run establishes. */
|
|
11
|
+
lineageOut?: string;
|
|
12
|
+
/** `--accept`: a reviewed identity map folded into the lineage as `replaced`. */
|
|
13
|
+
accept?: string;
|
|
14
|
+
/** `--key-from`: `Tag` or `Pset.Prop`, the authored key to compare on. */
|
|
15
|
+
keyFrom?: string;
|
|
16
|
+
/** `--geometry`: run the wasm mesh pass and attach world geometry hashes,
|
|
17
|
+
* boxes and volumes, promoting the comparison from `data` to `both`
|
|
18
|
+
* scope (issue #4956). Skips with a stderr warning, not a fatal error,
|
|
19
|
+
* when the wasm runtime is not built on this host. */
|
|
20
|
+
geometry?: boolean;
|
|
21
|
+
/** `--split-merge`: opt in to the split/merge detector. Only produces
|
|
22
|
+
* claims together with `--geometry` (issue #4956). */
|
|
23
|
+
splitMerge?: boolean;
|
|
24
|
+
/** `--successors`: opt in to the successor-match detector. Only produces
|
|
25
|
+
* claims together with `--geometry` (issue #4956). */
|
|
26
|
+
successors?: boolean;
|
|
8
27
|
json: boolean;
|
|
9
28
|
}
|
|
10
29
|
export declare function contentDiffCommand(options: ContentDiffOptions): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff-content.d.ts","sourceRoot":"","sources":["../../src/commands/diff-content.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diff-content.d.ts","sourceRoot":"","sources":["../../src/commands/diff-content.ts"],"names":[],"mappings":"AA6DA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;2DAGuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2DACuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;2DACuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8LnF"}
|
|
@@ -8,37 +8,99 @@
|
|
|
8
8
|
* The loop this closes: run it once, review the `renamed` matches, keep the
|
|
9
9
|
* sidecar. On the next run the accepted claims come back in as key aliases, the
|
|
10
10
|
* re-GUIDed elements are matched by key, and they never show up as churn again.
|
|
11
|
-
*
|
|
11
|
+
* Data scope by default — see `diff-engine.ts`; `--geometry` below opts into
|
|
12
|
+
* the wasm mesh pass.
|
|
13
|
+
*
|
|
14
|
+
* Issue #4955 adds three things on the same loop: `--key-from` keys the
|
|
15
|
+
* comparison on an authored identifier instead of GlobalId; `--lineage-out` /
|
|
16
|
+
* `--lineage-in` write and replay the 1:k lineage an external table rekeys on;
|
|
17
|
+
* and `--accept` folds a reviewed identity map (a human's answer to the
|
|
18
|
+
* suggestions a geometry-capable run produced) into that lineage as
|
|
19
|
+
* `replaced` entries.
|
|
20
|
+
*
|
|
21
|
+
* Issue #4956 adds `--geometry`: a lazily-loaded `@ifc-lite/wasm` mesh pass
|
|
22
|
+
* (`diff-geometry.ts`) that attaches world geometry hashes, bounding boxes and
|
|
23
|
+
* volumes to both files' fingerprints, promoting the scope from `data` to
|
|
24
|
+
* `both`. `--split-merge` / `--successors` turn on the two geometry-only
|
|
25
|
+
* detection stages; both are no-ops without `--geometry` (the engine abstains
|
|
26
|
+
* exactly as it does for a viewer session whose geometry hashing failed).
|
|
12
27
|
*/
|
|
13
|
-
import { readFile,
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
28
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
29
|
+
import { createIdentityMapSidecar, createLineageSidecar, diffModels, identityMapFromContentMatches, identityMapSidecarMismatches, keyAliasesFromLineage, keyAliasesFromSidecar, parseIdentityMapSidecar, serializeIdentityMapSidecar, serializeLineageSidecar, } from '@ifc-lite/diff';
|
|
30
|
+
import { parseAuthoredKeySpec } from '@ifc-lite/parser';
|
|
16
31
|
import { loadIfcBytes } from '../loader.js';
|
|
17
32
|
import { fatal, printJson } from '../output.js';
|
|
18
|
-
import { buildFileFingerprints, modelIdentityOf } from './diff-engine.js';
|
|
33
|
+
import { buildFileFingerprints, fallbackPairDuplicateAuthoredKeys, modelIdentityOf, } from './diff-engine.js';
|
|
34
|
+
import { readModel, refuseOverwritingAnInput, unwrapModel } from './diff-content-io.js';
|
|
35
|
+
import { resolveGeometryScope } from './diff-geometry.js';
|
|
36
|
+
import { mergeAliases, mergeLineage, readVerifiedLineage } from './diff-lineage-io.js';
|
|
37
|
+
import { printReport } from './diff-content-report.js';
|
|
19
38
|
export async function contentDiffCommand(options) {
|
|
20
39
|
const { basePath, headPath } = options;
|
|
40
|
+
const keySpec = options.keyFrom === undefined ? undefined : parseAuthoredKeySpec(options.keyFrom);
|
|
41
|
+
if (options.keyFrom !== undefined && !keySpec) {
|
|
42
|
+
fatal(`--key-from must be Tag or <PsetName>.<PropertyName>, got "${options.keyFrom}"`);
|
|
43
|
+
}
|
|
44
|
+
// `Tag` is case-insensitive at the CLI boundary, but the persisted sidecar
|
|
45
|
+
// contract is canonical and shared with the viewer's compare UI.
|
|
46
|
+
const keyProperty = keySpec?.kind === 'tag' ? 'Tag' : options.keyFrom?.trim();
|
|
21
47
|
// Before anything is read, and long before anything is written.
|
|
22
48
|
await refuseOverwritingAnInput(options);
|
|
23
49
|
const baseBytes = await readModel(basePath);
|
|
24
50
|
const headBytes = await readModel(headPath);
|
|
51
|
+
// The identity hash is over the file AS IT SITS ON DISK, before any
|
|
52
|
+
// `.ifcZIP` unwrapping (see `modelIdentityOf`) — so it is taken from the raw
|
|
53
|
+
// bytes, and the unwrap below is a separate step, not folded into this read.
|
|
25
54
|
const baseIdentity = modelIdentityOf(basePath, baseBytes);
|
|
26
55
|
const headIdentity = modelIdentityOf(headPath, headBytes);
|
|
56
|
+
const pinned = { base: baseIdentity, head: headIdentity, keyProperty };
|
|
27
57
|
const incoming = options.identityIn
|
|
28
|
-
? await readVerifiedSidecar(options.identityIn,
|
|
58
|
+
? await readVerifiedSidecar(options.identityIn, pinned)
|
|
29
59
|
: undefined;
|
|
60
|
+
const incomingLineage = options.lineageIn
|
|
61
|
+
? await readVerifiedLineage(options.lineageIn, pinned)
|
|
62
|
+
: undefined;
|
|
63
|
+
const accepted = options.accept ? await readVerifiedSidecar(options.accept, pinned) : undefined;
|
|
64
|
+
// Unwrapped ONCE and reused by both the parser and (if `--geometry` runs)
|
|
65
|
+
// the wasm mesh pass — `unwrapIfcZipView` is a cheap magic-byte no-op for an
|
|
66
|
+
// ordinary `.ifc` file, so a `--geometry` run over a `.ifcZIP` still sees the
|
|
67
|
+
// real STEP bytes rather than the zip container (issue #4956 review).
|
|
68
|
+
const baseUnwrapped = await unwrapModel(baseBytes, basePath);
|
|
69
|
+
const headUnwrapped = await unwrapModel(headBytes, headPath);
|
|
30
70
|
process.stderr.write('Loading files...\n');
|
|
31
|
-
const baseStore = await loadIfcBytes(
|
|
32
|
-
const headStore = await loadIfcBytes(
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
const baseStore = await loadIfcBytes(baseUnwrapped, basePath);
|
|
72
|
+
const headStore = await loadIfcBytes(headUnwrapped, headPath);
|
|
73
|
+
const duplicateAuthoredKeys = new Map();
|
|
74
|
+
const adapter = { keyProperty, duplicateAuthoredKeys };
|
|
75
|
+
const baseFingerprints = buildFileFingerprints(baseStore, adapter);
|
|
76
|
+
const headFingerprints = buildFileFingerprints(headStore, adapter);
|
|
77
|
+
fallbackPairDuplicateAuthoredKeys([
|
|
78
|
+
{ fingerprints: baseFingerprints, store: baseStore },
|
|
79
|
+
{ fingerprints: headFingerprints, store: headStore },
|
|
80
|
+
], duplicateAuthoredKeys);
|
|
81
|
+
for (const [value, ids] of duplicateAuthoredKeys) {
|
|
82
|
+
process.stderr.write(`Warning: ${keyProperty} = "${value}" names ${ids.length} entities; they fall back to GlobalId.\n`);
|
|
83
|
+
}
|
|
84
|
+
// Both sources of aliases are replayed together. A lineage's 1:1 entries and
|
|
85
|
+
// an identity map's claims are the same kind of thing under two file formats;
|
|
86
|
+
// where the two disagree about one head key, neither is applied — that is
|
|
87
|
+
// `resolveKeyAliases` rule 4, arriving from two files instead of one.
|
|
88
|
+
const aliases = mergeAliases(incoming ? keyAliasesFromSidecar(incoming) : undefined, incomingLineage ? keyAliasesFromLineage(incomingLineage.entries) : undefined);
|
|
89
|
+
// `--geometry` (issue #4956): see `diff-geometry.ts` for the wasm pass and
|
|
90
|
+
// the graceful fall-back to `scope: 'data'` when the runtime is absent.
|
|
91
|
+
const scope = await resolveGeometryScope(options.geometry ?? false, baseUnwrapped, headUnwrapped, baseFingerprints, headFingerprints, (message) => process.stderr.write(`Warning: ${message}\n`));
|
|
92
|
+
const diff = diffModels(baseFingerprints, headFingerprints, {
|
|
93
|
+
scope,
|
|
37
94
|
matchUnpairedByContent: true,
|
|
38
|
-
keyAliases:
|
|
95
|
+
keyAliases: aliases,
|
|
96
|
+
// Both are geometry-only stages (issue #4956): with no geometry pass run,
|
|
97
|
+
// the engine abstains and `diff.splitMerges` / `diff.successors` stay
|
|
98
|
+
// undefined, exactly as a viewer session with failed geometry hashing.
|
|
99
|
+
detectSplitMerge: options.splitMerge,
|
|
100
|
+
detectSuccessors: options.successors,
|
|
39
101
|
});
|
|
40
102
|
const applied = diff.appliedKeyAliases ?? new Map();
|
|
41
|
-
const ignored =
|
|
103
|
+
const ignored = aliases ? aliases.size - applied.size : 0;
|
|
42
104
|
let written;
|
|
43
105
|
if (options.identityOut) {
|
|
44
106
|
const entries = mergeIdentityClaims(incoming, applied, diff.contentMatches);
|
|
@@ -59,10 +121,25 @@ export async function contentDiffCommand(options) {
|
|
|
59
121
|
// optional, and `git log` dates a reviewed, committed artifact better
|
|
60
122
|
// than a self-reported timestamp does.
|
|
61
123
|
created: incoming?.created,
|
|
124
|
+
keyProperty,
|
|
62
125
|
});
|
|
63
126
|
await writeFile(options.identityOut, serializeIdentityMapSidecar(sidecar), 'utf-8');
|
|
64
127
|
written = { path: options.identityOut, entries: sidecar.entries };
|
|
65
128
|
}
|
|
129
|
+
let lineageWritten;
|
|
130
|
+
if (options.lineageOut) {
|
|
131
|
+
const { entries, deleted } = mergeLineage(incomingLineage, incoming, diff, accepted);
|
|
132
|
+
const sidecar = createLineageSidecar({
|
|
133
|
+
base: baseIdentity,
|
|
134
|
+
head: headIdentity,
|
|
135
|
+
entries,
|
|
136
|
+
deleted,
|
|
137
|
+
created: incomingLineage?.created,
|
|
138
|
+
keyProperty,
|
|
139
|
+
});
|
|
140
|
+
await writeFile(options.lineageOut, serializeLineageSidecar(sidecar), 'utf-8');
|
|
141
|
+
lineageWritten = { path: options.lineageOut, entries: sidecar.entries };
|
|
142
|
+
}
|
|
66
143
|
if (options.json) {
|
|
67
144
|
printJson({
|
|
68
145
|
base: { path: basePath, hash: baseIdentity.hash, schema: baseStore.schemaVersion },
|
|
@@ -74,12 +151,36 @@ export async function contentDiffCommand(options) {
|
|
|
74
151
|
base: match.base.map((entity) => entity.key),
|
|
75
152
|
head: match.head.map((entity) => entity.key),
|
|
76
153
|
})),
|
|
154
|
+
// Absent (not `[]`) exactly when the stage did not run or the geometry
|
|
155
|
+
// abstention fired — the engine's own "absent means not proved"
|
|
156
|
+
// contract, preserved rather than flattened to an empty array.
|
|
157
|
+
splitMerges: diff.splitMerges?.map((claim) => ({
|
|
158
|
+
kind: claim.kind,
|
|
159
|
+
confidence: claim.confidence,
|
|
160
|
+
whole: claim.whole.key,
|
|
161
|
+
pieces: claim.pieces.map((piece) => piece.key),
|
|
162
|
+
})),
|
|
163
|
+
successors: diff.successors?.map((claim) => ({
|
|
164
|
+
confidence: claim.confidence,
|
|
165
|
+
base: claim.base.key,
|
|
166
|
+
head: claim.head.key,
|
|
167
|
+
overlap: claim.overlap,
|
|
168
|
+
distance: claim.distance,
|
|
169
|
+
})),
|
|
77
170
|
identityMap: {
|
|
78
171
|
in: options.identityIn
|
|
79
172
|
? { path: options.identityIn, applied: applied.size, ignored }
|
|
80
173
|
: undefined,
|
|
81
174
|
out: written ? { path: written.path, entries: written.entries.length } : undefined,
|
|
82
175
|
},
|
|
176
|
+
lineage: {
|
|
177
|
+
in: options.lineageIn ? { path: options.lineageIn } : undefined,
|
|
178
|
+
out: lineageWritten
|
|
179
|
+
? { path: lineageWritten.path, entries: lineageWritten.entries.length }
|
|
180
|
+
: undefined,
|
|
181
|
+
},
|
|
182
|
+
keyProperty: keyProperty ?? null,
|
|
183
|
+
duplicateAuthoredKeys: [...duplicateAuthoredKeys.keys()],
|
|
83
184
|
});
|
|
84
185
|
return;
|
|
85
186
|
}
|
|
@@ -91,75 +192,11 @@ export async function contentDiffCommand(options) {
|
|
|
91
192
|
ignoredCount: ignored,
|
|
92
193
|
identityIn: options.identityIn,
|
|
93
194
|
written,
|
|
195
|
+
lineageIn: options.lineageIn,
|
|
196
|
+
lineageWritten,
|
|
197
|
+
keyProperty,
|
|
94
198
|
});
|
|
95
199
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Refuse to run at all if `--identity-out` names one of the two input models.
|
|
98
|
-
*
|
|
99
|
-
* The sidecar is a JSON document. Writing it over an IFC file destroys the
|
|
100
|
-
* user's model, and a mistyped or shell-completed path is all it takes — the
|
|
101
|
-
* two arguments right before it are IFC paths. So this is checked first, and
|
|
102
|
-
* the command exits without reading, comparing, or writing anything.
|
|
103
|
-
*
|
|
104
|
-
* Two independent tests, because a path string is not a file:
|
|
105
|
-
*
|
|
106
|
-
* 1. Resolved paths are equal. Catches `./v1.ifc` vs `v1.ifc` vs `sub/../v1.ifc`
|
|
107
|
-
* with no filesystem access, and is the whole answer on a platform where
|
|
108
|
-
* `stat` reports no usable inode.
|
|
109
|
-
* 2. The output already exists AND is the same file as an input, by device +
|
|
110
|
-
* inode. This is the exhaustive test: it catches a symlink, a hard link, a
|
|
111
|
-
* bind mount, and `V1.IFC` on a case-insensitive filesystem — every way two
|
|
112
|
-
* different strings can name one file. It is also sufficient on its own for
|
|
113
|
-
* the destructive case, because a path that does not resolve to an existing
|
|
114
|
-
* file cannot be overwriting an input: the inputs must exist to be read.
|
|
115
|
-
*
|
|
116
|
-
* `--identity-in` and `--identity-out` naming the SAME sidecar is not checked
|
|
117
|
-
* here, because that is the carry-forward workflow this feature is built around
|
|
118
|
-
* (read the accepted claims, write back the ones that still held).
|
|
119
|
-
*/
|
|
120
|
-
async function refuseOverwritingAnInput(options) {
|
|
121
|
-
const { identityOut } = options;
|
|
122
|
-
if (identityOut === undefined)
|
|
123
|
-
return;
|
|
124
|
-
const out = resolve(identityOut);
|
|
125
|
-
const outStat = await statOrUndefined(identityOut);
|
|
126
|
-
for (const [label, input] of [
|
|
127
|
-
['base model', options.basePath],
|
|
128
|
-
['head model', options.headPath],
|
|
129
|
-
]) {
|
|
130
|
-
if (resolve(input) !== out && !isSameFile(outStat, await statOrUndefined(input)))
|
|
131
|
-
continue;
|
|
132
|
-
fatal(`--identity-out ${identityOut} is the ${label} (${input}). ` +
|
|
133
|
-
'Writing the identity map there would overwrite the input file.');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
async function statOrUndefined(path) {
|
|
137
|
-
// A missing or unreadable path cannot be an input file being overwritten;
|
|
138
|
-
// reading the models is what reports it, with its own message.
|
|
139
|
-
try {
|
|
140
|
-
return await stat(path);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function isSameFile(a, b) {
|
|
147
|
-
return a !== undefined && b !== undefined && a.dev === b.dev && a.ino === b.ino;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Read one of the two input models, reporting a missing or unreadable path the
|
|
151
|
-
* way the rest of the command reports problems rather than throwing a raw
|
|
152
|
-
* `ENOENT` stack at the user — the same treatment `readVerifiedSidecar` already
|
|
153
|
-
* gives the sidecar.
|
|
154
|
-
*/
|
|
155
|
-
async function readModel(path) {
|
|
156
|
-
try {
|
|
157
|
-
return await readFile(path);
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
return fatal(`Cannot read ${path}: ${error.message}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
200
|
/**
|
|
164
201
|
* Read a sidecar and refuse it unless it was verified against exactly these two
|
|
165
202
|
* files.
|
|
@@ -171,7 +208,7 @@ async function readModel(path) {
|
|
|
171
208
|
* deliberately no override flag: the fix for a mismatch is to re-run the
|
|
172
209
|
* comparison that produced the claims, which is one command.
|
|
173
210
|
*/
|
|
174
|
-
async function readVerifiedSidecar(path,
|
|
211
|
+
async function readVerifiedSidecar(path, models) {
|
|
175
212
|
let text;
|
|
176
213
|
try {
|
|
177
214
|
text = await readFile(path, 'utf-8');
|
|
@@ -186,18 +223,12 @@ async function readVerifiedSidecar(path, base, head) {
|
|
|
186
223
|
catch (error) {
|
|
187
224
|
return fatal(error.message);
|
|
188
225
|
}
|
|
189
|
-
const problems = identityMapSidecarMismatches(sidecar,
|
|
226
|
+
const problems = identityMapSidecarMismatches(sidecar, models);
|
|
190
227
|
if (problems.length > 0) {
|
|
191
228
|
return fatal(`Identity map ${path} was not verified against these files:\n ${problems.join('\n ')}`);
|
|
192
229
|
}
|
|
193
230
|
return sidecar;
|
|
194
231
|
}
|
|
195
|
-
/** Distinct head keys a sidecar actually claims (self-claims and repeats are
|
|
196
|
-
* no-ops the consumer drops, so counting raw entries would overstate what
|
|
197
|
-
* could have applied). */
|
|
198
|
-
function countClaims(sidecar) {
|
|
199
|
-
return keyAliasesFromSidecar(sidecar).size;
|
|
200
|
-
}
|
|
201
232
|
/**
|
|
202
233
|
* The claims the output sidecar should carry: the incoming ones that still
|
|
203
234
|
* applied, plus the ones this run's content matching established.
|
|
@@ -227,44 +258,4 @@ function mergeIdentityClaims(incoming, applied, matches) {
|
|
|
227
258
|
entries.push(...identityMapFromContentMatches(matches));
|
|
228
259
|
return entries;
|
|
229
260
|
}
|
|
230
|
-
function printReport(report) {
|
|
231
|
-
const { diff } = report;
|
|
232
|
-
const out = (line) => {
|
|
233
|
-
process.stdout.write(`${line}\n`);
|
|
234
|
-
};
|
|
235
|
-
out('');
|
|
236
|
-
out(` Base: ${report.basePath}`);
|
|
237
|
-
out(` Head: ${report.headPath}`);
|
|
238
|
-
out(` Scope: data (the CLI has no geometry pipeline)`);
|
|
239
|
-
out('');
|
|
240
|
-
out(` Unchanged: ${diff.counts.unchanged}`);
|
|
241
|
-
out(` Modified: ${diff.counts.modified}`);
|
|
242
|
-
out(` Added: ${diff.counts.added}`);
|
|
243
|
-
out(` Deleted: ${diff.counts.deleted}`);
|
|
244
|
-
const byKind = new Map();
|
|
245
|
-
for (const match of diff.contentMatches ?? []) {
|
|
246
|
-
byKind.set(match.kind, (byKind.get(match.kind) ?? 0) + 1);
|
|
247
|
-
}
|
|
248
|
-
out('');
|
|
249
|
-
if (byKind.size === 0) {
|
|
250
|
-
out(' Content matches: none');
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
out(' Content matches:');
|
|
254
|
-
for (const [kind, count] of [...byKind].sort()) {
|
|
255
|
-
out(` ${kind.padEnd(13)} ${count}`);
|
|
256
|
-
}
|
|
257
|
-
out(' (renamed / moved / reshaped are resolved; the rest need a human)');
|
|
258
|
-
}
|
|
259
|
-
if (report.identityIn) {
|
|
260
|
-
out('');
|
|
261
|
-
out(` Identity map in: ${report.identityIn}`);
|
|
262
|
-
out(` applied: ${report.appliedCount}, ignored: ${report.ignoredCount}`);
|
|
263
|
-
}
|
|
264
|
-
if (report.written) {
|
|
265
|
-
out('');
|
|
266
|
-
out(` Identity map out: ${report.written.path} (${report.written.entries.length} claims)`);
|
|
267
|
-
}
|
|
268
|
-
out('');
|
|
269
|
-
}
|
|
270
261
|
//# sourceMappingURL=diff-content.js.map
|