@ifc-lite/cli 0.26.0 → 0.28.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 (146) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/ask.d.ts +8 -0
  3. package/dist/commands/ask.d.ts.map +1 -1
  4. package/dist/commands/ask.js +10 -9
  5. package/dist/commands/ask.js.map +1 -1
  6. package/dist/commands/bsdd.d.ts.map +1 -1
  7. package/dist/commands/bsdd.js +49 -13
  8. package/dist/commands/bsdd.js.map +1 -1
  9. package/dist/commands/delivery-checks.d.ts +56 -0
  10. package/dist/commands/delivery-checks.d.ts.map +1 -0
  11. package/dist/commands/delivery-checks.js +222 -0
  12. package/dist/commands/delivery-checks.js.map +1 -0
  13. package/dist/commands/delivery-recipe.d.ts +23 -0
  14. package/dist/commands/delivery-recipe.d.ts.map +1 -0
  15. package/dist/commands/delivery-recipe.js +104 -0
  16. package/dist/commands/delivery-recipe.js.map +1 -0
  17. package/dist/commands/delivery-report.d.ts +51 -0
  18. package/dist/commands/delivery-report.d.ts.map +1 -0
  19. package/dist/commands/delivery-report.js +114 -0
  20. package/dist/commands/delivery-report.js.map +1 -0
  21. package/dist/commands/delivery.d.ts +2 -0
  22. package/dist/commands/delivery.d.ts.map +1 -0
  23. package/dist/commands/delivery.js +127 -0
  24. package/dist/commands/delivery.js.map +1 -0
  25. package/dist/commands/diff-classification-label.d.ts +12 -0
  26. package/dist/commands/diff-classification-label.d.ts.map +1 -0
  27. package/dist/commands/diff-classification-label.js +20 -0
  28. package/dist/commands/diff-classification-label.js.map +1 -0
  29. package/dist/commands/diff-engine.d.ts.map +1 -1
  30. package/dist/commands/diff-engine.js +33 -11
  31. package/dist/commands/diff-engine.js.map +1 -1
  32. package/dist/commands/diff-test-helpers.d.ts +12 -0
  33. package/dist/commands/diff-test-helpers.d.ts.map +1 -1
  34. package/dist/commands/diff-test-helpers.js +68 -0
  35. package/dist/commands/diff-test-helpers.js.map +1 -1
  36. package/dist/commands/diff-unit-scale-fixtures.d.ts +34 -0
  37. package/dist/commands/diff-unit-scale-fixtures.d.ts.map +1 -0
  38. package/dist/commands/diff-unit-scale-fixtures.js +94 -0
  39. package/dist/commands/diff-unit-scale-fixtures.js.map +1 -0
  40. package/dist/commands/export.d.ts +11 -0
  41. package/dist/commands/export.d.ts.map +1 -1
  42. package/dist/commands/export.js +9 -14
  43. package/dist/commands/export.js.map +1 -1
  44. package/dist/commands/gym.js +2 -2
  45. package/dist/commands/merge.d.ts.map +1 -1
  46. package/dist/commands/merge.js +8 -0
  47. package/dist/commands/merge.js.map +1 -1
  48. package/dist/commands/query-aggregation.d.ts +27 -4
  49. package/dist/commands/query-aggregation.d.ts.map +1 -1
  50. package/dist/commands/query-aggregation.js +67 -6
  51. package/dist/commands/query-aggregation.js.map +1 -1
  52. package/dist/commands/query-output.d.ts +7 -0
  53. package/dist/commands/query-output.d.ts.map +1 -1
  54. package/dist/commands/query-output.js +63 -27
  55. package/dist/commands/query-output.js.map +1 -1
  56. package/dist/commands/query-storey.d.ts +24 -0
  57. package/dist/commands/query-storey.d.ts.map +1 -0
  58. package/dist/commands/query-storey.js +71 -0
  59. package/dist/commands/query-storey.js.map +1 -0
  60. package/dist/commands/query.d.ts +2 -25
  61. package/dist/commands/query.d.ts.map +1 -1
  62. package/dist/commands/query.js +39 -158
  63. package/dist/commands/query.js.map +1 -1
  64. package/dist/commands/schedule-aggregate.d.ts +51 -0
  65. package/dist/commands/schedule-aggregate.d.ts.map +1 -0
  66. package/dist/commands/schedule-aggregate.js +121 -0
  67. package/dist/commands/schedule-aggregate.js.map +1 -0
  68. package/dist/commands/schedule-columns.d.ts +15 -0
  69. package/dist/commands/schedule-columns.d.ts.map +1 -0
  70. package/dist/commands/schedule-columns.js +63 -0
  71. package/dist/commands/schedule-columns.js.map +1 -0
  72. package/dist/commands/schedule-group.d.ts +65 -0
  73. package/dist/commands/schedule-group.d.ts.map +1 -0
  74. package/dist/commands/schedule-group.js +0 -0
  75. package/dist/commands/schedule-group.js.map +1 -0
  76. package/dist/commands/schedule-presets.d.ts +21 -0
  77. package/dist/commands/schedule-presets.d.ts.map +1 -0
  78. package/dist/commands/schedule-presets.js +62 -0
  79. package/dist/commands/schedule-presets.js.map +1 -0
  80. package/dist/commands/schedule-render-html.d.ts +13 -0
  81. package/dist/commands/schedule-render-html.d.ts.map +1 -0
  82. package/dist/commands/schedule-render-html.js +100 -0
  83. package/dist/commands/schedule-render-html.js.map +1 -0
  84. package/dist/commands/schedule-render-md.d.ts +12 -0
  85. package/dist/commands/schedule-render-md.d.ts.map +1 -0
  86. package/dist/commands/schedule-render-md.js +77 -0
  87. package/dist/commands/schedule-render-md.js.map +1 -0
  88. package/dist/commands/schedule-render.d.ts +51 -0
  89. package/dist/commands/schedule-render.d.ts.map +1 -0
  90. package/dist/commands/schedule-render.js +205 -0
  91. package/dist/commands/schedule-render.js.map +1 -0
  92. package/dist/commands/schedule-spec.d.ts +29 -0
  93. package/dist/commands/schedule-spec.d.ts.map +1 -0
  94. package/dist/commands/schedule-spec.js +111 -0
  95. package/dist/commands/schedule-spec.js.map +1 -0
  96. package/dist/commands/schedule.d.ts +23 -0
  97. package/dist/commands/schedule.d.ts.map +1 -0
  98. package/dist/commands/schedule.js +249 -0
  99. package/dist/commands/schedule.js.map +1 -0
  100. package/dist/commands/schema.d.ts.map +1 -1
  101. package/dist/commands/schema.js +192 -17
  102. package/dist/commands/schema.js.map +1 -1
  103. package/dist/commands/stats-aggregation.d.ts +31 -6
  104. package/dist/commands/stats-aggregation.d.ts.map +1 -1
  105. package/dist/commands/stats-aggregation.js +79 -11
  106. package/dist/commands/stats-aggregation.js.map +1 -1
  107. package/dist/commands/stats.d.ts.map +1 -1
  108. package/dist/commands/stats.js +4 -4
  109. package/dist/commands/stats.js.map +1 -1
  110. package/dist/commands/validate.d.ts +7 -4
  111. package/dist/commands/validate.d.ts.map +1 -1
  112. package/dist/commands/validate.js +9 -6
  113. package/dist/commands/validate.js.map +1 -1
  114. package/dist/commands/where-filter.d.ts +42 -0
  115. package/dist/commands/where-filter.d.ts.map +1 -0
  116. package/dist/commands/where-filter.js +88 -0
  117. package/dist/commands/where-filter.js.map +1 -0
  118. package/dist/energy-export.d.ts.map +1 -1
  119. package/dist/energy-export.js +12 -2
  120. package/dist/energy-export.js.map +1 -1
  121. package/dist/geometry-report.d.ts.map +1 -1
  122. package/dist/geometry-report.js +15 -3
  123. package/dist/geometry-report.js.map +1 -1
  124. package/dist/headless-backend.d.ts +20 -5
  125. package/dist/headless-backend.d.ts.map +1 -1
  126. package/dist/headless-backend.js +131 -90
  127. package/dist/headless-backend.js.map +1 -1
  128. package/dist/index.js +28 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/output.d.ts +13 -0
  131. package/dist/output.d.ts.map +1 -1
  132. package/dist/output.js +21 -0
  133. package/dist/output.js.map +1 -1
  134. package/dist/property-filter-match.d.ts +12 -0
  135. package/dist/property-filter-match.d.ts.map +1 -0
  136. package/dist/property-filter-match.js +17 -0
  137. package/dist/property-filter-match.js.map +1 -0
  138. package/dist/query-overlay-relations.d.ts +29 -0
  139. package/dist/query-overlay-relations.d.ts.map +1 -0
  140. package/dist/query-overlay-relations.js +88 -0
  141. package/dist/query-overlay-relations.js.map +1 -0
  142. package/dist/query-overlay.d.ts +44 -0
  143. package/dist/query-overlay.d.ts.map +1 -0
  144. package/dist/query-overlay.js +100 -0
  145. package/dist/query-overlay.js.map +1 -0
  146. package/package.json +21 -21
