@soulcraft/cor 3.0.16 → 3.0.17

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.
@@ -563,6 +563,16 @@ export declare class MetadataIndexManager implements MetadataIndexProvider {
563
563
  * same rebuild). Skipped at/above the u32 saturation point of
564
564
  * `getTotalEntityCount` (a saturated count would make equality lie).
565
565
  */
566
+ /**
567
+ * The verified-unpostable allowance for posted-vs-canonical floors: how
568
+ * many canonical noun rows the last COMPLETED rebuild PROVED carry no
569
+ * readable metadata (each verified by direct read at rebuild time, each
570
+ * RE-VERIFIED here so an id that has since gained metadata stops
571
+ * counting). Beyond {@link UNPOSTABLE_ID_CAP} the stamp is count-only
572
+ * and the allowance carries a bounded, logged staleness caveat.
573
+ * Advisory: any fault reads as 0 (may over-heal, never under-detects).
574
+ */
575
+ private verifiedUnpostableAllowance;
566
576
  private detectDerivedStrand;
567
577
  /**
568
578
  * **#72 Phase C.** Build the shared mmap `BinaryIdMapper` from the
@@ -73,6 +73,20 @@ const LSM_MANIFEST_VERSION = 2;
73
73
  * field registry already depends on.
74
74
  */
75
75
  const LSM_MANIFEST_KEY = '__metadata_lsm_manifest__';
76
+ /**
77
+ * Durable stamp of the entities a COMPLETED rebuild verified as
78
+ * unpostable (canonical rows with NO readable metadata — verified by
79
+ * direct read, not invented postings). The strand detector's
80
+ * posted-count floor accounts for them: without this, a fixed cohort of
81
+ * metadata-less canonical rows makes EVERY boot read as stranded and
82
+ * re-triggers a pointless full heal forever (memory-vm's constant-52,
83
+ * CORTEX-RESTART-STRAND 2026-07-13 — four consecutive boots).
84
+ */
85
+ const UNPOSTABLE_STAMP_KEY = '__metadata_verified_unpostable__';
86
+ /** Above this many ids the stamp stores count-only (the boot-time
87
+ * re-verify would stop being cheap); detection then carries a bounded
88
+ * staleness caveat, logged when it engages. */
89
+ const UNPOSTABLE_ID_CAP = 1000;
76
90
  /**
77
91
  * **LSM durability (step 5).** Lower clamp (32 MiB) for the bounded-memtable
78
92
  * size-flush trigger. Even when the dynamic 'metadata' budget is tiny, the
@@ -1275,6 +1289,41 @@ export class MetadataIndexManager {
1275
1289
  * same rebuild). Skipped at/above the u32 saturation point of
1276
1290
  * `getTotalEntityCount` (a saturated count would make equality lie).
1277
1291
  */
