@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?.()
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
3085
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
14
|
+
export declare const COR_VERSION = "3.0.18";
|
|
15
15
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/cor",
|
|
3
|
-
"version": "3.0.
|
|
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",
|