@soulcraft/cor 3.0.16 → 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.
- package/dist/utils/NativeMetadataIndex.d.ts +25 -0
- package/dist/utils/NativeMetadataIndex.js +187 -22
- package/dist/utils/opsLog.d.ts +15 -0
- package/dist/utils/opsLog.js +17 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -563,6 +578,16 @@ export declare class MetadataIndexManager implements MetadataIndexProvider {
|
|
|
563
578
|
* same rebuild). Skipped at/above the u32 saturation point of
|
|
564
579
|
* `getTotalEntityCount` (a saturated count would make equality lie).
|
|
565
580
|
*/
|
|
581
|
+
/**
|
|
582
|
+
* The verified-unpostable allowance for posted-vs-canonical floors: how
|
|
583
|
+
* many canonical noun rows the last COMPLETED rebuild PROVED carry no
|
|
584
|
+
* readable metadata (each verified by direct read at rebuild time, each
|
|
585
|
+
* RE-VERIFIED here so an id that has since gained metadata stops
|
|
586
|
+
* counting). Beyond {@link UNPOSTABLE_ID_CAP} the stamp is count-only
|
|
587
|
+
* and the allowance carries a bounded, logged staleness caveat.
|
|
588
|
+
* Advisory: any fault reads as 0 (may over-heal, never under-detects).
|
|
589
|
+
*/
|
|
590
|
+
private verifiedUnpostableAllowance;
|
|
566
591
|
private detectDerivedStrand;
|
|
567
592
|
/**
|
|
568
593
|
* **#72 Phase C.** Build the shared mmap `BinaryIdMapper` from the
|
|
@@ -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';
|
|
@@ -73,6 +74,20 @@ const LSM_MANIFEST_VERSION = 2;
|
|
|
73
74
|
* field registry already depends on.
|
|
74
75
|
*/
|
|
75
76
|
const LSM_MANIFEST_KEY = '__metadata_lsm_manifest__';
|
|
77
|
+
/**
|
|
78
|
+
* Durable stamp of the entities a COMPLETED rebuild verified as
|
|
79
|
+
* unpostable (canonical rows with NO readable metadata — verified by
|
|
80
|
+
* direct read, not invented postings). The strand detector's
|
|
81
|
+
* posted-count floor accounts for them: without this, a fixed cohort of
|
|
82
|
+
* metadata-less canonical rows makes EVERY boot read as stranded and
|
|
83
|
+
* re-triggers a pointless full heal forever (memory-vm's constant-52,
|
|
84
|
+
* CORTEX-RESTART-STRAND 2026-07-13 — four consecutive boots).
|
|
85
|
+
*/
|
|
86
|
+
const UNPOSTABLE_STAMP_KEY = '__metadata_verified_unpostable__';
|
|
87
|
+
/** Above this many ids the stamp stores count-only (the boot-time
|
|
88
|
+
* re-verify would stop being cheap); detection then carries a bounded
|
|
89
|
+
* staleness caveat, logged when it engages. */
|
|
90
|
+
const UNPOSTABLE_ID_CAP = 1000;
|
|
76
91
|
/**
|
|
77
92
|
* **LSM durability (step 5).** Lower clamp (32 MiB) for the bounded-memtable
|
|
78
93
|
* size-flush trigger. Even when the dynamic 'metadata' budget is tiny, the
|
|
@@ -208,6 +223,21 @@ export class MetadataIndexManager {
|
|
|
208
223
|
* init() (`columnStore.init` / `warmCache` / `lazyLoadCounts`) on shared state.
|
|
209
224
|
*/
|
|
210
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;
|
|
211
241
|
lastFlushTime = Date.now();
|
|
212
242
|
autoFlushThreshold = 10;
|
|
213
243
|
dirtyFields = new Set();
|
|
@@ -1109,6 +1139,36 @@ export class MetadataIndexManager {
|
|
|
1109
1139
|
// with NO O(N) rebuild. Runs after wireIdMapper (the engine's posting
|
|
1110
1140
|
// lists reference the mapper's entity ints) and before any read/mutation.
|
|
1111
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
|
+
}
|
|
1112
1172
|
// **#18 read-side epoch guard.** AFTER the cortex-2.x legacy gate and the
|
|
1113
1173
|
// LSM cold-open, classify the brain's derived-index epoch against this
|
|
1114
1174
|
// build's expectation. The common case (current epoch) is a single tiny
|
|
@@ -1144,6 +1204,13 @@ export class MetadataIndexManager {
|
|
|
1144
1204
|
prodLog.info('[NativeMetadataIndex] Field registry missing but the LSM engine ' +
|
|
1145
1205
|
'recovered durable state (SSTables/memtable) — serving from it, ' +
|
|
1146
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();
|
|
1147
1214
|
this.readinessInitialized = true;
|
|
1148
1215
|
return;
|
|
1149
1216
|
}
|
|
@@ -1221,7 +1288,24 @@ export class MetadataIndexManager {
|
|
|
1221
1288
|
this.readinessInitialized = true;
|
|
1222
1289
|
if (this.epochMigrationPending) {
|
|
1223
1290
|
this.epochMigrationPending = false;
|
|
1224
|
-
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
|
+
}
|
|
1225
1309
|
}
|
|
1226
1310
|
}
|
|
1227
1311
|
/**
|
|
@@ -1275,6 +1359,41 @@ export class MetadataIndexManager {
|
|
|
1275
1359
|
* same rebuild). Skipped at/above the u32 saturation point of
|
|
1276
1360
|
* `getTotalEntityCount` (a saturated count would make equality lie).
|
|
1277
1361
|
*/
|
|
1362
|
+
/**
|
|
1363
|
+
* The verified-unpostable allowance for posted-vs-canonical floors: how
|
|
1364
|
+
* many canonical noun rows the last COMPLETED rebuild PROVED carry no
|
|
1365
|
+
* readable metadata (each verified by direct read at rebuild time, each
|
|
1366
|
+
* RE-VERIFIED here so an id that has since gained metadata stops
|
|
1367
|
+
* counting). Beyond {@link UNPOSTABLE_ID_CAP} the stamp is count-only
|
|
1368
|
+
* and the allowance carries a bounded, logged staleness caveat.
|
|
1369
|
+
* Advisory: any fault reads as 0 (may over-heal, never under-detects).
|
|
1370
|
+
*/
|
|
1371
|
+
async verifiedUnpostableAllowance() {
|
|
1372
|
+
try {
|
|
1373
|
+
const stamp = (await this.storage.getMetadata(UNPOSTABLE_STAMP_KEY));
|
|
1374
|
+
if (!stamp || typeof stamp.nounCount !== 'number' || stamp.nounCount <= 0)
|
|
1375
|
+
return 0;
|
|
1376
|
+
if (Array.isArray(stamp.nounIds) && stamp.nounIds.length === stamp.nounCount) {
|
|
1377
|
+
const getMeta = this.storage.getNounMetadata;
|
|
1378
|
+
if (typeof getMeta !== 'function')
|
|
1379
|
+
return stamp.nounCount;
|
|
1380
|
+
let stillUnpostable = 0;
|
|
1381
|
+
for (const id of stamp.nounIds) {
|
|
1382
|
+
const md = await getMeta.call(this.storage, id).catch(() => undefined);
|
|
1383
|
+
if (md == null)
|
|
1384
|
+
stillUnpostable++;
|
|
1385
|
+
}
|
|
1386
|
+
return stillUnpostable;
|
|
1387
|
+
}
|
|
1388
|
+
prodLog.warn(`[NativeMetadataIndex] unpostable stamp is count-only ` +
|
|
1389
|
+
`(${stamp.nounCount} > id cap) — the posted-count floor carries its ` +
|
|
1390
|
+
`staleness until the next completed rebuild refreshes it.`);
|
|
1391
|
+
return stamp.nounCount;
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
return 0;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1278
1397
|
async detectDerivedStrand() {
|
|
1279
1398
|
if (!this.lsmEngine || !this.lsmMetaDir)
|
|
1280
1399
|
return;
|
|
@@ -1364,13 +1483,31 @@ export class MetadataIndexManager {
|
|
|
1364
1483
|
// smaller than the (1–2 entity) system skew is masked here and
|
|
1365
1484
|
// falls to the archive census above; the exact invariant (a
|
|
1366
1485
|
// persisted posted-count stamp) is CORTEX-BILLION-SCALE-SPINE work.
|
|
1367
|
-
|
|
1486
|
+
//
|
|
1487
|
+
// UNPOSTABLE ALLOWANCE (the constant-52, CORTEX-RESTART-STRAND):
|
|
1488
|
+
// canonical rows with NO readable metadata cannot carry a
|
|
1489
|
+
// posting, and a completed rebuild PROVES which those are (each
|
|
1490
|
+
// verified by direct read) and stamps them. Without the
|
|
1491
|
+
// allowance the same cohort re-flags every boot and re-triggers
|
|
1492
|
+
// a pointless full heal forever. The stamp's ids are RE-VERIFIED
|
|
1493
|
+
// here (an id that has since gained metadata stops counting), so
|
|
1494
|
+
// the floor stays exact; beyond UNPOSTABLE_ID_CAP the stamp is
|
|
1495
|
+
// count-only and the allowance carries a bounded, logged
|
|
1496
|
+
// staleness caveat.
|
|
1497
|
+
const unpostable = await this.verifiedUnpostableAllowance();
|
|
1498
|
+
if (posted + unpostable < canonical) {
|
|
1368
1499
|
this.strandDetected = true;
|
|
1369
1500
|
prodLog.error(`[NativeMetadataIndex] STRAND: canonical holds ${canonical} ` +
|
|
1370
|
-
`entities but the index has postings for only ${posted}
|
|
1371
|
-
|
|
1372
|
-
`
|
|
1373
|
-
`
|
|
1501
|
+
`entities but the index has postings for only ${posted}` +
|
|
1502
|
+
(unpostable > 0 ? ` (+${unpostable} verified-unpostable allowance)` : '') +
|
|
1503
|
+
` — ${canonical - posted - unpostable}+ entities are durable in ` +
|
|
1504
|
+
`canonical but UNQUERYABLE (lost postings). Reporting not-ready ` +
|
|
1505
|
+
`so the cold-open gate rebuilds from canonical.`);
|
|
1506
|
+
}
|
|
1507
|
+
else if (unpostable > 0 && posted < canonical) {
|
|
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.`);
|
|
1374
1511
|
}
|
|
1375
1512
|
}
|
|
1376
1513
|
}
|
|
@@ -1401,6 +1538,7 @@ export class MetadataIndexManager {
|
|
|
1401
1538
|
idSpace: 'u64',
|
|
1402
1539
|
});
|
|
1403
1540
|
this.idMapperNative = handle.mapper;
|
|
1541
|
+
this.idMapperCreatedFresh = handle.created;
|
|
1404
1542
|
this.native.setIdMapper(handle.mapper);
|
|
1405
1543
|
}
|
|
1406
1544
|
async loadFieldRegistry() {
|
|
@@ -2401,15 +2539,23 @@ export class MetadataIndexManager {
|
|
|
2401
2539
|
for (const t of Object.values(NounType)) {
|
|
2402
2540
|
posted += this.native.getEntityCountByType(t);
|
|
2403
2541
|
}
|
|
2542
|
+
// Same verified-unpostable allowance as the strand detector — a
|
|
2543
|
+
// metadata-less canonical row (proven by the last completed
|
|
2544
|
+
// rebuild) cannot carry a posting and must not read as damage
|
|
2545
|
+
// here either, or brainy's repairIndex loops the same heal the
|
|
2546
|
+
// detector fix just broke.
|
|
2547
|
+
const unpostable = await this.verifiedUnpostableAllowance();
|
|
2548
|
+
const floor = posted + unpostable;
|
|
2404
2549
|
return {
|
|
2405
2550
|
name: 'posted-count-floor',
|
|
2406
|
-
holds:
|
|
2407
|
-
detail:
|
|
2408
|
-
? `posted ${posted} ≥ canonical ${canonical}`
|
|
2551
|
+
holds: floor >= canonical,
|
|
2552
|
+
detail: floor >= canonical
|
|
2553
|
+
? `posted ${posted}${unpostable > 0 ? ` (+${unpostable} verified-unpostable)` : ''} ≥ canonical ${canonical}`
|
|
2409
2554
|
: `canonical holds ${canonical} entities but only ${posted} are ` +
|
|
2410
|
-
`posted
|
|
2555
|
+
`posted (${unpostable} verified-unpostable allowed) — ` +
|
|
2556
|
+
`${canonical - floor} durable-but-unqueryable`,
|
|
2411
2557
|
expected: canonical,
|
|
2412
|
-
actual:
|
|
2558
|
+
actual: floor,
|
|
2413
2559
|
heal: 'rebuild',
|
|
2414
2560
|
};
|
|
2415
2561
|
},
|
|
@@ -2904,11 +3050,11 @@ export class MetadataIndexManager {
|
|
|
2904
3050
|
'function';
|
|
2905
3051
|
if (bulkCapable)
|
|
2906
3052
|
this.lsmEngine.setBulkLoad(true);
|
|
2907
|
-
let
|
|
2908
|
-
let
|
|
3053
|
+
let nounResult;
|
|
3054
|
+
let verbResult;
|
|
2909
3055
|
try {
|
|
2910
|
-
|
|
2911
|
-
|
|
3056
|
+
nounResult = await this.rebuildEntityMetadata('noun');
|
|
3057
|
+
verbResult = await this.rebuildEntityMetadata('verb');
|
|
2912
3058
|
}
|
|
2913
3059
|
finally {
|
|
2914
3060
|
// Leaving bulk mode performs the walk's single durability point
|
|
@@ -2922,7 +3068,22 @@ export class MetadataIndexManager {
|
|
|
2922
3068
|
this.isRebuilding = false;
|
|
2923
3069
|
await this.flushRebuildDirty();
|
|
2924
3070
|
await this.flush();
|
|
2925
|
-
|
|
3071
|
+
// Persist what this COMPLETED walk PROVED unpostable (each id
|
|
3072
|
+
// verified by direct canonical read) so the next boot's strand
|
|
3073
|
+
// detector uses the honest floor instead of re-flagging the same
|
|
3074
|
+
// cohort forever. Refreshed on every completed rebuild; an empty
|
|
3075
|
+
// set clears the stamp.
|
|
3076
|
+
await this.storage.saveMetadata(UNPOSTABLE_STAMP_KEY, {
|
|
3077
|
+
nounCount: nounResult.noMetadata,
|
|
3078
|
+
nounIds: nounResult.noMetadataIds,
|
|
3079
|
+
verbCount: verbResult.noMetadata,
|
|
3080
|
+
verifiedAt: Date.now(),
|
|
3081
|
+
});
|
|
3082
|
+
opsLine(`Metadata index rebuild completed! Processed ${nounResult.processed} nouns and ` +
|
|
3083
|
+
`${verbResult.processed} verbs` +
|
|
3084
|
+
(nounResult.noMetadata > 0
|
|
3085
|
+
? ` (${nounResult.noMetadata} canonical noun(s) verified metadata-less — stamped for the strand detector)`
|
|
3086
|
+
: ''));
|
|
2926
3087
|
// A completed rebuild re-posted every canonical entity into a freshly
|
|
2927
3088
|
// re-opened engine (reopenLsmEngineFresh wiped the stale shard set) —
|
|
2928
3089
|
// any cold-open strand verdict is now satisfied.
|
|
@@ -2950,6 +3111,7 @@ export class MetadataIndexManager {
|
|
|
2950
3111
|
let processed = 0;
|
|
2951
3112
|
let sinceFlush = 0;
|
|
2952
3113
|
let noMetadata = 0;
|
|
3114
|
+
const noMetadataIds = [];
|
|
2953
3115
|
// Heal observability (CORTEX-RESTART-STRAND, memory's ask): a rebuild
|
|
2954
3116
|
// must never be silent for minutes — the operator cannot distinguish
|
|
2955
3117
|
// converging from stuck. Every 500 posted entities logs progress with
|
|
@@ -2990,8 +3152,8 @@ export class MetadataIndexManager {
|
|
|
2990
3152
|
if (processed - lastProgressAt >= 500) {
|
|
2991
3153
|
lastProgressAt = processed;
|
|
2992
3154
|
const elapsedS = (Date.now() - startedAt) / 1000;
|
|
2993
|
-
|
|
2994
|
-
`
|
|
3155
|
+
opsLine(`rebuild(${kind}): ${processed} posted in ${elapsedS.toFixed(1)}s ` +
|
|
3156
|
+
`(${(processed / Math.max(elapsedS, 0.001)).toFixed(0)}/s)`);
|
|
2995
3157
|
}
|
|
2996
3158
|
if (++sinceFlush >= 5000) {
|
|
2997
3159
|
await this.flushRebuildDirty();
|
|
@@ -3000,22 +3162,25 @@ export class MetadataIndexManager {
|
|
|
3000
3162
|
}
|
|
3001
3163
|
else {
|
|
3002
3164
|
noMetadata++;
|
|
3165
|
+
if (noMetadataIds.length < UNPOSTABLE_ID_CAP)
|
|
3166
|
+
noMetadataIds.push(it.id);
|
|
3003
3167
|
}
|
|
3004
3168
|
}
|
|
3005
3169
|
hasMore = page.hasMore;
|
|
3006
3170
|
await this.yieldToEventLoop();
|
|
3007
3171
|
}
|
|
3008
3172
|
const totalS = (Date.now() - startedAt) / 1000;
|
|
3009
|
-
|
|
3173
|
+
opsLine(`rebuild(${kind}): COMPLETE — ${processed} posted ` +
|
|
3010
3174
|
`in ${totalS.toFixed(1)}s (${(processed / Math.max(totalS, 0.001)).toFixed(0)}/s)` +
|
|
3011
3175
|
(noMetadata > 0 ? `, ${noMetadata} without metadata` : ''));
|
|
3012
3176
|
if (noMetadata > 0) {
|
|
3013
|
-
|
|
3177
|
+
opsLine(`rebuild(${kind}): ${noMetadata} enumerated ` +
|
|
3014
3178
|
`entit${noMetadata === 1 ? 'y' : 'ies'} carried no canonical ` +
|
|
3015
3179
|
`metadata (verified by direct read) and were not indexed — ` +
|
|
3016
|
-
`expected only for metadata-less legacy records
|
|
3180
|
+
`expected only for metadata-less legacy records. First ids: ` +
|
|
3181
|
+
noMetadataIds.slice(0, 10).join(', '));
|
|
3017
3182
|
}
|
|
3018
|
-
return processed;
|
|
3183
|
+
return { processed, noMetadata, noMetadataIds };
|
|
3019
3184
|
}
|
|
3020
3185
|
/**
|
|
3021
3186
|
* Rebuild-path verify-read for a single entity's canonical metadata.
|
|
@@ -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",
|