@bldrs-ai/conway 1.476.1409 → 1.484.1410

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.
@@ -30,7 +30,7 @@ var import_node_process = require("node:process");
30
30
  var readline = __toESM(require("node:readline"), 1);
31
31
 
32
32
  // compiled/src/version/version.js
33
- var versionString = "Conway v1.476.1409";
33
+ var versionString = "Conway v1.484.1410";
34
34
 
35
35
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
36
36
  var wasmType = "";
@@ -14965,7 +14965,7 @@ ${t5.join("\n")}` : "";
14965
14965
  var import_process = require("process");
14966
14966
 
14967
14967
  // compiled/src/version/version.js
14968
- var versionString = "Conway v1.476.1409";
14968
+ var versionString = "Conway v1.484.1410";
14969
14969
 
14970
14970
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
14971
14971
  function pThreadsAllowed() {
@@ -15943,7 +15943,7 @@ var ParsingBuffer = class {
15943
15943
  };
15944
15944
 
15945
15945
  // compiled/src/version/version.js
15946
- var versionString = "Conway v1.476.1409";
15946
+ var versionString = "Conway v1.484.1410";
15947
15947
 
15948
15948
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
15949
15949
  function pThreadsAllowed() {
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
944
944
  var entity_types_ifc_gen_default = EntityTypesIfc;
945
945
 
946
946
  // compiled/src/version/version.js
947
- var versionString = "Conway v1.476.1409";
947
+ var versionString = "Conway v1.484.1410";
948
948
 
949
949
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
950
950
  var wasmType = "";
@@ -88,6 +88,39 @@ function csvSafeString(from) {
88
88
  }
89
89
  return from;
90
90
  }
91
+ /**
92
+ * Read back the first field of a line written with csvSafeString.
93
+ *
94
+ * Naive `line.split(',')[0]` is wrong for exactly the inputs csvSafeString
95
+ * exists for: a model filename containing a comma comes back quoted, and
96
+ * splitting on ',' truncates it mid-name. The zero-geometry collision check
97
+ * keys on this field, so a truncated name silently changes which stems it
98
+ * thinks collide.
99
+ *
100
+ * @param line A CSV line whose first field was written by csvSafeString.
101
+ * @return {string} The unescaped first field.
102
+ */
103
+ function csvFirstField(line) {
104
+ const trimmed = line.replace(/[\r\n]+$/, '');
105
+ if (!trimmed.startsWith('"')) {
106
+ return trimmed.split(',')[0];
107
+ }
108
+ let result = '';
109
+ for (let cursor = 1; cursor < trimmed.length; ++cursor) {
110
+ if (trimmed[cursor] !== '"') {
111
+ result += trimmed[cursor];
112
+ continue;
113
+ }
114
+ // A doubled quote is a literal one; a lone quote closes the field.
115
+ if (trimmed[cursor + 1] === '"') {
116
+ result += '"';
117
+ ++cursor;
118
+ continue;
119
+ }
120
+ break;
121
+ }
122
+ return result;
123
+ }
91
124
  /**
92
125
  * Encapsulates a string in a CSV safe way, taking
93
126
  * file paths (assumed by directory characters / and \,
@@ -206,6 +239,15 @@ async function runForFile(filePath, outputPath, maxTimeout, perfPath) {
206
239
  './compiled/src/ifc/ifc_regression_main.js';
207
240
  const safeExecCommand = `node --experimental-specifier-resolution=node ${childScript} -d${perfFlag} "${filePath}" "${outputPath}"`;
208
241
  console.log(`Current File: ${filePath}`);
242
+ // Remove any stale digest before the child runs. Without this, a child that
243
+ // dies without writing one leaves the committed baseline CSV in place, the
244
+ // batch hashes THAT, and the model reports an unchanged hash with no failure
245
+ // and no zero-geometry row - green, while extraction is completely broken.
246
+ // That is precisely the case this gate exists to catch.
247
+ const staleDigest = `${outputPath}.csv`;
248
+ if (fs.existsSync(staleDigest)) {
249
+ await fsPromises.rm(staleDigest, { force: true });
250
+ }
209
251
  // Use safeExecWithCancellation, will kill the process if it takes longer than MAX_TIMEOUT_MS.
210
252
  const process = await safeExecWithCancellation(safeExecCommand, MAX_TIMEOUT_MS);
211
253
  totalTime += Date.now() - startTime;
@@ -228,54 +270,41 @@ async function runForFile(filePath, outputPath, maxTimeout, perfPath) {
228
270
  }
229
271
  const outputFile = path.basename(outputPath);
230
272
  let fileHash;
273
+ // Absent digest counts as zero geometry: the stale file was removed before
274
+ // the child ran, so nothing here can be left over from the committed
275
+ // baseline. A child that dies without writing one is exactly the case this
276
+ // gate exists to catch, and hashing the old file would report it as green.
277
+ let producedNoGeometry = true;
231
278
  const outputCSV = `${outputPath}.csv`;
232
279
  if (fs.existsSync(outputCSV)) {
280
+ const digest = await fsPromises.readFile(outputCSV);
233
281
  fileHash = crypto
234
282
  .createHash('sha1')
235
- .update(await fsPromises.readFile(outputCSV))
283
+ .update(digest)
236
284
  .digest('hex');
285
+ // A digest with nothing after its header means the model loaded and
286
+ // produced no geometry at all. The child still exits 0, so this is
287
+ // invisible to every other signal the harness has — see #477 / #478.
288
+ //
289
+ // Known limitation: a digest row is any hashed entity, and that includes
290
+ // non-renderable ones (IFCSURFACESTYLE, IFCPOLYLINE, IFCCENTERLINEPROFILEDEF
291
+ // …), so "has rows" is weaker than "has renderable geometry". A model that
292
+ // emits only styles and profiles reads as fine here. That is a deliberate
293
+ // floor, not an oversight: this catches the total-blank case with zero
294
+ // false positives, which is what an unconditional CI gate needs. Tightening
295
+ // it to a mesh/vertex count means teaching the digest which types are
296
+ // renderable and re-blessing every baseline; tracked on #478.
297
+ producedNoGeometry =
298
+ digest.toString('utf8').split('\n').filter((line) => line.trim().length > 0).length <= 1;
237
299
  }
238
300
  return {
239
301
  type: 'Run',
240
302
  errorLines: errorLines.length > 0 ? errorLines : undefined,
241
303
  outputFile,
242
304
  hash: fileHash,
305
+ producedNoGeometry,
243
306
  };
244
307
  }
245
- /**
246
- * Run a file's digest, retrying ONCE if the first attempt times out.
247
- *
248
- * A per-model timeout is not a deterministic parse/geometry failure — it
249
- * surfaces as a failed.csv row with an empty code AND empty signal (the
250
- * TimeoutError path sets neither) and, historically, has come from transient
251
- * core oversubscription on the CI runner rather than the model itself.
252
- * ISSUE_159_kleine_Wohnung_R22.ifc (a geometry-dense ~18.5k-item model) has
253
- * reddened the rc-regression gate this way twice: first co-scheduled at
254
- * --concurrency 2, then again running ALONE at --concurrency 1, so serializing
255
- * the batch was not enough to make it reliable.
256
- *
257
- * Digest regeneration is idempotent, so a second attempt is safe: a transient
258
- * timeout clears on the retry, while a genuine hang times out both times and
259
- * still lands in failed.csv — the gate keeps its protective value against a
260
- * model that truly never loads. Only timeouts are retried; a real non-zero
261
- * exit or signal is returned immediately (those ARE deterministic).
262
- *
263
- * @param filePath Model file to digest.
264
- * @param outputPath Digest output path (without extension).
265
- * @param maxTimeout Per-attempt timeout in ms.
266
- * @param perfPath Optional path the child writes its one-row perf CSV to.
267
- * @return The first attempt's result, or the retry's result on a timeout.
268
- */
269
- async function runForFileWithTimeoutRetry(filePath, outputPath, maxTimeout, perfPath) {
270
- const timedOut = (r) => r.type === 'Failed' && r.message === 'Execution timed out';
271
- const first = await runForFile(filePath, outputPath, maxTimeout, perfPath);
272
- if (!timedOut(first)) {
273
- return first;
274
- }
275
- console.log(`"${path.basename(filePath)}" timed out; retrying once ` +
276
- `(a transient timeout clears, a true hang times out again).`);
277
- return runForFile(filePath, outputPath, maxTimeout, perfPath);
278
- }
279
308
  // Model files the regression harness understands: IFC plus STEP AP214.
280
309
  const SUPPORTED_MODEL_EXTENSIONS = ['.ifc', '.stp', '.step'];
281
310
  /**
@@ -355,12 +384,13 @@ function getSystemMemoryUsagePercent() {
355
384
  * @param errorLines
356
385
  * @param fileLines
357
386
  * @param failedLines
387
+ * @param zeroGeometryLines
358
388
  * @param memUtilization
359
389
  * @param maxTimeout
360
390
  * @param concurrency Max children processed at once (>= 1).
361
391
  * @param perfDir If set, the child writes its perf CSV here as <basename>.perf.csv.
362
392
  */
363
- async function processIFCFilesInParallel(ifcFiles, outputPath, errorLines, fileLines, failedLines, memUtilization, maxTimeout, concurrency, perfDir) {
393
+ async function processIFCFilesInParallel(ifcFiles, outputPath, errorLines, fileLines, failedLines, zeroGeometryLines, memUtilization, maxTimeout, concurrency, perfDir) {
364
394
  const concurrencyLimit = Math.max(1, concurrency);
365
395
  console.log(`Concurrency: ${concurrencyLimit} children - Max Timeout: ${maxTimeout} ms`);
366
396
  const limit = pLimit(concurrencyLimit);
@@ -378,7 +408,7 @@ async function processIFCFilesInParallel(ifcFiles, outputPath, errorLines, fileL
378
408
  const perfChildPath = perfDir ?
379
409
  path.join(perfDir, `${path.parse(ifcPath).name}.perf.csv`) :
380
410
  undefined;
381
- const fileResults = await runForFileWithTimeoutRetry(ifcPath, path.join(outputPath, path.parse(ifcPath).name), maxTimeout, perfChildPath);
411
+ const fileResults = await runForFile(ifcPath, path.join(outputPath, path.parse(ifcPath).name), maxTimeout, perfChildPath);
382
412
  activeTasks--;
383
413
  console.log(`Completed task for "${path.basename(ifcPath)}". Active tasks: ${activeTasks}`);
384
414
  return { ifcPath, fileResults };
@@ -394,6 +424,9 @@ async function processIFCFilesInParallel(ifcFiles, outputPath, errorLines, fileL
394
424
  fileLines.push(`${csvSafeString(path.basename(ifcPath))},` +
395
425
  `${csvSafeString(fileResults.hash ?? '')},` +
396
426
  `${fileResults.errorLines?.length ?? 0}\n`);
427
+ if (fileResults.producedNoGeometry) {
428
+ zeroGeometryLines.push(`${csvSafeString(path.basename(ifcPath))}\n`);
429
+ }
397
430
  }
398
431
  else {
399
432
  // it's 'Failed'
@@ -412,10 +445,11 @@ async function processIFCFilesInParallel(ifcFiles, outputPath, errorLines, fileL
412
445
  * @param errorLines
413
446
  * @param fileLines
414
447
  * @param failedLines
448
+ * @param zeroGeometryLines
415
449
  * @param maxTimeout
416
450
  * @param perfDir If set, the child writes its perf CSV here as <basename>.perf.csv.
417
451
  */
418
- async function recursiveWalk(parentPath, excludeRegex, outputPath, errorLines, fileLines, failedLines, maxTimeout, perfDir) {
452
+ async function recursiveWalk(parentPath, excludeRegex, outputPath, errorLines, fileLines, failedLines, zeroGeometryLines, maxTimeout, perfDir) {
419
453
  const items = await fsPromises.readdir(parentPath, { withFileTypes: true });
420
454
  items.sort((a, b) => (a.name > b.name ? 1 : -1));
421
455
  for (const item of items) {
@@ -424,18 +458,21 @@ async function recursiveWalk(parentPath, excludeRegex, outputPath, errorLines, f
424
458
  continue;
425
459
  }
426
460
  if (item.isDirectory()) {
427
- await recursiveWalk(resolved, excludeRegex, outputPath, errorLines, fileLines, failedLines, maxTimeout, perfDir);
461
+ await recursiveWalk(resolved, excludeRegex, outputPath, errorLines, fileLines, failedLines, zeroGeometryLines, maxTimeout, perfDir);
428
462
  }
429
463
  else if (isSupportedModelFile(resolved)) {
430
464
  const perfChildPath = perfDir ?
431
465
  path.join(perfDir, `${path.parse(resolved).name}.perf.csv`) :
432
466
  undefined;
433
- const fileResults = await runForFileWithTimeoutRetry(resolved, path.join(outputPath, path.parse(resolved).name), maxTimeout, perfChildPath);
467
+ const fileResults = await runForFile(resolved, path.join(outputPath, path.parse(resolved).name), maxTimeout, perfChildPath);
434
468
  if (fileResults.type === 'Run') {
435
469
  if (fileResults.errorLines !== void 0) {
436
470
  errorLines.push(...fileResults.errorLines);
437
471
  }
438
472
  fileLines.push(`${csvSafeString(path.basename(resolved))},${csvSafeString(fileResults.hash ?? '')},${fileResults.errorLines?.length ?? 0}\n`);
473
+ if (fileResults.producedNoGeometry) {
474
+ zeroGeometryLines.push(`${csvSafeString(path.basename(resolved))}\n`);
475
+ }
439
476
  }
440
477
  else {
441
478
  failedLines.push(`${csvSafeString(path.basename(resolved))},${csvSafeString(fileResults.code?.toString() ?? '')},${csvSafeString(fileResults.signal ?? '')}\n`);
@@ -557,25 +594,60 @@ const args = yargs(process.argv.slice(SKIP_PARAMS))
557
594
  const mainPath = path.join(outputPath, 'main.csv');
558
595
  const errorPath = path.join(outputPath, 'errors.csv');
559
596
  const failedPath = path.join(outputPath, 'failed.csv');
597
+ const zeroGeometryPath = path.join(outputPath, 'zero_geometry.csv');
560
598
  const errorLines = [];
561
599
  const fileLines = [];
562
600
  const failedLines = [];
601
+ const zeroGeometryLines = [];
563
602
  const excludeRegex = excludeFilter.length > 0 ? new RegExp(excludeFilter) : undefined;
564
603
  if (doParallel) {
565
604
  console.log('Processing in parallel mode...');
566
605
  // 1) Collect all IFC files first
567
606
  const allIFCFiles = await collectIFCFiles(ifcFolder, excludeRegex);
568
607
  // 2) Process them in parallel
569
- await processIFCFilesInParallel(allIFCFiles, outputPath, errorLines, fileLines, failedLines, memUtilization, maxTimeout, concurrency, perfTmpDir);
608
+ await processIFCFilesInParallel(allIFCFiles, outputPath, errorLines, fileLines, failedLines, zeroGeometryLines, memUtilization, maxTimeout, concurrency, perfTmpDir);
570
609
  }
571
610
  else {
572
611
  console.log('Processing in serial mode...');
573
- await recursiveWalk(ifcFolder, excludeRegex, outputPath, errorLines, fileLines, failedLines, maxTimeout, perfTmpDir);
612
+ await recursiveWalk(ifcFolder, excludeRegex, outputPath, errorLines, fileLines, failedLines, zeroGeometryLines, maxTimeout, perfTmpDir);
574
613
  }
575
614
  // Write out results
576
615
  await fsPromises.writeFile(mainPath, `file,hash,errors\n${fileLines.join('')}`);
577
616
  await fsPromises.writeFile(errorPath, `${errorCSVHeader}\n${errorLines.join('')}`);
578
617
  await fsPromises.writeFile(failedPath, `file,code,signal\n${failedLines.join('')}`);
618
+ // Models that loaded and produced no geometry. Written as its own
619
+ // artifact rather than folded into failed.csv, because the two need
620
+ // different gates: failed.csv is empty and stays empty, while a
621
+ // handful of zero-geometry models are known and tracked (#477), so
622
+ // CI compares this against an explicit allowlist instead.
623
+ // Models whose digest stem collides cannot be judged: two models
624
+ // sharing a basename write the SAME digest file, so in --parallel one
625
+ // can be read between another's truncate and its first row. That
626
+ // collision is a pre-existing harness bug (it also means the blessed
627
+ // digest is whichever model ran last) - `ifc/index.ifc` and
628
+ // `ifc/bldrs/index.ifc` are a live example, both in the smoke subset.
629
+ // Suppress rather than guess, so it cannot red an unrelated PR, and
630
+ // say so loudly.
631
+ const stemCounts = new Map();
632
+ for (const line of fileLines) {
633
+ const stem = path.parse(csvFirstField(line)).name;
634
+ stemCounts.set(stem, (stemCounts.get(stem) ?? 0) + 1);
635
+ }
636
+ const collidingStems = new Set([...stemCounts].filter(([, count]) => count > 1).map(([stem]) => stem));
637
+ if (collidingStems.size > 0) {
638
+ console.warn(`WARNING: ${collidingStems.size} digest stem(s) are written by more ` +
639
+ `than one model, so their digests overwrite each other and their ` +
640
+ `zero-geometry status cannot be determined: ${[...collidingStems].join(', ')}`);
641
+ }
642
+ const reportableZeroGeometry = zeroGeometryLines.filter((line) => !collidingStems.has(path.parse(csvFirstField(line)).name));
643
+ await fsPromises.writeFile(zeroGeometryPath, `file\n${reportableZeroGeometry.join('')}`);
644
+ if (reportableZeroGeometry.length > 0) {
645
+ console.log(`\n${reportableZeroGeometry.length} model(s) loaded but produced NO geometry:`);
646
+ for (const line of reportableZeroGeometry) {
647
+ console.log(` ${line.trim()}`);
648
+ }
649
+ console.log('');
650
+ }
579
651
  // Aggregate per-child perf rows (if requested) before runDiff so the
580
652
  // run completes deterministically even when the git diff step is
581
653
  // skipped or fails.
@@ -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.476.1409';
8
+ const versionString = 'Conway v1.484.1410';
9
9
  export { versionString };