@@ -0,0 +1,205 @@
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
+ * CSV / JSON rendering for `ifc-lite schedule`.
6
+ *
7
+ * A schedule is a list of header names plus one raw value per (row, column).
8
+ * CSV routes every cell through `escapeCsvCell` (RFC 4180 quoting + the
9
+ * spreadsheet-formula-injection guard) shared with the `export` command; a
10
+ * missing value renders as an empty CSV cell and as JSON `null`.
11
+ */
12
+ import { escapeCsvCell } from '@ifc-lite/export';
13
+ import { columnValueToCsv } from './export.js';
14
+ /**
15
+ * Render the schedule as RFC-4180 CSV. Headers and cells both go through the
16
+ * shared escaper so a header or value containing a comma, quote, newline, or a
17
+ * leading formula trigger is correctly quoted/neutralised.
18
+ */
19
+ export function renderScheduleCsv(columns, rows) {
20
+ const sep = ',';
21
+ const headerLine = columns.map(c => escapeCsvCell(c.header, { delimiter: sep })).join(sep);
22
+ const dataLines = rows.map(row => row.map(value => escapeCsvCell(columnValueToCsv(value), { delimiter: sep })).join(sep));
23
+ return [headerLine, ...dataLines].join('\n');
24
+ }
25
+ /**
26
+ * Render the schedule as a JSON array of row objects keyed by header. Raw
27
+ * values keep their type; a missing value is `null`.
28
+ */
29
+ export function renderScheduleJson(columns, rows) {
30
+ return rows.map(row => scheduleRowToJson(columns, row));
31
+ }
32
+ /** One data row as a header-keyed object; a missing value is `null`. */
33
+ function scheduleRowToJson(columns, row) {
34
+ const obj = {};
35
+ columns.forEach((col, i) => {
36
+ const value = row[i];
37
+ obj[col.header] = value == null ? null : value;
38
+ });
39
+ return obj;
40
+ }
41
+ /**
42
+ * The subtotal/total label for a group: `Subtotal (H1=v1, H2=v2)` or `Total`,
43
+ * suffixed with `: <n>` when a `count` aggregation is present (the row count of
44
+ * the group / whole schedule).
45
+ */
46
+ function subtotalLabel(data) {
47
+ const base = data.kind === 'total'
48
+ ? 'Total'
49
+ : `Subtotal (${data.groupValues.map(g => `${g.header}=${g.value == null ? '' : String(g.value)}`).join(', ')})`;
50
+ const count = data.values.find(v => v.spec === 'count');
51
+ return count ? `${base}: ${count.value}` : base;
52
+ }
53
+ /**
54
+ * Pick the column that carries the subtotal/total label. A forced choice
55
+ * (the first group column, or column 0 for the grand total) can collide with
56
+ * a `--subtotals` aggregation targeting that same column — `sum:Area` when
57
+ * `Area` is the first declared column, or the first group-by header — and a
58
+ * fixed-width row array has no spare slot to hold both a text label and a
59
+ * numeric value in the same cell. Prefer, in order: the first group column
60
+ * that is not itself an aggregation target (keeps the label meaningfully
61
+ * tied to a group value), then the first column overall that is not a
62
+ * target, and only when every column is a target — no free slot exists at
63
+ * all — fall back to the historical group/0 choice (that one column's
64
+ * aggregation is what the label displaces; every other aggregation still
65
+ * lands in its own column).
66
+ */
67
+ function chooseLabelCol(columns, data) {
68
+ const targetIdx = new Set();
69
+ for (const v of data.values) {
70
+ if (v.spec === 'count')
71
+ continue;
72
+ const header = v.spec.slice(v.spec.indexOf(':') + 1);
73
+ const idx = columns.findIndex(c => c.header === header);
74
+ if (idx !== -1)
75
+ targetIdx.add(idx);
76
+ }
77
+ for (const g of data.groupValues) {
78
+ if (!targetIdx.has(g.index))
79
+ return g.index;
80
+ }
81
+ for (let i = 0; i < columns.length; i++) {
82
+ if (!targetIdx.has(i))
83
+ return i;
84
+ }
85
+ return data.groupValues.length > 0 ? data.groupValues[0].index : 0;
86
+ }
87
+ /**
88
+ * A subtotal/total row as a full-width cell array. The label sits in a
89
+ * column none of this row's aggregations target (see `chooseLabelCol`); each
90
+ * numeric aggregation fills its own target column; every other column is
91
+ * blank.
92
+ *
93
+ * `--subtotals` allows more than one aggregation on the same `--columns`
94
+ * header (`sum:Area,avg:Area` is a reasonable request, and the JSON renderer
95
+ * already keys those by `spec` without collision — see `subtotalToJson`).
96
+ * The tabular formats have only one cell per column, so when two or more
97
+ * aggregations target the same column their values are joined into one cell
98
+ * as `"sum: 30, avg: 15"` instead of the second one silently overwriting the
99
+ * first. A single aggregation on a column keeps rendering as its bare value
100
+ * (a number, same as before) so the common case is unaffected.
101
+ *
102
+ * When every declared column is an aggregation target there is no free slot
103
+ * for the label at all (`--columns Area --subtotals sum:Area`), so the label
104
+ * takes a targeted column. Those displaced aggregations are appended to the
105
+ * label cell as `Total (sum: 30, avg: 15)` rather than dropped, which is what
106
+ * used to happen: the tabular formats printed a bare `Total` while JSON still
107
+ * reported the values.
108
+ *
109
+ * Exported so the Markdown/HTML renderers (`schedule-render-md.ts`,
110
+ * `schedule-render-html.ts`) lay out subtotal/total rows identically to CSV —
111
+ * same label, same column placement — instead of re-deriving the shape.
112
+ */
113
+ export function subtotalCells(columns, data) {
114
+ const cells = columns.map(() => '');
115
+ const labelCol = chooseLabelCol(columns, data);
116
+ // Group every non-count aggregation by its target column index so a
117
+ // collision can be detected before any cell is written. Aggregations
118
+ // targeting the label's own column are kept too (under `labelCol`), to be
119
+ // folded into the label text rather than dropped.
120
+ const byIdx = new Map();
121
+ for (const v of data.values) {
122
+ if (v.spec === 'count')
123
+ continue;
124
+ const header = v.spec.slice(v.spec.indexOf(':') + 1);
125
+ const idx = columns.findIndex(c => c.header === header);
126
+ if (idx === -1)
127
+ continue; // header is not a declared column: nowhere to put it
128
+ const bucket = byIdx.get(idx);
129
+ if (bucket)
130
+ bucket.push(v);
131
+ else
132
+ byIdx.set(idx, [v]);
133
+ }
134
+ const label = subtotalLabel(data);
135
+ const displaced = byIdx.get(labelCol);
136
+ cells[labelCol] = displaced ? `${label} (${joinAggValues(displaced)})` : label;
137
+ for (const [idx, values] of byIdx) {
138
+ if (idx === labelCol)
139
+ continue; // already folded into the label cell
140
+ if (values.length === 1) {
141
+ cells[idx] = values[0].value == null ? '' : values[0].value;
142
+ continue;
143
+ }
144
+ // Two-plus aggregations on one column: join as "mode: value" pairs so
145
+ // every value survives instead of the last one winning.
146
+ cells[idx] = joinAggValues(values);
147
+ }
148
+ return cells;
149
+ }
150
+ /** `sum:Area`/`avg:Area` values as the shared `"sum: 30, avg: 15"` cell text. */
151
+ function joinAggValues(values) {
152
+ return values
153
+ .map(v => `${v.spec.slice(0, v.spec.indexOf(':'))}: ${v.value == null ? '' : v.value}`)
154
+ .join(', ');
155
+ }
156
+ /**
157
+ * Render a schedule with `--subtotals`. Grouped output emits each contiguous
158
+ * group's rows followed by its subtotal row; the grand total closes the table.
159
+ * Without `--group-by` only the flat rows and the grand total are emitted.
160
+ */
161
+ export function renderScheduleCsvWithSubtotals(columns, plan) {
162
+ const sep = ',';
163
+ const escapeRow = (row) => row.map(value => escapeCsvCell(columnValueToCsv(value), { delimiter: sep })).join(sep);
164
+ const lines = [columns.map(c => escapeCsvCell(c.header, { delimiter: sep })).join(sep)];
165
+ if (plan.groups.length > 0) {
166
+ for (const group of plan.groups) {
167
+ for (const row of group.rows)
168
+ lines.push(escapeRow(row));
169
+ lines.push(escapeRow(subtotalCells(columns, group.subtotal)));
170
+ }
171
+ }
172
+ else {
173
+ for (const row of plan.rows)
174
+ lines.push(escapeRow(row));
175
+ }
176
+ lines.push(escapeRow(subtotalCells(columns, plan.total)));
177
+ return lines.join('\n');
178
+ }
179
+ /** The JSON form of a subtotal/total row: `__row` marker + group + aggregated fields. */
180
+ function subtotalToJson(data) {
181
+ const obj = { __row: data.kind };
182
+ for (const g of data.groupValues)
183
+ obj[g.header] = g.value == null ? null : g.value;
184
+ for (const v of data.values)
185
+ obj[v.spec] = v.value;
186
+ return obj;
187
+ }
188
+ /** Render a schedule with `--subtotals` as flat JSON rows plus `__row`-marked subtotal/total objects. */
189
+ export function renderScheduleJsonWithSubtotals(columns, plan) {
190
+ const out = [];
191
+ if (plan.groups.length > 0) {
192
+ for (const group of plan.groups) {
193
+ for (const row of group.rows)
194
+ out.push(scheduleRowToJson(columns, row));
195
+ out.push(subtotalToJson(group.subtotal));
196
+ }
197
+ }
198
+ else {
199
+ for (const row of plan.rows)
200
+ out.push(scheduleRowToJson(columns, row));
201
+ }
202
+ out.push(subtotalToJson(plan.total));
203
+ return out;
204
+ }
205
+ //# sourceMappingURL=schedule-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-render.js","sourceRoot":"","sources":["../../src/commands/schedule-render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /** The reusable definition persisted by `--save` and consumed by `--spec`. */
2
+ export interface ScheduleSpec {
3
+ preset?: string;
4
+ type?: string;
5
+ columns?: string;
6
+ where?: string;
7
+ sort?: string;
8
+ groupBy?: string;
9
+ subtotals?: string;
10
+ format?: string;
11
+ }
12
+ /**
13
+ * Load a `--spec` file. Fatal (not a thrown exception the top-level handler
14
+ * reformats) on every way a spec can fail to be a usable schedule definition:
15
+ * the file doesn't exist or isn't readable, its content isn't valid JSON,
16
+ * isn't a JSON object, or has a declared field that isn't a string — each
17
+ * would otherwise surface only once `schedule.ts` tries to use the bad value
18
+ * (or, for a non-object/wrong-type field, might silently coerce into an empty
19
+ * `--columns`/`--type`, producing an empty schedule with no error at all).
20
+ */
21
+ export declare function loadScheduleSpec(path: string): Promise<ScheduleSpec>;
22
+ /**
23
+ * Write the schedule definition actually used by this invocation (after
24
+ * `--preset` defaults are folded in, so a saved spec is self-contained and
25
+ * `--spec`-loadable without also needing the original `--preset`) to `path`.
26
+ * Undefined fields are omitted rather than written as `null`.
27
+ */
28
+ export declare function saveScheduleSpec(path: string, spec: ScheduleSpec): Promise<void>;
29
+ //# sourceMappingURL=schedule-spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-spec.d.ts","sourceRoot":"","sources":["../../src/commands/schedule-spec.ts"],"names":[],"mappings":"AA6BA,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAwD1E;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAWtF"}
@@ -0,0 +1,111 @@
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
+ * `--save`/`--spec` reusable schedule definitions for `ifc-lite schedule`.
6
+ *
7
+ * A spec is a small JSON file holding exactly the flag strings the CLI itself
8
+ * accepts — `--type`, `--columns`, `--where`, `--sort`, `--group-by`,
9
+ * `--subtotals`, `--format` — plus an optional `preset` field naming a
10
+ * `--preset` to start from. It is a plain file at whatever path the caller
11
+ * gives `--spec`/`--save`, not a config-directory convention: `--save` writes
12
+ * one, `--spec` reads one back.
13
+ *
14
+ * Precedence when both a spec and an explicit flag are given (`schedule.ts`):
15
+ * explicit CLI flag > spec field > preset field (whether `--preset` on the
16
+ * command line or `preset` inside the spec) — the same "most specific wins"
17
+ * rule `--preset` already uses against explicit flags.
18
+ *
19
+ * A spec naming an unresolvable `preset`, or missing/malformed in a way that
20
+ * would otherwise silently resolve to an empty schedule, is a `fatal(...)`:
21
+ * failing loudly beats emitting a table with no rows or columns.
22
+ */
23
+ import { readFile, writeFile } from 'node:fs/promises';
24
+ import { fatal } from '../output.js';
25
+ import { logger } from '../logger.js';
26
+ import { parseWhereFilter } from './where-filter.js';
27
+ const SPEC_FIELDS = [
28
+ 'preset', 'type', 'columns', 'where', 'sort', 'groupBy', 'subtotals', 'format',
29
+ ];
30
+ /**
31
+ * Load a `--spec` file. Fatal (not a thrown exception the top-level handler
32
+ * reformats) on every way a spec can fail to be a usable schedule definition:
33
+ * the file doesn't exist or isn't readable, its content isn't valid JSON,
34
+ * isn't a JSON object, or has a declared field that isn't a string — each
35
+ * would otherwise surface only once `schedule.ts` tries to use the bad value
36
+ * (or, for a non-object/wrong-type field, might silently coerce into an empty
37
+ * `--columns`/`--type`, producing an empty schedule with no error at all).
38
+ */
39
+ export async function loadScheduleSpec(path) {
40
+ let content;
41
+ try {
42
+ content = await readFile(path, 'utf-8');
43
+ }
44
+ catch (err) {
45
+ fatal(`--spec "${path}" could not be read: ${err.message}`);
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(content);
50
+ }
51
+ catch (err) {
52
+ fatal(`--spec "${path}" is not valid JSON: ${err.message}`);
53
+ }
54
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
55
+ fatal(`--spec "${path}" must be a JSON object with schedule fields (type, columns, where, sort, groupBy, subtotals, format, preset), got ${Array.isArray(parsed) ? 'an array' : typeof parsed}.`);
56
+ }
57
+ const obj = parsed;
58
+ // An unrecognised key (a mistyped "group-by" instead of "groupBy", say) used
59
+ // to be silently ignored by the loop below, which only ever reads the
60
+ // fields it already knows about — the mistyped key had no effect and the
61
+ // command exited 0 with whatever the OTHER, correctly-spelled fields
62
+ // produced (an ungrouped schedule, for a mistyped "groupBy"), never
63
+ // surfacing that the field was dropped. Reject it up front instead.
64
+ const unknownKeys = Object.keys(obj).filter(k => !SPEC_FIELDS.includes(k));
65
+ if (unknownKeys.length > 0) {
66
+ fatal(`--spec "${path}" has unrecognised field(s): ${unknownKeys.join(', ')}. Valid fields: ${SPEC_FIELDS.join(', ')}.`);
67
+ }
68
+ const spec = {};
69
+ for (const field of SPEC_FIELDS) {
70
+ if (!(field in obj))
71
+ continue;
72
+ const value = obj[field];
73
+ if (typeof value !== 'string') {
74
+ fatal(`--spec "${path}": field "${field}" must be a string, got ${Array.isArray(value) ? 'an array' : typeof value}.`);
75
+ }
76
+ spec[field] = value;
77
+ }
78
+ if (spec.preset === undefined && spec.type === undefined) {
79
+ fatal(`--spec "${path}" declares neither "preset" nor "type" — nothing to schedule. Expected at least one, plus "columns" unless a preset supplies it.`);
80
+ }
81
+ // Validate `where` the same way an explicit --where flag is (schedule.ts
82
+ // calls the identical parseWhereFilter once it applies the filter) —
83
+ // eagerly here, rather than deferring the same fatal(...) until the run
84
+ // gets around to using it (by which point --save, if also given, may
85
+ // already have written a spec built from this same broken value).
86
+ if (spec.where !== undefined) {
87
+ parseWhereFilter(spec.where);
88
+ }
89
+ return spec;
90
+ }
91
+ /**
92
+ * Write the schedule definition actually used by this invocation (after
93
+ * `--preset` defaults are folded in, so a saved spec is self-contained and
94
+ * `--spec`-loadable without also needing the original `--preset`) to `path`.
95
+ * Undefined fields are omitted rather than written as `null`.
96
+ */
97
+ export async function saveScheduleSpec(path, spec) {
98
+ const out = {};
99
+ for (const field of SPEC_FIELDS) {
100
+ if (spec[field] !== undefined)
101
+ out[field] = spec[field];
102
+ }
103
+ try {
104
+ await writeFile(path, JSON.stringify(out, null, 2) + '\n');
105
+ }
106
+ catch (err) {
107
+ fatal(`--save "${path}" could not be written: ${err.message}`);
108
+ }
109
+ logger.info(`Schedule spec written to ${path}`);
110
+ }
111
+ //# sourceMappingURL=schedule-spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-spec.js","sourceRoot":"","sources":["../../src/commands/schedule-spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { type ScheduleColumn } from './schedule-columns.js';
2
+ import { type ScheduleRow } from './schedule-render.js';
3
+ /**
4
+ * Resolve one column path against one entity.
5
+ *
6
+ * A `Set.Member` path (and the five native attributes) goes through the shared
7
+ * `resolveColumnValue` the `export` command uses, so schedule columns inherit
8
+ * its type-inheritance / same-named-pset / complex-property behaviour and agree
9
+ * with `--where`. A plain attribute name the shared resolver does not treat as
10
+ * native (e.g. `Tag`, `PredefinedType`) is then read from the canonical
11
+ * entity-attribute list — the same surface `query --attributes` exposes — so a
12
+ * missing value stays `null`.
13
+ */
14
+ export declare function resolveScheduleValue(entity: any, path: string, bim: any): unknown;
15
+ /**
16
+ * Build one row per entity, resolving every declared column. Reads
17
+ * properties/quantities/attributes/materials at most once per entity
18
+ * (see `memoizedColumnAccessors`) — measured 5.6x on FM_ARC_DigitalHub,
19
+ * 6 columns: 40.6ms -> 7.2ms per row build.
20
+ */
21
+ export declare function buildScheduleRows(entities: any[], columns: ScheduleColumn[], bim: any): ScheduleRow[];
22
+ export declare function scheduleCommand(args: string[]): Promise<void>;
23
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/commands/schedule.ts"],"names":[],"mappings":"AAmDA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAW9B;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CA2DjF;AAgCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,WAAW,EAAE,CAGrG;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiHnE"}
@@ -0,0 +1,249 @@
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
+ * ifc-lite schedule <file.ifc> [--preset <name>] --type <IfcClass>
6
+ * --columns "<Header>=<path>[, ...]"
7
+ * [--where "<expr>"] [--sort "<Header>[:asc|desc][, ...]"]
8
+ * [--group-by "<Header>[, ...]"]
9
+ * [--subtotals "<agg>[, ...]"] [--format csv|json|md|html]
10
+ * [--spec <file.json>] [--save <file.json>]
11
+ *
12
+ * `--preset door|window|space|wall|material-takeoff` supplies a default
13
+ * `--type` and `--columns` (and, for some presets, a default group-by/sort/
14
+ * subtotals) so a common schedule runs with no other flags. An explicit flag of
15
+ * the same name overrides the preset's default; `--where`/`--format` apply as
16
+ * normal. See `schedule-presets.ts`.
17
+ *
18
+ * `--spec <file.json>` loads a saved schedule definition (see
19
+ * `schedule-spec.ts`) supplying the same defaults a preset would (lower
20
+ * priority than an explicit flag, higher priority than `--preset`); `--save
21
+ * <file.json>` writes the definition this invocation resolved to — after any
22
+ * `--preset`/`--spec` defaults are folded in — so it can be re-run later with
23
+ * `--spec` alone. A spec that names an unresolvable preset, isn't valid JSON,
24
+ * or is missing both `type` and `preset` is a `fatal(...)`, never a silent
25
+ * empty schedule.
26
+ *
27
+ * Produce a tabular schedule of one IFC class: pick the entities of `--type`,
28
+ * (optionally) narrow them with the same `--where` filter `query` uses, and
29
+ * emit one row per entity with the requested columns. A column path is a plain
30
+ * attribute name, a `PsetName.PropName`, or a `QtoName.QtyName`, resolved
31
+ * through the SAME resolver the `export` command uses so schedule columns and
32
+ * query filters agree on type-inheritance / same-named-pset / complex-property
33
+ * behaviour. `--format md` emits a GFM table; `--format html` emits a
34
+ * standalone HTML document — both lay out `--group-by`/`--subtotals` rows
35
+ * identically to CSV/JSON (see `schedule-render-md.ts`/`schedule-render-html.ts`)
36
+ * and escape cell text for their format (`|`/newline for Markdown;
37
+ * `&`/`<`/`>`/quotes for HTML — model text is untrusted).
38
+ *
39
+ * `--sort`, `--group-by` and `--subtotals` all name column HEADERS (never
40
+ * paths); a header that is not among `--columns` is a `fatal(...)`. Rows are
41
+ * ordered by group first (so each group is contiguous) then by the remaining
42
+ * sort keys, with the original entity order as the stable tiebreaker.
43
+ * `--subtotals` emits a subtotal row after each group and a grand-total row;
44
+ * without `--group-by` it emits only the grand total.
45
+ */
46
+ import { createHeadlessContext } from '../loader.js';
47
+ import { getFlag, fatal, printJson, firstNonBlank, isBlank } from '../output.js';
48
+ import { normalizeTypeName, parseWhereFilter, applyWhereFilter } from './query.js';
49
+ import { resolveColumnValue } from './export.js';
50
+ import { parseColumnSpec } from './schedule-columns.js';
51
+ import { renderScheduleCsv, renderScheduleJson, renderScheduleCsvWithSubtotals, renderScheduleJsonWithSubtotals, } from './schedule-render.js';
52
+ import { renderScheduleMarkdown, renderScheduleMarkdownWithSubtotals } from './schedule-render-md.js';
53
+ import { renderScheduleHtml, renderScheduleHtmlWithSubtotals } from './schedule-render-html.js';
54
+ import { parseSortSpec, parseGroupBySpec, orderRows } from './schedule-group.js';
55
+ import { parseSubtotalsSpec, buildSubtotalPlan } from './schedule-aggregate.js';
56
+ import { resolvePreset } from './schedule-presets.js';
57
+ import { loadScheduleSpec, saveScheduleSpec } from './schedule-spec.js';
58
+ const FORMATS = ['csv', 'json', 'md', 'html'];
59
+ /**
60
+ * Resolve one column path against one entity.
61
+ *
62
+ * A `Set.Member` path (and the five native attributes) goes through the shared
63
+ * `resolveColumnValue` the `export` command uses, so schedule columns inherit
64
+ * its type-inheritance / same-named-pset / complex-property behaviour and agree
65
+ * with `--where`. A plain attribute name the shared resolver does not treat as
66
+ * native (e.g. `Tag`, `PredefinedType`) is then read from the canonical
67
+ * entity-attribute list — the same surface `query --attributes` exposes — so a
68
+ * missing value stays `null`.
69
+ */
70
+ export function resolveScheduleValue(entity, path, bim) {
71
+ // `Material` is a pseudo-column: resolve the element's associated material
72
+ // name through the same fallback chain `packages/mcp/src/material-naming.ts`
73
+ // `materialFallbackName` applies (duplicated locally the same way
74
+ // `firstNonBlank`/`isBlank` already are in `output.ts` — a cross-package
75
+ // import would widen `@ifc-lite/mcp`'s public surface for one helper). A
76
+ // plain `.name`/`.materials[0]` pair (the pre-fix behaviour) only covers a
77
+ // bare `IfcMaterial` or `IfcMaterialList`: the normal `IfcWall` shape is a
78
+ // `MaterialLayerSet` with no top-level `LayerSetName`, whose real name
79
+ // lives on `layers[]`/`profiles[]`/`constituents[]` — reading only
80
+ // `.name`/`.materials[0]` returns `null` for every such wall, and
81
+ // `--preset material-takeoff`'s default `--group-by Material` collapses
82
+ // them into one unlabelled group instead of one group per real material.
83
+ // A whitespace-only leaf name falls through the same way (#3714's family),
84
+ // and a fully blank result renders `(no material)` — the same placeholder
85
+ // `query --group-by material` uses — rather than a silent empty cell, so a
86
+ // group-by/subtotal on `Material` buckets blank elements together under a
87
+ // readable label instead of splitting on `null` vs `''` vs whitespace.
88
+ if (path === 'Material') {
89
+ const mat = bim.materials(entity.ref);
90
+ if (!mat)
91
+ return '(no material)';
92
+ return (firstNonBlank(mat.name, mat.materials?.[0]?.name, mat.layers?.find(l => !isBlank(l.materialName))?.materialName, mat.profiles?.find(p => !isBlank(p.materialName))?.materialName, mat.constituents?.find(c => !isBlank(c.materialName))?.materialName) ?? '(no material)');
93
+ }
94
+ // A dot-free path checks the canonical entity-attribute list FIRST.
95
+ // resolveColumnValue's own bare-name fallback (below) sweeps EVERY property
96
+ // and quantity set for a member with that literal name — a user-authored
97
+ // pset property that happens to share a schema attribute's name (e.g. a
98
+ // "Tag" property in some custom Pset) would otherwise shadow the real
99
+ // IfcDoor.Tag attribute that a column like --preset door's `Mark=Tag`
100
+ // means. A dotted `Pset.Prop`/`Qto.Qty` path is unambiguous and always
101
+ // goes straight through resolveColumnValue.
102
+ if (!path.includes('.')) {
103
+ const attrs = bim.attributes(entity.ref);
104
+ const hit = attrs.find(a => a.name === path);
105
+ if (hit && hit.value != null)
106
+ return hit.value;
107
+ }
108
+ const viaColumn = resolveColumnValue(entity, path, bim);
109
+ if (viaColumn != null)
110
+ return viaColumn;
111
+ return null;
112
+ }
113
+ /** Wrap a single-argument function so a repeated call with the SAME argument (by `===`) is served from cache instead of re-invoked. */
114
+ function memoize(fn) {
115
+ const cache = new Map();
116
+ return (arg) => {
117
+ const hit = cache.get(arg);
118
+ if (hit !== undefined || cache.has(arg))
119
+ return hit;
120
+ const result = fn(arg);
121
+ cache.set(arg, result);
122
+ return result;
123
+ };
124
+ }
125
+ /**
126
+ * Wrap `bim` so `properties`/`quantities`/`attributes`/`materials` — the only
127
+ * four methods `resolveScheduleValue` (and the `resolveColumnValue` it
128
+ * delegates to) ever call — are read at most ONCE per entity ref for the
129
+ * whole schedule, however many `--columns` request them. Resolving each
130
+ * column independently means an N-column row calls e.g. `bim.properties(ref)`
131
+ * N times for the identical entity, re-walking the same property-set list
132
+ * from scratch each time.
133
+ */
134
+ function memoizedColumnAccessors(bim) {
135
+ return {
136
+ properties: memoize((ref) => bim.properties(ref)),
137
+ quantities: memoize((ref) => bim.quantities(ref)),
138
+ attributes: memoize((ref) => bim.attributes(ref)),
139
+ materials: memoize((ref) => bim.materials(ref)),
140
+ };
141
+ }
142
+ /**
143
+ * Build one row per entity, resolving every declared column. Reads
144
+ * properties/quantities/attributes/materials at most once per entity
145
+ * (see `memoizedColumnAccessors`) — measured 5.6x on FM_ARC_DigitalHub,
146
+ * 6 columns: 40.6ms -> 7.2ms per row build.
147
+ */
148
+ export function buildScheduleRows(entities, columns, bim) {
149
+ const cached = memoizedColumnAccessors(bim);
150
+ return entities.map((e) => columns.map(col => resolveScheduleValue(e, col.path, cached)));
151
+ }
152
+ export async function scheduleCommand(args) {
153
+ const filePath = args.find(a => !a.startsWith('-'));
154
+ if (!filePath) {
155
+ fatal('Usage: ifc-lite schedule <file.ifc> --type IfcDoor --columns "Name=Name, Mark=Pset_DoorCommon.Reference" [--where PsetName.Prop=Value] [--format csv|json]');
156
+ }
157
+ // --spec loads a saved definition (see schedule-spec.ts); its fields sit
158
+ // between an explicit flag (wins) and a --preset default (loses) in
159
+ // precedence, whether the preset comes from --preset or the spec's own
160
+ // "preset" field.
161
+ const specPath = getFlag(args, '--spec');
162
+ const spec = specPath ? await loadScheduleSpec(specPath) : undefined;
163
+ // --preset supplies default type/columns (and optionally group-by/sort/
164
+ // subtotals); an explicit flag of the same name overrides the preset default.
165
+ const presetName = getFlag(args, '--preset') ?? spec?.preset;
166
+ const preset = presetName ? resolvePreset(presetName) : undefined;
167
+ const typeRaw = getFlag(args, '--type') ?? spec?.type ?? preset?.type;
168
+ if (!typeRaw) {
169
+ fatal('--type is required (or pass --preset/--spec), e.g. --type IfcDoor or --preset door');
170
+ }
171
+ const type = normalizeTypeName(typeRaw);
172
+ const columnsSpec = getFlag(args, '--columns') ?? spec?.columns ?? preset?.columns;
173
+ const columns = parseColumnSpec(columnsSpec);
174
+ const format = (getFlag(args, '--format') ?? spec?.format ?? 'csv').toLowerCase();
175
+ if (!FORMATS.includes(format)) {
176
+ fatal(`Unknown --format "${format}". Expected ${FORMATS.join(', ')}.`);
177
+ }
178
+ const whereFilter = getFlag(args, '--where') ?? spec?.where;
179
+ // --sort / --group-by / --subtotals name declared column HEADERS; an unknown
180
+ // header is a fatal(...) with the valid headers listed (raised during parse).
181
+ const sortSpec = getFlag(args, '--sort') ?? spec?.sort ?? preset?.sort;
182
+ const groupBySpec = getFlag(args, '--group-by') ?? spec?.groupBy ?? preset?.groupBy;
183
+ const subtotalsSpec = getFlag(args, '--subtotals') ?? spec?.subtotals ?? preset?.subtotals;
184
+ const sortKeys = parseSortSpec(sortSpec, columns);
185
+ const groupKeys = parseGroupBySpec(groupBySpec, columns);
186
+ const subtotalAggs = parseSubtotalsSpec(subtotalsSpec, columns);
187
+ // --save's target path is captured here (so --save itself is validated up
188
+ // front, same as every other flag) but the file is written only once this
189
+ // run has actually succeeded — see below.
190
+ const savePath = getFlag(args, '--save');
191
+ const { bim } = await createHeadlessContext(filePath);
192
+ let entities = bim.query().byType(...type.split(',')).toArray();
193
+ // --where reuses the exact query filter (property sets + quantity fallback).
194
+ if (whereFilter) {
195
+ const parsed = parseWhereFilter(whereFilter);
196
+ entities = applyWhereFilter(entities, parsed, bim);
197
+ }
198
+ // One row per entity, in entity order — the stable tiebreaker for ordering.
199
+ let rows = buildScheduleRows(entities, columns, bim);
200
+ // Order once, after row assembly: groups contiguous, then remaining sort keys.
201
+ if (sortKeys.length > 0 || groupKeys.length > 0) {
202
+ rows = orderRows(rows, sortKeys, groupKeys);
203
+ }
204
+ // --save persists the definition this invocation actually resolved to
205
+ // (after --preset/--spec defaults are folded in), so the written file is
206
+ // self-contained and reloadable with --spec alone. Written only here, once
207
+ // --type/--where/--sort/--group-by/--subtotals have all parsed AND the
208
+ // input file has loaded successfully — writing it any earlier (its
209
+ // original position, before --where was even parsed and before the input
210
+ // file was opened) could persist a broken spec despite the run's own
211
+ // non-zero exit.
212
+ if (savePath) {
213
+ await saveScheduleSpec(savePath, {
214
+ type, columns: columnsSpec, where: whereFilter,
215
+ sort: sortSpec, groupBy: groupBySpec, subtotals: subtotalsSpec, format,
216
+ });
217
+ }
218
+ if (subtotalAggs.length > 0) {
219
+ const plan = buildSubtotalPlan(rows, groupKeys, subtotalAggs);
220
+ if (format === 'json') {
221
+ printJson(renderScheduleJsonWithSubtotals(columns, plan));
222
+ return;
223
+ }
224
+ if (format === 'md') {
225
+ process.stdout.write(renderScheduleMarkdownWithSubtotals(columns, plan) + '\n');
226
+ return;
227
+ }
228
+ if (format === 'html') {
229
+ process.stdout.write(renderScheduleHtmlWithSubtotals(columns, plan));
230
+ return;
231
+ }
232
+ process.stdout.write(renderScheduleCsvWithSubtotals(columns, plan) + '\n');
233
+ return;
234
+ }
235
+ if (format === 'json') {
236
+ printJson(renderScheduleJson(columns, rows));
237
+ return;
238
+ }
239
+ if (format === 'md') {
240
+ process.stdout.write(renderScheduleMarkdown(columns, rows) + '\n');
241
+ return;
242
+ }
243
+ if (format === 'html') {
244
+ process.stdout.write(renderScheduleHtml(columns, rows));
245
+ return;
246
+ }
247
+ process.stdout.write(renderScheduleCsv(columns, rows) + '\n');
248
+ }
249
+ //# sourceMappingURL=schedule.js.map