@bldrs-ai/conway 1.389.1221 → 1.398.1224

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.
@@ -116,6 +116,20 @@ export default class StepParser<TypeIDType> extends StepHeaderParser {
116
116
  * @return {BlockParseResult} The parsing result, including the index and result enum.
117
117
  */
118
118
  parseDataBlock(input: ParsingBuffer, onProgress?: ParseProgressCallback): BlockParseResult<TypeIDType>;
119
+ /**
120
+ * Streaming driver over parseDataBlockIncremental: identical parse (same
121
+ * generator body) but invokes `onRecordBoundary` at every top-level record
122
+ * boundary so a caller feeding the parser from a moving window can slide
123
+ * that window forward while the rewind stack is empty. See
124
+ * streaming_index_builder.ts for the coordinator that owns the window.
125
+ *
126
+ * @param input The input parsing buffer, positioned at the data section.
127
+ * @param onRecordBoundary Called at each top-level record boundary with the
128
+ * buffer; the callback may rebase the buffer's window in place.
129
+ * @param onProgress Optional byte-cursor progress callback.
130
+ * @return {BlockParseResult} The parsing result, including the index and result enum.
131
+ */
132
+ parseDataBlockStreamed(input: ParsingBuffer, onRecordBoundary: (input: ParsingBuffer) => void, onProgress?: ParseProgressCallback): BlockParseResult<TypeIDType>;
119
133
  /**
120
134
  * Cooperative variant of parseDataBlock: identical parse (same generator
121
135
  * body), but periodically awaits a macrotask so the event loop can run —
@@ -1 +1 @@
1
- {"version":3,"file":"step_parser.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/step_parser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"step_parser.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/step_parser.ts"],"names":[],"mappings":"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"}
@@ -310,6 +310,29 @@ export default class StepParser extends StepHeaderParser {
310
310
  onProgress?.(next.value);
311
311
  }
312
312
  }
313
+ /**
314
+ * Streaming driver over parseDataBlockIncremental: identical parse (same
315
+ * generator body) but invokes `onRecordBoundary` at every top-level record
316
+ * boundary so a caller feeding the parser from a moving window can slide
317
+ * that window forward while the rewind stack is empty. See
318
+ * streaming_index_builder.ts for the coordinator that owns the window.
319
+ *
320
+ * @param input The input parsing buffer, positioned at the data section.
321
+ * @param onRecordBoundary Called at each top-level record boundary with the
322
+ * buffer; the callback may rebase the buffer's window in place.
323
+ * @param onProgress Optional byte-cursor progress callback.
324
+ * @return {BlockParseResult} The parsing result, including the index and result enum.
325
+ */
326
+ parseDataBlockStreamed(input, onRecordBoundary, onProgress) {
327
+ const parser = this.parseDataBlockIncremental(input, onRecordBoundary);
328
+ while (true) {
329
+ const next = parser.next();
330
+ if (next.done === true) {
331
+ return next.value;
332
+ }
333
+ onProgress?.(next.value);
334
+ }
335
+ }
313
336
  /**
314
337
  * Cooperative variant of parseDataBlock: identical parse (same generator
315
338
  * body), but periodically awaits a macrotask so the event loop can run —
@@ -347,7 +370,7 @@ export default class StepParser extends StepHeaderParser {
347
370
  * @yields {number} The current byte cursor within the input buffer.
348
371
  * @return {BlockParseResult} The parsing result, including the index and result enum.
349
372
  */
350
- *parseDataBlockIncremental(input) {
373
+ *parseDataBlockIncremental(input, onRecordBoundary) {
351
374
  const indexResult = { elements: [] };
352
375
  const match = input.match;
353
376
  const comment = () => match(commentParser);
@@ -394,7 +417,10 @@ export default class StepParser extends StepHeaderParser {
394
417
  return syntaxError();
395
418
  }
396
419
  whitespace();
397
- const startElement = input.cursor;
420
+ // File-absolute (== input.cursor when initialOffset is 0, i.e. every
421
+ // resident parse; file-absolute under a streaming moving window so the
422
+ // recorded address/length stay stable as the window slides).
423
+ const startElement = input.address;
398
424
  let stackDepth = 1;
399
425
  const savedInlineElements = inlineElements;
400
426
  inlineElements = void 0;
@@ -492,6 +518,11 @@ export default class StepParser extends StepHeaderParser {
492
518
  if ((++parsedElementCount & PARSE_PROGRESS_ELEMENT_MASK) === 0) {
493
519
  yield input.cursor;
494
520
  }
521
+ // Top-level record boundary: the rewind stack is empty and the cursor
522
+ // sits at the next record's leading whitespace/`#`. A streaming driver
523
+ // uses this to slide its moving window forward (see
524
+ // streaming_index_builder.ts). No-op on the resident path.
525
+ onRecordBoundary?.(input);
495
526
  if (!charws(HASH)) {
496
527
  if (tokenws(END_SECTION)) {
497
528
  return parseResult(ParseResult.COMPLETE);
@@ -516,7 +547,10 @@ export default class StepParser extends StepHeaderParser {
516
547
  }
517
548
  whitespace();
518
549
  inlineElements = void 0;
519
- const startElement = input.cursor;
550
+ // File-absolute (== input.cursor when initialOffset is 0, i.e. every
551
+ // resident parse; file-absolute under a streaming moving window so the
552
+ // recorded address/length stay stable as the window slides).
553
+ const startElement = input.address;
520
554
  // todo, loop and read inline then add them to a special "External Mapping" node.
521
555
  while (!charws(CLOSE_PAREN)) {
522
556
  input.begin();
@@ -547,7 +581,10 @@ export default class StepParser extends StepHeaderParser {
547
581
  return syntaxError();
548
582
  }
549
583
  whitespace();
550
- const startElement = input.cursor;
584
+ // File-absolute (== input.cursor when initialOffset is 0, i.e. every
585
+ // resident parse; file-absolute under a streaming moving window so the
586
+ // recorded address/length stay stable as the window slides).
587
+ const startElement = input.address;
551
588
  let stackDepth = 1;
552
589
  inlineElements = void 0;
553
590
  while (stackDepth > 0) {
@@ -0,0 +1,60 @@
1
+ import { ByteSource } from "./byte_source.js";
2
+ import StepParser, { ParseResult, StepHeader, StepIndexEntry } from "./step_parser.js";
3
+ /**
4
+ * Diagnostics from a streaming index build — how hard the moving window had
5
+ * to work, so a pool-size sweep can compare cost against a resident parse.
6
+ */
7
+ export interface StreamingIndexStats {
8
+ /** Target window size in bytes (the pool). */
9
+ pool: number;
10
+ /** Physical window buffer size actually used (grows past `pool` only if a
11
+ * single record exceeds the pool). */
12
+ windowBytes: number;
13
+ /** Number of times the window slid forward. */
14
+ slides: number;
15
+ /** Largest single top-level record observed, in bytes. */
16
+ maxRecordLen: number;
17
+ /** Total bytes read from the source (≈ file size + re-reads on growth). */
18
+ bytesRead: number;
19
+ }
20
+ /**
21
+ * The output of a streaming index build: the same element index a resident
22
+ * `parseDataBlock` produces, plus the header and window diagnostics.
23
+ */
24
+ export interface StreamingIndexResult<TypeIDType> {
25
+ header: StepHeader;
26
+ elements: StepIndexEntry<TypeIDType>[];
27
+ result: ParseResult;
28
+ stats: StreamingIndexStats;
29
+ }
30
+ /**
31
+ * Build the entity index by streaming a `ByteSource` through a fixed-size
32
+ * moving window, instead of parsing one resident buffer. The parse loop is
33
+ * byte-for-byte the resident one (`StepParser.parseDataBlockStreamed` drives
34
+ * the same generator); this coordinator only owns the window: it fills it,
35
+ * parses the header from the first fill, then slides the window forward at
36
+ * top-level record boundaries as the parser advances.
37
+ *
38
+ * Because the parser records file-absolute addresses and its rewind stack is
39
+ * empty at every top-level boundary, sliding there is transparent: the
40
+ * unconsumed tail is copied to the front, fresh bytes are appended, and the
41
+ * buffer is rebased so `address` keeps its file-absolute value. Peak JS heap
42
+ * for the index build is therefore `window + index columns`, independent of
43
+ * file size.
44
+ *
45
+ * The window slides only once the cursor passes `pool / 2`, bounding the
46
+ * memmove frequency; a record up to `pool / 2` bytes always fits after a
47
+ * slide. If a single record exceeds that (never on the current corpus, whose
48
+ * largest STEP record is ~25 KB), the whole parse restarts from the
49
+ * beginning with the window doubled, and repeats until every record fits —
50
+ * correctness over the pathological case, at the cost of a re-scan. (M1's
51
+ * production path will instead grow in place / restart from the last
52
+ * boundary; from-scratch keeps the spike simple.)
53
+ *
54
+ * @param source The byte source.
55
+ * @param parser The STEP parser (typed to the schema).
56
+ * @param pool Target window size in bytes.
57
+ * @return {StreamingIndexResult} The index, header, result and diagnostics.
58
+ */
59
+ export declare function buildIndexStreaming<TypeIDType>(source: ByteSource, parser: StepParser<TypeIDType>, pool: number): StreamingIndexResult<TypeIDType>;
60
+ //# sourceMappingURL=streaming_index_builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming_index_builder.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/streaming_index_builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,UAAU,EAAE,EACjB,WAAW,EACX,UAAU,EACV,cAAc,EACf,MAAM,eAAe,CAAA;AAGtB;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAElC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAA;IAEZ;0CACsC;IACtC,WAAW,EAAE,MAAM,CAAA;IAEnB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IAEd,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAA;IAEpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CAClB;AAGD;;;GAGG;AACH,MAAM,WAAW,oBAAoB,CAAC,UAAU;IAC9C,MAAM,EAAE,UAAU,CAAA;IAClB,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,CAAA;IACtC,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,mBAAmB,CAAA;CAC3B;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAC1C,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC,EAC9B,IAAI,EAAE,MAAM,GAAI,oBAAoB,CAAC,UAAU,CAAC,CA2GnD"}
@@ -0,0 +1,115 @@
1
+ import ParsingBuffer from "../../parsing/parsing_buffer.js";
2
+ import { ParseResult, } from "./step_parser.js";
3
+ /**
4
+ * Build the entity index by streaming a `ByteSource` through a fixed-size
5
+ * moving window, instead of parsing one resident buffer. The parse loop is
6
+ * byte-for-byte the resident one (`StepParser.parseDataBlockStreamed` drives
7
+ * the same generator); this coordinator only owns the window: it fills it,
8
+ * parses the header from the first fill, then slides the window forward at
9
+ * top-level record boundaries as the parser advances.
10
+ *
11
+ * Because the parser records file-absolute addresses and its rewind stack is
12
+ * empty at every top-level boundary, sliding there is transparent: the
13
+ * unconsumed tail is copied to the front, fresh bytes are appended, and the
14
+ * buffer is rebased so `address` keeps its file-absolute value. Peak JS heap
15
+ * for the index build is therefore `window + index columns`, independent of
16
+ * file size.
17
+ *
18
+ * The window slides only once the cursor passes `pool / 2`, bounding the
19
+ * memmove frequency; a record up to `pool / 2` bytes always fits after a
20
+ * slide. If a single record exceeds that (never on the current corpus, whose
21
+ * largest STEP record is ~25 KB), the whole parse restarts from the
22
+ * beginning with the window doubled, and repeats until every record fits —
23
+ * correctness over the pathological case, at the cost of a re-scan. (M1's
24
+ * production path will instead grow in place / restart from the last
25
+ * boundary; from-scratch keeps the spike simple.)
26
+ *
27
+ * @param source The byte source.
28
+ * @param parser The STEP parser (typed to the schema).
29
+ * @param pool Target window size in bytes.
30
+ * @return {StreamingIndexResult} The index, header, result and diagnostics.
31
+ */
32
+ export function buildIndexStreaming(source, parser, pool) {
33
+ const fileSize = source.byteLength;
34
+ // Grow-and-restart loop: the body runs a full streamed parse at the current
35
+ // window size. It only re-runs if a single record couldn't fit the window
36
+ // (false end-of-file before true EOF), doubling the window each time.
37
+ let windowBytes = Math.max(pool, MIN_WINDOW);
38
+ for (;;) {
39
+ const window = new Uint8Array(windowBytes);
40
+ let windowStartFile = 0;
41
+ let windowLen = source.read(0, windowBytes, window, 0);
42
+ let bytesRead = windowLen;
43
+ const input = new ParsingBuffer(window, 0, windowLen);
44
+ const [header, headerResult] = parser.parseHeader(input);
45
+ if (headerResult !== ParseResult.COMPLETE) {
46
+ // Header didn't complete in the first window: either malformed, or a
47
+ // header larger than the pool. Report it as-is — the resident parse
48
+ // would surface the same header result.
49
+ return {
50
+ header,
51
+ elements: [],
52
+ result: headerResult,
53
+ stats: { pool, windowBytes, slides: 0, maxRecordLen: 0, bytesRead },
54
+ };
55
+ }
56
+ // Slide once the cursor is past half the window; a record up to this much
57
+ // headroom always fits without a false EOF.
58
+ const slideThreshold = windowBytes >> 1;
59
+ let slides = 0;
60
+ let maxRecordLen = 0;
61
+ let prevBoundaryFile = windowStartFile + input.cursor;
62
+ const onRecordBoundary = (buffer) => {
63
+ const cursor = buffer.cursor;
64
+ const recordFileStart = windowStartFile + cursor;
65
+ const recordLen = recordFileStart - prevBoundaryFile;
66
+ if (recordLen > maxRecordLen) {
67
+ maxRecordLen = recordLen;
68
+ }
69
+ prevBoundaryFile = recordFileStart;
70
+ // Window already spans to EOF — nothing left to slide in.
71
+ if (windowStartFile + windowLen >= fileSize) {
72
+ return;
73
+ }
74
+ // Enough headroom remains; defer the slide to bound memmove frequency.
75
+ if (cursor < slideThreshold) {
76
+ return;
77
+ }
78
+ // Slide: move the unconsumed tail to the front, refill behind it.
79
+ const tail = windowLen - cursor;
80
+ window.copyWithin(0, cursor, windowLen);
81
+ const want = windowBytes - tail;
82
+ const got = source.read(windowStartFile + windowLen, want, window, tail);
83
+ bytesRead += got;
84
+ windowLen = tail + got;
85
+ windowStartFile = recordFileStart;
86
+ buffer.rebaseWindow(window, 0, windowLen, windowStartFile);
87
+ ++slides;
88
+ };
89
+ const [index, result] = parser.parseDataBlockStreamed(input, onRecordBoundary);
90
+ // A parse that stopped short of true EOF, with the window not spanning to
91
+ // EOF, means a single record overflowed the window: grow and retry.
92
+ const stoppedShort = result !== ParseResult.COMPLETE;
93
+ const notAtEof = windowStartFile + windowLen < fileSize;
94
+ if (stoppedShort && notAtEof) {
95
+ windowBytes *= 2;
96
+ continue;
97
+ }
98
+ // Capture the final record's length (no boundary fires after the last).
99
+ const lastRecordLen = (windowStartFile + input.cursor) - prevBoundaryFile;
100
+ if (lastRecordLen > maxRecordLen) {
101
+ maxRecordLen = lastRecordLen;
102
+ }
103
+ return {
104
+ header,
105
+ elements: index.elements,
106
+ result,
107
+ stats: { pool, windowBytes, slides, maxRecordLen, bytesRead },
108
+ };
109
+ }
110
+ }
111
+ // Floor on the window so a pathologically tiny pool still holds the header
112
+ // and a record with slide headroom. Tests drive pools down to this to force
113
+ // many slides on a small fixture; the pool sweep uses ≥ 128 KB anyway.
114
+ // eslint-disable-next-line no-magic-numbers
115
+ const MIN_WINDOW = 4 * 1024;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=streaming_index_builder.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming_index_builder.test.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/streaming_index_builder.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // M0 spike: streaming index build must be byte-identical to the resident
3
+ // parse. The parser reads file-absolute addresses and only rewinds within a
4
+ // single top-level record, so feeding it from a moving window that slides at
5
+ // record boundaries must produce the exact same index columns — at every
6
+ // pool size, including one small enough to force many slides + straddles.
7
+ import * as fs from "fs";
8
+ import { beforeAll, describe, expect, test } from "@jest/globals";
9
+ import ParsingBuffer from "../../parsing/parsing_buffer.js";
10
+ import IfcStepParser from "../../ifc/ifc_step_parser.js";
11
+ import { BufferByteSource } from "./byte_source.js";
12
+ import { buildIndexStreaming } from "./streaming_index_builder.js";
13
+ import { ParseResult } from "./step_parser.js";
14
+ /**
15
+ * Flatten the index (top-level records + their inline / multi-mapping
16
+ * children) into a stable, comparable column dump: every field the streaming
17
+ * parse must reproduce identically.
18
+ *
19
+ * @param elements The index elements.
20
+ * @return {object[]} A flat list of column tuples in traversal order.
21
+ */
22
+ function columns(elements) {
23
+ const out = [];
24
+ const visit = (e) => {
25
+ out.push({
26
+ address: e.address,
27
+ length: e.length,
28
+ typeID: e.typeID,
29
+ expressID: e.expressID ?? null,
30
+ });
31
+ for (const child of e.inlineEntities ?? []) {
32
+ visit(child);
33
+ }
34
+ for (const child of e.multiMapping ?? []) {
35
+ visit(child);
36
+ }
37
+ };
38
+ for (const e of elements) {
39
+ visit(e);
40
+ }
41
+ return out;
42
+ }
43
+ let bytes;
44
+ let residentColumns;
45
+ let residentCount;
46
+ beforeAll(() => {
47
+ bytes = new Uint8Array(fs.readFileSync("data/index.ifc"));
48
+ const input = new ParsingBuffer(bytes);
49
+ IfcStepParser.Instance.parseHeader(input);
50
+ const [index, result] = IfcStepParser.Instance.parseDataBlock(input);
51
+ expect(result).toBe(ParseResult.COMPLETE);
52
+ residentColumns = columns(index.elements);
53
+ residentCount = index.elements.length;
54
+ });
55
+ describe("buildIndexStreaming", () => {
56
+ // A tiny pool relative to the 18 KB fixture forces repeated slides and at
57
+ // least one record straddling a window boundary; a large one takes the
58
+ // whole file in a single window (no slide). Both must match the resident
59
+ // parse exactly.
60
+ const POOLS = [4 * 1024, 8 * 1024, 16 * 1024, 128 * 1024];
61
+ test.each(POOLS)("produces the resident index at pool=%d bytes", (pool) => {
62
+ const source = new BufferByteSource(bytes);
63
+ const result = buildIndexStreaming(source, IfcStepParser.Instance, pool);
64
+ expect(result.result).toBe(ParseResult.COMPLETE);
65
+ expect(result.elements.length).toBe(residentCount);
66
+ expect(columns(result.elements)).toEqual(residentColumns);
67
+ });
68
+ test("the small pool actually slides the window (straddle exercised)", () => {
69
+ const source = new BufferByteSource(bytes);
70
+ const result = buildIndexStreaming(source, IfcStepParser.Instance, 4 * 1024);
71
+ // 18 KB fixture through a 4 KB window must slide several times.
72
+ expect(result.stats.slides).toBeGreaterThan(1);
73
+ // Physical window never grew past the pool (no record exceeds it).
74
+ expect(result.stats.windowBytes).toBe(4 * 1024);
75
+ // Every byte read once (no growth re-reads) — bounded to file size.
76
+ expect(result.stats.bytesRead).toBe(bytes.length);
77
+ });
78
+ test("the large pool takes the whole file in one window (no slide)", () => {
79
+ const source = new BufferByteSource(bytes);
80
+ const result = buildIndexStreaming(source, IfcStepParser.Instance, 128 * 1024);
81
+ expect(result.stats.slides).toBe(0);
82
+ expect(result.stats.maxRecordLen).toBeGreaterThan(0);
83
+ });
84
+ test("grows the window and restarts when a single record exceeds the pool", () => {
85
+ // Synthetic file with one record far larger than a tiny pool — the
86
+ // corpus never hits this (largest STEP record ~25 KB), so exercise the
87
+ // grow-and-restart safety valve directly. A long text literal makes one
88
+ // record ~12 KB; a 4 KB pool must grow past it and still index it.
89
+ const bigValue = "X".repeat(12000);
90
+ const text = "ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n" +
91
+ "#1=IFCWALL('a');\n" +
92
+ `#2=IFCPROPERTYSINGLEVALUE('${bigValue}');\n` +
93
+ "#3=IFCWALL('c');\nENDSEC;\n";
94
+ const synthetic = new TextEncoder().encode(text);
95
+ const resident = new ParsingBuffer(synthetic);
96
+ IfcStepParser.Instance.parseHeader(resident);
97
+ const [residentIndex, residentResult] = IfcStepParser.Instance.parseDataBlock(resident);
98
+ expect(residentResult).toBe(ParseResult.COMPLETE);
99
+ const streamed = buildIndexStreaming(new BufferByteSource(synthetic), IfcStepParser.Instance, 4 * 1024);
100
+ expect(streamed.result).toBe(ParseResult.COMPLETE);
101
+ // Window grew past the 4 KB pool to fit the ~12 KB record.
102
+ expect(streamed.stats.windowBytes).toBeGreaterThan(4 * 1024);
103
+ expect(streamed.stats.maxRecordLen).toBeGreaterThan(12000);
104
+ // ...and the index is still byte-identical to the resident parse.
105
+ expect(columns(streamed.elements)).toEqual(columns(residentIndex.elements));
106
+ });
107
+ });
@@ -5,5 +5,5 @@
5
5
  // only the first segment (major) is meaningful and is the one CI carries forward.
6
6
  // Must stay in `vN.N.N` shape: the CI stamp regex, scripts/updateVersion.mjs, and
7
7
  // statistics.ts all match `v\d+\.\d+\.\d+`.
8
- const versionString = 'Conway v1.389.1221';
8
+ const versionString = 'Conway v1.398.1224';
9
9
  export { versionString };