1292
+ /**
1293
+ * The verified-unpostable allowance for posted-vs-canonical floors: how
1294
+ * many canonical noun rows the last COMPLETED rebuild PROVED carry no
1295
+ * readable metadata (each verified by direct read at rebuild time, each
1296
+ * RE-VERIFIED here so an id that has since gained metadata stops
1297
+ * counting). Beyond {@link UNPOSTABLE_ID_CAP} the stamp is count-only
1298
+ * and the allowance carries a bounded, logged staleness caveat.
1299
+ * Advisory: any fault reads as 0 (may over-heal, never under-detects).
1300
+ */
1301
+ async verifiedUnpostableAllowance() {
1302
+ try {
1303
+ const stamp = (await this.storage.getMetadata(UNPOSTABLE_STAMP_KEY));
1304
+ if (!stamp || typeof stamp.nounCount !== 'number' || stamp.nounCount <= 0)
1305
+ return 0;
1306
+ if (Array.isArray(stamp.nounIds) && stamp.nounIds.length === stamp.nounCount) {
1307
+ const getMeta = this.storage.getNounMetadata;
1308
+ if (typeof getMeta !== 'function')
1309
+ return stamp.nounCount;
1310
+ let stillUnpostable = 0;
1311
+ for (const id of stamp.nounIds) {
1312
+ const md = await getMeta.call(this.storage, id).catch(() => undefined);
1313
+ if (md == null)
1314
+ stillUnpostable++;
1315
+ }
1316
+ return stillUnpostable;
1317
+ }
1318
+ prodLog.warn(`[NativeMetadataIndex] unpostable stamp is count-only ` +
1319
+ `(${stamp.nounCount} > id cap) — the posted-count floor carries its ` +
1320
+ `staleness until the next completed rebuild refreshes it.`);
1321
+ return stamp.nounCount;
1322
+ }
1323
+ catch {
1324
+ return 0;
1325
+ }
1326
+ }
1278
1327
  async detectDerivedStrand() {
1279
1328
  if (!this.lsmEngine || !this.lsmMetaDir)
1280
1329
  return;
@@ -1364,13 +1413,31 @@ export class MetadataIndexManager {
1364
1413
  // smaller than the (1–2 entity) system skew is masked here and
1365
1414
  // falls to the archive census above; the exact invariant (a
1366
1415
  // persisted posted-count stamp) is CORTEX-BILLION-SCALE-SPINE work.
1367
- if (posted < canonical) {
1416
+ //
1417
+ // UNPOSTABLE ALLOWANCE (the constant-52, CORTEX-RESTART-STRAND):
1418
+ // canonical rows with NO readable metadata cannot carry a
1419
+ // posting, and a completed rebuild PROVES which those are (each
1420
+ // verified by direct read) and stamps them. Without the
1421
+ // allowance the same cohort re-flags every boot and re-triggers
1422
+ // a pointless full heal forever. The stamp's ids are RE-VERIFIED
1423
+ // here (an id that has since gained metadata stops counting), so
1424
+ // the floor stays exact; beyond UNPOSTABLE_ID_CAP the stamp is
1425
+ // count-only and the allowance carries a bounded, logged
1426
+ // staleness caveat.
1427
+ const unpostable = await this.verifiedUnpostableAllowance();
1428
+ if (posted + unpostable < canonical) {
1368
1429
  this.strandDetected = true;
1369
1430
  prodLog.error(`[NativeMetadataIndex] STRAND: canonical holds ${canonical} ` +
1370
- `entities but the index has postings for only ${posted}` +
1371
- `${canonical - posted}+ entities are durable in canonical but ` +
1372
- `UNQUERYABLE (lost postings). Reporting not-ready so the ` +
1373
- `cold-open gate rebuilds from canonical.`);
1431
+ `entities but the index has postings for only ${posted}` +
1432
+ (unpostable > 0 ? ` (+${unpostable} verified-unpostable allowance)` : '') +
1433
+ ` ${canonical - posted - unpostable}+ entities are durable in ` +
1434
+ `canonical but UNQUERYABLE (lost postings). Reporting not-ready ` +
1435
+ `so the cold-open gate rebuilds from canonical.`);
1436
+ }
1437
+ else if (unpostable > 0 && posted < canonical) {
1438
+ prodLog.info(`[NativeMetadataIndex] posted ${posted} < canonical ${canonical} is ` +
1439
+ `FULLY explained by ${unpostable} verified metadata-less canonical ` +
1440
+ `entities (stamped by the last completed rebuild) — no strand, no heal.`);
1374
1441
  }
1375
1442
  }
1376
1443
  }
@@ -2401,15 +2468,23 @@ export class MetadataIndexManager {
2401
2468
  for (const t of Object.values(NounType)) {
2402
2469
  posted += this.native.getEntityCountByType(t);
2403
2470
  }
2471
+ // Same verified-unpostable allowance as the strand detector — a
2472
+ // metadata-less canonical row (proven by the last completed
2473
+ // rebuild) cannot carry a posting and must not read as damage
2474
+ // here either, or brainy's repairIndex loops the same heal the
2475
+ // detector fix just broke.
2476
+ const unpostable = await this.verifiedUnpostableAllowance();
2477
+ const floor = posted + unpostable;
2404
2478
  return {
2405
2479
  name: 'posted-count-floor',
2406
- holds: posted >= canonical,
2407
- detail: posted >= canonical
2408
- ? `posted ${posted} ≥ canonical ${canonical}`
2480
+ holds: floor >= canonical,
2481
+ detail: floor >= canonical
2482
+ ? `posted ${posted}${unpostable > 0 ? ` (+${unpostable} verified-unpostable)` : ''} ≥ canonical ${canonical}`
2409
2483
  : `canonical holds ${canonical} entities but only ${posted} are ` +
2410
- `posted ${canonical - posted} durable-but-unqueryable`,
2484
+ `posted (${unpostable} verified-unpostable allowed) — ` +
2485
+ `${canonical - floor} durable-but-unqueryable`,
2411
2486
  expected: canonical,
2412
- actual: posted,
2487
+ actual: floor,
2413
2488
  heal: 'rebuild',
2414
2489
  };
2415
2490
  },
