@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.
Files changed (49) hide show
  1. package/README.md +8 -1
  2. package/dist/commands/diff-content-io.d.ts +50 -0
  3. package/dist/commands/diff-content-io.d.ts.map +1 -0
  4. package/dist/commands/diff-content-io.js +104 -0
  5. package/dist/commands/diff-content-io.js.map +1 -0
  6. package/dist/commands/diff-content-report.d.ts +23 -0
  7. package/dist/commands/diff-content-report.d.ts.map +1 -0
  8. package/dist/commands/diff-content-report.js +67 -0
  9. package/dist/commands/diff-content-report.js.map +1 -0
  10. package/dist/commands/diff-content.d.ts +19 -0
  11. package/dist/commands/diff-content.d.ts.map +1 -1
  12. package/dist/commands/diff-content.js +120 -129
  13. package/dist/commands/diff-content.js.map +1 -1
  14. package/dist/commands/diff-engine.d.ts +32 -1
  15. package/dist/commands/diff-engine.d.ts.map +1 -1
  16. package/dist/commands/diff-engine.js +75 -15
  17. package/dist/commands/diff-engine.js.map +1 -1
  18. package/dist/commands/diff-geometry.d.ts +107 -0
  19. package/dist/commands/diff-geometry.d.ts.map +1 -0
  20. package/dist/commands/diff-geometry.js +262 -0
  21. package/dist/commands/diff-geometry.js.map +1 -0
  22. package/dist/commands/diff-lineage-io.d.ts +23 -0
  23. package/dist/commands/diff-lineage-io.d.ts.map +1 -0
  24. package/dist/commands/diff-lineage-io.js +119 -0
  25. package/dist/commands/diff-lineage-io.js.map +1 -0
  26. package/dist/commands/diff.d.ts.map +1 -1
  27. package/dist/commands/diff.js +34 -3
  28. package/dist/commands/diff.js.map +1 -1
  29. package/dist/commands/rekey.d.ts +27 -0
  30. package/dist/commands/rekey.d.ts.map +1 -0
  31. package/dist/commands/rekey.js +192 -0
  32. package/dist/commands/rekey.js.map +1 -0
  33. package/dist/commands/validate.d.ts.map +1 -1
  34. package/dist/commands/validate.js +2 -1
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/headless-backend.d.ts.map +1 -1
  37. package/dist/headless-backend.js +31 -23
  38. package/dist/headless-backend.js.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/query-overlay-relations.d.ts +8 -28
  42. package/dist/query-overlay-relations.d.ts.map +1 -1
  43. package/dist/query-overlay-relations.js +68 -78
  44. package/dist/query-overlay-relations.js.map +1 -1
  45. package/dist/query-overlay.d.ts +6 -2
  46. package/dist/query-overlay.d.ts.map +1 -1
  47. package/dist/query-overlay.js +61 -5
  48. package/dist/query-overlay.js.map +1 -1
  49. 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`, or `hbjson`
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":"AAmCA,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,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFnF"}
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
- * See `diff-engine.ts` for why this path is data-scope only.
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, stat, writeFile } from 'node:fs/promises';
14
- import { resolve } from 'node:path';
15
- import { createIdentityMapSidecar, diffModels, identityMapFromContentMatches, identityMapSidecarMismatches, keyAliasesFromSidecar, parseIdentityMapSidecar, serializeIdentityMapSidecar, } from '@ifc-lite/diff';
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, baseIdentity, headIdentity)
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(baseBytes, basePath);
32
- const headStore = await loadIfcBytes(headBytes, headPath);
33
- const diff = diffModels(buildFileFingerprints(baseStore), buildFileFingerprints(headStore), {
34
- // No meshes in Node: `data` is the honest description of what this path can
35
- // compare. See diff-engine.ts.
36
- scope: 'data',
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: incoming ? keyAliasesFromSidecar(incoming) : undefined,
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 = incoming ? countClaims(incoming) - applied.size : 0;
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, base, head) {
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, { base, head });
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