@soulcraft/cor 3.0.17 → 3.0.18

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.
@@ -133,6 +133,21 @@ export declare class MetadataIndexManager implements MetadataIndexProvider {
133
133
  * init() (`columnStore.init` / `warmCache` / `lazyLoadCounts`) on shared state.
134
134
  */
135
135
  private epochMigrationPending;
136
+ /**
137
+ * TRUE when {@link wireIdMapper}'s factory CREATED the mmap mapper fresh
138
+ * this open (both `_id_mapper/*` stores were absent). Benign on a truly
139
+ * fresh brain; POISON when durable int-keyed postings survive alongside
140
+ * it (a lost/deleted `_id_mapper/` directory) — every surviving posting
141
+ * references ints the new mapper never assigned, and minting would pair
142
+ * NEW ints against OLD-int postings silently. Detected right after the
143
+ * LSM cold-open; the response is the coordinated full rebuild.
144
+ */
145
+ private idMapperCreatedFresh;
146
+ /** TRUE when the lost-mapper guard fired this open: the coordinated
147
+ * rebuild must complete BEFORE init returns (blocking), because until
148
+ * vector+graph re-mint, their durable state pairs old ints with a
149
+ * mapper that never assigned them — strictly unservable. */
150
+ private lostMapperHeal;
136
151
  private lastFlushTime;
137
152
  private autoFlushThreshold;
138
153
  private dirtyFields;
@@ -35,6 +35,7 @@ const BUCKETED_INDEX_FIELDS = new Set([
35
35
  import { TypeUtils, NOUN_TYPE_COUNT, VERB_TYPE_COUNT } from '@soulcraft/brainy/types/graphTypes';
36
36
  import { loadNativeModule } from '../native/index.js';
37
37
  import { composeInvariantReport } from './invariantReport.js';
38
+ import { opsLine } from './opsLog.js';
38
39
  import { declareDerivedFamilies, METADATA_FAMILIES } from './derivedFamilies.js';
39
40
  import { openOrCreateBinaryIdMapper } from './binaryIdMapperFactory.js';
40
41
  import { NativeColumnStore } from './NativeColumnStore.js';
@@ -222,6 +223,21 @@ export class MetadataIndexManager {
222
223
  * init() (`columnStore.init` / `warmCache` / `lazyLoadCounts`) on shared state.
223
224
  */
224
225
  epochMigrationPending = false;
226
+ /**
227
+ * TRUE when {@link wireIdMapper}'s factory CREATED the mmap mapper fresh
228
+ * this open (both `_id_mapper/*` stores were absent). Benign on a truly
229
+ * fresh brain; POISON when durable int-keyed postings survive alongside
230
+ * it (a lost/deleted `_id_mapper/` directory) — every surviving posting
231
+ * references ints the new mapper never assigned, and minting would pair
232
+ * NEW ints against OLD-int postings silently. Detected right after the
233
+ * LSM cold-open; the response is the coordinated full rebuild.
234
+ */
235
+ idMapperCreatedFresh = false;
236
+ /** TRUE when the lost-mapper guard fired this open: the coordinated
237
+ * rebuild must complete BEFORE init returns (blocking), because until
238
+ * vector+graph re-mint, their durable state pairs old ints with a
239
+ * mapper that never assigned them — strictly unservable. */
240
+ lostMapperHeal = false;
225
241
  lastFlushTime = Date.now();
226
242
  autoFlushThreshold = 10;
227
243
  dirtyFields = new Set();
@@ -1123,6 +1139,36 @@ export class MetadataIndexManager {
1123
1139
  // with NO O(N) rebuild. Runs after wireIdMapper (the engine's posting
1124
1140
  // lists reference the mapper's entity ints) and before any read/mutation.
1125
1141
  await this.openLsmEngine();
1142
+ // LOST-MAPPER GUARD (MEMORY-COR-IDMAP-MIGRATION, root-caused
1143
+ // 2026-07-14): the ONE genuinely dangerous open-state — the mmap
1144
+ // mapper was just created EMPTY while durable int-keyed postings
1145
+ // survive. (Empirically: 3.0.x-born brains carry their mapper and
1146
+ // upgrade cleanly; this cell arises from a lost/deleted `_id_mapper/`
1147
+ // directory, e.g. deleter-era damage.) Serving would resolve every
1148
+ // surviving posting through ints the mapper never assigned, and the
1149
+ // 3.0.16 mint would pair NEW ints against OLD-int postings silently.
1150
+ // Response: loud, then the coordinated full rebuild (metadata →
1151
+ // vector → graph under the migration lock) so re-minted ints are
1152
+ // globally consistent; strandDetected is the belt when no
1153
+ // coordinator is wired (raw/test usage) so the metadata leg at least
1154
+ // reports not-ready instead of serving wrong pairings.
1155
+ // The trigger is EMPTINESS, not who-created-it: TWO entry points open
1156
+ // `_id_mapper/*` (this manager and the plugin's entityIdMapper
1157
+ // provider), and whichever runs first re-creates the directories — a
1158
+ // created-fresh flag from one path is blind to the other. An EMPTY
1159
+ // mapper alongside durable postings is the poison regardless of how
1160
+ // it got empty (deleted directory, truncated store).
1161
+ const mapperEmpty = this.native.entityIdMapperSize() === 0;
1162
+ if ((this.idMapperCreatedFresh || mapperEmpty) && this.lsmHasDurableState()) {
1163
+ opsLine(`LOST-MAPPER: _id_mapper/ was recreated EMPTY while durable ` +
1164
+ `int-keyed postings survive — the surviving index state is ` +
1165
+ `unresolvable and would serve WRONG pairings. Forcing the ` +
1166
+ `coordinated full rebuild from canonical (metadata + vector + ` +
1167
+ `graph; ints re-mint consistently).`);
1168
+ this.strandDetected = true;
1169
+ this.epochMigrationPending = true;
1170
+ this.lostMapperHeal = true;
1171
+ }
1126
1172
  // **#18 read-side epoch guard.** AFTER the cortex-2.x legacy gate and the
1127
1173
  // LSM cold-open, classify the brain's derived-index epoch against this
1128
1174
  // build's expectation. The common case (current epoch) is a single tiny
@@ -1158,6 +1204,13 @@ export class MetadataIndexManager {
1158
1204
  prodLog.info('[NativeMetadataIndex] Field registry missing but the LSM engine ' +
1159
1205
  'recovered durable state (SSTables/memtable) — serving from it, ' +
1160
1206
  'NO rebuild.');
1207
+ // DETECTOR COVERAGE (memory-vm 2026-07-14): this early serve
1208
+ // path previously SKIPPED detectDerivedStrand entirely — a
1209
+ // brain with lost postings that also lost its field registry
1210
+ // served the gap SILENTLY (posted < canonical, zero log
1211
+ // lines). The strand check is boot-path-invariant: it runs
1212
+ // here too, with the same verified-unpostable allowance.
1213
+ await this.detectDerivedStrand();
1161
1214
  this.readinessInitialized = true;
1162
1215
  return;
1163
1216
  }
@@ -1235,7 +1288,24 @@ export class MetadataIndexManager {
1235
1288
  this.readinessInitialized = true;
1236
1289
  if (this.epochMigrationPending) {
1237
1290
  this.epochMigrationPending = false;
1238
- this.migrationCoordinatorGetter?.()?.start().catch(() => { });
1291
+ const coordinator = this.migrationCoordinatorGetter?.();
1292
+ if (this.lostMapperHeal) {
1293
+ // Lost-mapper heal BLOCKS init: until vector+graph re-mint, their
1294
+ // durable state pairs old ints with a mapper that never assigned
1295
+ // them — brainy must not get an initialized brain before the
1296
+ // coordinated rebuild completes. (The epoch path below stays
1297
+ // fire-and-forget per the rc.9 lock contract — brainy sees the
1298
+ // stale marker and waits on the lock itself.)
1299
+ this.lostMapperHeal = false;
1300
+ if (coordinator) {
1301
+ await coordinator.start();
1302
+ }
1303
+ // No coordinator (raw/test usage): strandDetected already gates
1304
+ // the metadata leg; the loud LOST-MAPPER ops line stands.
1305
+ }
1306
+ else {
1307
+ coordinator?.start().catch(() => { });
1308
+ }
1239
1309
  }
1240
1310
  }
1241
1311
  /**
@@ -1435,9 +1505,9 @@ export class MetadataIndexManager {
1435
1505
  `so the cold-open gate rebuilds from canonical.`);
1436
1506
  }
1437
1507
  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.`);
1508
+ opsLine(`posted ${posted} < canonical ${canonical} is FULLY explained by ` +
1509
+ `${unpostable} verified metadata-less canonical entities (stamped ` +
1510
+ `by the last completed rebuild) — no strand, no heal.`);
1441
1511
  }
1442
1512
  }
1443
1513
  }
@@ -1468,6 +1538,7 @@ export class MetadataIndexManager {
1468
1538
  idSpace: 'u64',
1469
1539
  });
1470
1540
  this.idMapperNative = handle.mapper;
1541
+ this.idMapperCreatedFresh = handle.created;
1471
1542
  this.native.setIdMapper(handle.mapper);
1472
1543
  }
1473
1544
  async loadFieldRegistry() {
@@ -3008,7 +3079,7 @@ export class MetadataIndexManager {
3008
3079
  verbCount: verbResult.noMetadata,
3009
3080
  verifiedAt: Date.now(),
3010
3081
  });
3011
- prodLog.info(`Metadata index rebuild completed! Processed ${nounResult.processed} nouns and ` +
3082
+ opsLine(`Metadata index rebuild completed! Processed ${nounResult.processed} nouns and ` +
3012
3083
  `${verbResult.processed} verbs` +
3013
3084
  (nounResult.noMetadata > 0
3014
3085
  ? ` (${nounResult.noMetadata} canonical noun(s) verified metadata-less — stamped for the strand detector)`
@@ -3081,8 +3152,8 @@ export class MetadataIndexManager {
3081
3152
  if (processed - lastProgressAt >= 500) {
3082
3153
  lastProgressAt = processed;
3083
3154
  const elapsedS = (Date.now() - startedAt) / 1000;
3084
- prodLog.info(`[NativeMetadataIndex] rebuild(${kind}): ${processed} posted ` +
3085
- `in ${elapsedS.toFixed(1)}s (${(processed / Math.max(elapsedS, 0.001)).toFixed(0)}/s)`);
3155
+ opsLine(`rebuild(${kind}): ${processed} posted in ${elapsedS.toFixed(1)}s ` +
3156
+ `(${(processed / Math.max(elapsedS, 0.001)).toFixed(0)}/s)`);
3086
3157
  }
3087
3158
  if (++sinceFlush >= 5000) {
3088
3159
  await this.flushRebuildDirty();
@@ -3099,11 +3170,11 @@ export class MetadataIndexManager {
3099
3170
  await this.yieldToEventLoop();
3100
3171
  }
3101
3172
  const totalS = (Date.now() - startedAt) / 1000;
3102
- prodLog.info(`[NativeMetadataIndex] rebuild(${kind}): COMPLETE — ${processed} posted ` +
3173
+ opsLine(`rebuild(${kind}): COMPLETE — ${processed} posted ` +
3103
3174
  `in ${totalS.toFixed(1)}s (${(processed / Math.max(totalS, 0.001)).toFixed(0)}/s)` +
3104
3175
  (noMetadata > 0 ? `, ${noMetadata} without metadata` : ''));
3105
3176
  if (noMetadata > 0) {
3106
- prodLog.warn(`[NativeMetadataIndex] rebuild(${kind}): ${noMetadata} enumerated ` +
3177
+ opsLine(`rebuild(${kind}): ${noMetadata} enumerated ` +
3107
3178
  `entit${noMetadata === 1 ? 'y' : 'ies'} carried no canonical ` +
3108
3179
  `metadata (verified by direct read) and were not indexed — ` +
3109
3180
  `expected only for metadata-less legacy records. First ids: ` +
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module utils/opsLog
3
+ * @description The always-visible operational channel (CORTEX-RESTART-STRAND,
4
+ * 2026-07-14). brainy's prodLog defaults to ERROR level in production, which
5
+ * silenced ALL heal narration on memory-vm — an operator could not tell a
6
+ * converging heal from a hang until BRAINY_LOG_LEVEL=info was set by hand.
7
+ * Index lifecycle events (heal start / progress / completion, verified
8
+ * anomaly accounting) are OPERATIONAL, not debug: they must reach the
9
+ * journal regardless of logger configuration, exactly like the version
10
+ * banner. One writer, `console.error`, `[cor]`-prefixed — never routed
11
+ * through prodLog (no duplicate lines on boxes that set info-level).
12
+ */
13
+ /** Emit one always-visible operational line. */
14
+ export declare function opsLine(message: string): void;
15
+ //# sourceMappingURL=opsLog.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @module utils/opsLog
3
+ * @description The always-visible operational channel (CORTEX-RESTART-STRAND,
4
+ * 2026-07-14). brainy's prodLog defaults to ERROR level in production, which
5
+ * silenced ALL heal narration on memory-vm — an operator could not tell a
6
+ * converging heal from a hang until BRAINY_LOG_LEVEL=info was set by hand.
7
+ * Index lifecycle events (heal start / progress / completion, verified
8
+ * anomaly accounting) are OPERATIONAL, not debug: they must reach the
9
+ * journal regardless of logger configuration, exactly like the version
10
+ * banner. One writer, `console.error`, `[cor]`-prefixed — never routed
11
+ * through prodLog (no duplicate lines on boxes that set info-level).
12
+ */
13
+ /** Emit one always-visible operational line. */
14
+ export function opsLine(message) {
15
+ console.error(`[cor] ${message}`);
16
+ }
17
+ //# sourceMappingURL=opsLog.js.map
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.17";
14
+ export declare const COR_VERSION = "3.0.18";
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.17';
14
+ export const COR_VERSION = '3.0.18';
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.17",
3
+ "version": "3.0.18",
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",