@@ -2904,11 +2979,11 @@ export class MetadataIndexManager {
2904
2979
  'function';
2905
2980
  if (bulkCapable)
2906
2981
  this.lsmEngine.setBulkLoad(true);
2907
- let totalNounsProcessed;
2908
- let totalVerbsProcessed;
2982
+ let nounResult;
2983
+ let verbResult;
2909
2984
  try {
2910
- totalNounsProcessed = await this.rebuildEntityMetadata('noun');
2911
- totalVerbsProcessed = await this.rebuildEntityMetadata('verb');
2985
+ nounResult = await this.rebuildEntityMetadata('noun');
2986
+ verbResult = await this.rebuildEntityMetadata('verb');
2912
2987
  }
2913
2988
  finally {
2914
2989
  // Leaving bulk mode performs the walk's single durability point
@@ -2922,7 +2997,22 @@ export class MetadataIndexManager {
2922
2997
  this.isRebuilding = false;
2923
2998
  await this.flushRebuildDirty();
2924
2999
  await this.flush();
2925
- prodLog.info(`Metadata index rebuild completed! Processed ${totalNounsProcessed} nouns and ${totalVerbsProcessed} verbs`);
3000
+ // Persist what this COMPLETED walk PROVED unpostable (each id
3001
+ // verified by direct canonical read) so the next boot's strand
3002
+ // detector uses the honest floor instead of re-flagging the same
3003
+ // cohort forever. Refreshed on every completed rebuild; an empty
3004
+ // set clears the stamp.
3005
+ await this.storage.saveMetadata(UNPOSTABLE_STAMP_KEY, {
3006
+ nounCount: nounResult.noMetadata,
3007
+ nounIds: nounResult.noMetadataIds,
3008
+ verbCount: verbResult.noMetadata,
3009
+ verifiedAt: Date.now(),
3010
+ });
3011
+ prodLog.info(`Metadata index rebuild completed! Processed ${nounResult.processed} nouns and ` +
3012
+ `${verbResult.processed} verbs` +
3013
+ (nounResult.noMetadata > 0
3014
+ ? ` (${nounResult.noMetadata} canonical noun(s) verified metadata-less — stamped for the strand detector)`
3015
+ : ''));
2926
3016
  // A completed rebuild re-posted every canonical entity into a freshly
2927
3017
  // re-opened engine (reopenLsmEngineFresh wiped the stale shard set) —
2928
3018
  // any cold-open strand verdict is now satisfied.
@@ -2950,6 +3040,7 @@ export class MetadataIndexManager {
2950
3040
  let processed = 0;
2951
3041
  let sinceFlush = 0;
2952
3042
  let noMetadata = 0;
3043
+ const noMetadataIds = [];
2953
3044
  // Heal observability (CORTEX-RESTART-STRAND, memory's ask): a rebuild
2954
3045
  // must never be silent for minutes — the operator cannot distinguish
2955
3046
  // converging from stuck. Every 500 posted entities logs progress with
@@ -3000,6 +3091,8 @@ export class MetadataIndexManager {
3000
3091
  }
3001
3092
  else {
3002
3093
  noMetadata++;
3094
+ if (noMetadataIds.length < UNPOSTABLE_ID_CAP)
3095
+ noMetadataIds.push(it.id);
3003
3096
  }
3004
3097
  }
3005
3098
  hasMore = page.hasMore;
@@ -3013,9 +3106,10 @@ export class MetadataIndexManager {
3013
3106
  prodLog.warn(`[NativeMetadataIndex] rebuild(${kind}): ${noMetadata} enumerated ` +
3014
3107
  `entit${noMetadata === 1 ? 'y' : 'ies'} carried no canonical ` +
3015
3108
  `metadata (verified by direct read) and were not indexed — ` +
3016
- `expected only for metadata-less legacy records.`);
3109
+ `expected only for metadata-less legacy records. First ids: ` +
3110
+ noMetadataIds.slice(0, 10).join(', '));
3017
3111
  }
3018
- return processed;
3112
+ return { processed, noMetadata, noMetadataIds };
3019
3113
  }
3020
3114
  /**
3021
3115
  * Rebuild-path verify-read for a single entity's canonical metadata.
package/dist/version.d.ts CHANGED
@@ -11,5 +11,5 @@
11
11
  * `check:brainy`-style drift between the two fails the release.
12
12
  */
13
13
  /** The @soulcraft/cor version this build was cut from. */
14
- export declare const COR_VERSION = "3.0.16";
14
+ export declare const COR_VERSION = "3.0.17";
15
15
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -11,5 +11,5 @@
11
11
  * `check:brainy`-style drift between the two fails the release.
12
12
  */
13
13
  /** The @soulcraft/cor version this build was cut from. */
14
- export const COR_VERSION = '3.0.16';
14
+ export const COR_VERSION = '3.0.17';
15
15
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cor",
3
- "version": "3.0.16",
3
+ "version": "3.0.17",
4
4
  "description": "Native Rust acceleration for Brainy — SIMD distance, vector quantization, zero-copy mmap, native embeddings. Free tier for storage, Pro license for compute acceleration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",