@dogfood-lab/ingest 1.8.0 → 1.10.0
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/lib/integrity.js +15 -1
- package/lib/unsafe-segment.js +15 -7
- package/load-context.js +344 -55
- package/package.json +1 -1
- package/persist.js +250 -5
- package/rebuild-indexes.js +18 -2
- package/run.js +525 -44
- package/verify-chain.js +253 -17
package/verify-chain.js
CHANGED
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
* push that touched a record but not the chain), and it catches middle-deletion,
|
|
28
28
|
* reorder, and forged-insertion (they break the seq run or the prev-link).
|
|
29
29
|
*
|
|
30
|
+
* The `pre_adoption` exemption inside collectOrphanRecords (below) narrows the
|
|
31
|
+
* "push that touched a record but not the chain" promise one step further: it
|
|
32
|
+
* excuses an un-ledgered, no-integrity record from `ok` only when the record's
|
|
33
|
+
* OWN claimed timing.finished_at/verification.verified_at predates
|
|
34
|
+
* CHAIN_ADOPTION_DATE (F-1907b2bc, wave 14), so dropping a record with a
|
|
35
|
+
* fabricated RECENT date is still caught as a genuine orphan. Dropping one with
|
|
36
|
+
* a fabricated OLD date is not caught — that record is indistinguishable from
|
|
37
|
+
* genuine history, because both are self-reported fields the same privileged
|
|
38
|
+
* writer controls. That residual is the SAME already-disclosed ingest-write-
|
|
39
|
+
* credential trust boundary named two paragraphs up, not a new one.
|
|
40
|
+
*
|
|
30
41
|
* KNOWN LIMITATION — tail truncation. Removing the most-recent entries (and
|
|
31
42
|
* their record files) leaves a SHORTER but internally-consistent chain, which
|
|
32
43
|
* verifies OK: the offline verifier has no external record of the expected head
|
|
@@ -72,7 +83,16 @@ import { findJsonFiles } from './rebuild-indexes.js';
|
|
|
72
83
|
* @property {ChainBreak[]} [breaks] - All independent breaks (only present when
|
|
73
84
|
* `collectAllBreaks` is set). The first element equals `break`.
|
|
74
85
|
* @property {ChainOrphan[]} [orphans] - Records on disk but absent from the
|
|
75
|
-
* ledger (only present when `collectOrphans`
|
|
86
|
+
* ledger AND written by chain-aware code (only present when `collectOrphans`
|
|
87
|
+
* is set). These are genuine torn writes — persist-succeeded, ledger-append-
|
|
88
|
+
* missed — and they make `ok` false.
|
|
89
|
+
* @property {ChainOrphan[]} [pre_adoption] - Records on disk but absent from
|
|
90
|
+
* the ledger that predate the chain's adoption: no `integrity` block AND a
|
|
91
|
+
* self-reported timing.finished_at/verification.verified_at before
|
|
92
|
+
* CHAIN_ADOPTION_DATE (F-1907b2bc — a no-integrity record whose own claimed
|
|
93
|
+
* timestamp is on/after that date is a genuine `orphans` entry instead;
|
|
94
|
+
* only present when `collectOrphans` is set). Visible for operator
|
|
95
|
+
* awareness; does NOT make `ok` false.
|
|
76
96
|
*/
|
|
77
97
|
|
|
78
98
|
/**
|
|
@@ -93,6 +113,10 @@ import { findJsonFiles } from './rebuild-indexes.js';
|
|
|
93
113
|
* ledger (a torn write between record-write and ledger-append). An orphan
|
|
94
114
|
* makes the result `ok: false` — the audit DETECTS the orphan instead of
|
|
95
115
|
* silently passing while a real record lives outside the tamper-evident chain.
|
|
116
|
+
* F-29134790 (wave 12): also returns `pre_adoption[]` — records absent from
|
|
117
|
+
* the ledger that predate the chain's existence entirely (see
|
|
118
|
+
* collectOrphanRecords's adoption-boundary doc below). Pre-adoption records
|
|
119
|
+
* are visible for operator awareness but do NOT affect `ok`.
|
|
96
120
|
* @returns {ChainVerifyResult}
|
|
97
121
|
*/
|
|
98
122
|
export function verifyChain(repoRoot, opts = {}) {
|
|
@@ -112,7 +136,10 @@ export function verifyChain(repoRoot, opts = {}) {
|
|
|
112
136
|
head_digest: GENESIS_DIGEST,
|
|
113
137
|
break: brk,
|
|
114
138
|
...(collectAllBreaks ? { breaks: [brk] } : {}),
|
|
115
|
-
|
|
139
|
+
// pre_adoption included alongside orphans (both empty here) so the
|
|
140
|
+
// result shape is consistent whenever collectOrphans is set, regardless
|
|
141
|
+
// of which early-return path produced it.
|
|
142
|
+
...(collectOrphans ? { orphans: [], pre_adoption: [] } : {}),
|
|
116
143
|
};
|
|
117
144
|
}
|
|
118
145
|
|
|
@@ -191,8 +218,12 @@ export function verifyChain(repoRoot, opts = {}) {
|
|
|
191
218
|
prevDigest = entry.submission_digest;
|
|
192
219
|
}
|
|
193
220
|
|
|
194
|
-
const
|
|
221
|
+
const reconciled = collectOrphans ? collectOrphanRecords(repoRoot, entries) : null;
|
|
222
|
+
const orphans = reconciled ? reconciled.orphans : null;
|
|
223
|
+
const preAdoption = reconciled ? reconciled.preAdoption : null;
|
|
195
224
|
|
|
225
|
+
// Only genuine orphans (post-adoption, ledger-eligible records) affect ok.
|
|
226
|
+
// pre_adoption is informational — see collectOrphanRecords's doc comment.
|
|
196
227
|
const ok = breaks.length === 0 && (orphans === null || orphans.length === 0);
|
|
197
228
|
return {
|
|
198
229
|
ok,
|
|
@@ -200,36 +231,168 @@ export function verifyChain(repoRoot, opts = {}) {
|
|
|
200
231
|
head_digest: headDigest,
|
|
201
232
|
break: breaks.length > 0 ? breaks[0] : null,
|
|
202
233
|
...(collectAllBreaks ? { breaks } : {}),
|
|
203
|
-
...(collectOrphans ? { orphans } : {}),
|
|
234
|
+
...(collectOrphans ? { orphans, pre_adoption: preAdoption } : {}),
|
|
204
235
|
};
|
|
205
236
|
}
|
|
206
237
|
|
|
238
|
+
/**
|
|
239
|
+
* The commit that gave persist.js's writeRecord its UNCONDITIONAL
|
|
240
|
+
* integrity-stamping logic — f4ca987 "feat(integrity): tamper-evident hash
|
|
241
|
+
* chain over records/", 2026-06-21T18:35:36-04:00 (2026-06-21T22:35:36Z).
|
|
242
|
+
* Every record genuinely written by writeRecord() from this date onward
|
|
243
|
+
* carries a fully-formed `integrity` block; a record with none at all can
|
|
244
|
+
* only predate it — see collectOrphanRecords' F-1907b2bc doc paragraph below.
|
|
245
|
+
*
|
|
246
|
+
* Set to the START of the commit's UTC calendar day, not its exact
|
|
247
|
+
* timestamp, so the boundary errs toward NOT excusing a borderline record
|
|
248
|
+
* (the fail-loud direction) rather than toward hair-splitting a commit
|
|
249
|
+
* second no real record is anywhere near — the actual gap in this repo's own
|
|
250
|
+
* history is five weeks wide (last genuine pre-chain record 2026-05-26,
|
|
251
|
+
* first genuine ledgered record 2026-07-03; see F-29134790's doc paragraph
|
|
252
|
+
* below), so this constant has a wide margin on both sides.
|
|
253
|
+
*/
|
|
254
|
+
export const CHAIN_ADOPTION_DATE = new Date('2026-06-21T00:00:00Z');
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The later of a record's own self-reported `timing.finished_at` /
|
|
258
|
+
* `verification.verified_at`, or `null` when neither is present and
|
|
259
|
+
* parseable. F-1907b2bc: used only to sanity-check a would-be `preAdoption`
|
|
260
|
+
* bucketing — this is NOT a trust boundary (the same privileged writer that
|
|
261
|
+
* can drop a record with no `integrity` block can also forge these fields;
|
|
262
|
+
* see the top-of-file threat-model paragraph). It only stops the tool from
|
|
263
|
+
* asserting "predates the chain" when the record's own data already
|
|
264
|
+
* contradicts that claim, or supports no claim at all.
|
|
265
|
+
*
|
|
266
|
+
* F-a85b7e84 (wave 16, LOW): a candidate must be a `string` before it is
|
|
267
|
+
* handed to `new Date()`. JSON has no Date type — every field a real
|
|
268
|
+
* writeRecord() call ever produces here is a string (Date.prototype.toJSON
|
|
269
|
+
* always serializes to one) or absent — so `typeof value === 'string'` costs
|
|
270
|
+
* nothing against genuine records. Without it, `new Date()`'s permissive
|
|
271
|
+
* ToPrimitive coercion accepts non-date JSON values that a bare falsy check
|
|
272
|
+
* (`if (!value) continue`) does not filter out: `new Date(true)` coerces the
|
|
273
|
+
* boolean to the number 1 (1970-01-01T00:00:00.001Z), a bare integer is read
|
|
274
|
+
* as an epoch-millisecond offset, and an array's comma-joined
|
|
275
|
+
* `toString()` (e.g. `[2026, 3, 1]` -> `'2026,3,1'`) fallback-parses as a
|
|
276
|
+
* real date — all landing well before CHAIN_ADOPTION_DATE with none of the
|
|
277
|
+
* "type a plausible fake ISO string" effort the surviving forgery residual
|
|
278
|
+
* (this function's own doc paragraph above) assumes. A non-string value now
|
|
279
|
+
* falls through to the same "no readable timestamp" orphan branch as an
|
|
280
|
+
* absent field, exactly like an unparseable string already did.
|
|
281
|
+
*
|
|
282
|
+
* @param {object} record
|
|
283
|
+
* @returns {Date|null}
|
|
284
|
+
*/
|
|
285
|
+
function latestClaimedTimestamp(record) {
|
|
286
|
+
const candidates = [record.timing?.finished_at, record.verification?.verified_at];
|
|
287
|
+
let latest = null;
|
|
288
|
+
for (const value of candidates) {
|
|
289
|
+
if (typeof value !== 'string') continue;
|
|
290
|
+
const parsed = new Date(value);
|
|
291
|
+
if (isNaN(parsed.getTime())) continue;
|
|
292
|
+
if (latest === null || parsed.getTime() > latest.getTime()) latest = parsed;
|
|
293
|
+
}
|
|
294
|
+
return latest;
|
|
295
|
+
}
|
|
296
|
+
|
|
207
297
|
/**
|
|
208
298
|
* INGEST-PROACT-001 — reconcile on-disk records against the ledger.
|
|
209
299
|
*
|
|
210
300
|
* Walks every `*.json` under `records/` (accepted AND `_rejected/`) and flags
|
|
211
|
-
* any record file whose
|
|
212
|
-
*
|
|
213
|
-
*
|
|
301
|
+
* any record file whose repo-relative PATH is absent from the ledger. Such a
|
|
302
|
+
* file is an ORPHAN: persist wrote the record but the ledger append did not
|
|
303
|
+
* happen (a crash in the torn window between the two steps), so the record
|
|
214
304
|
* exists OUTSIDE the tamper-evident chain and the plain chain-walk is blind to
|
|
215
305
|
* it. The chain-manifest's own ledger lines are NOT under `records/`, so they
|
|
216
306
|
* are never mistaken for orphans.
|
|
217
307
|
*
|
|
308
|
+
* F-d4bcf5d0 (wave 10, HIGH): this used to ALSO fall back to a run_id-only
|
|
309
|
+
* match — "a record is accounted for if EITHER its path OR its run_id is
|
|
310
|
+
* ledgered anywhere". That fallback was unsound: a single run_id can
|
|
311
|
+
* legitimately own TWO on-disk records at TWO different paths (the
|
|
312
|
+
* accepted-after-a-prior-rejection resubmission flow persist.js's
|
|
313
|
+
* isRetryableRejection carve-out exists for — see
|
|
314
|
+
* F-0f9e4077/F-4036ae25/F-f8952a50), each needing its OWN ledger line. Once
|
|
315
|
+
* ANY entry for a run_id was ledgered, the fallback treated EVERY later
|
|
316
|
+
* record sharing that run_id as accounted for — including one at a
|
|
317
|
+
* genuinely un-ledgered path, exactly the torn-write shape this
|
|
318
|
+
* reconciliation pass exists to catch (proven live: a ledgered `_rejected`
|
|
319
|
+
* record followed by an accepted record for the SAME run_id whose OWN
|
|
320
|
+
* ledger line was then lost — the plain run_id match hid the orphan).
|
|
321
|
+
* Path identity is both the necessary AND the sufficient check: every ledger
|
|
322
|
+
* line carries an exact `path` field (lib/chain-manifest.js's documented
|
|
323
|
+
* line shape, always populated by persist.js's appendChainEntry call), and
|
|
324
|
+
* this function's own `relPath` computation (`relative(repoRoot,
|
|
325
|
+
* absPath).split(sep).join('/')`) is the identical normalization persist.js
|
|
326
|
+
* uses to produce that field. Dropping the run_id fallback removes the
|
|
327
|
+
* unsound generalization without weakening real coverage — see
|
|
328
|
+
* ingest-proact-001-004-verify-chain-reconcile-allbreaks.test.js's
|
|
329
|
+
* 'shared run_id, different paths' regression test.
|
|
330
|
+
*
|
|
331
|
+
* F-29134790 (wave 12, HIGH): path-identity membership is correct for records
|
|
332
|
+
* the chain COULD have ledgered, but on its own it has no ADOPTION BOUNDARY —
|
|
333
|
+
* every un-ledgered path reads identically, so a record that predates the
|
|
334
|
+
* chain feature's existence is indistinguishable from a genuine crash-window
|
|
335
|
+
* torn write. Proven live against this repo's own `records/` tree: 53 files
|
|
336
|
+
* with no ledger line, every one dated 2026-03-19 through 2026-05-26 — all
|
|
337
|
+
* BEFORE persist.js gained its integrity-stamping logic (f4ca987,
|
|
338
|
+
* 2026-06-21) — versus the 7 real ledgered records, all dated 2026-07-03
|
|
339
|
+
* onward. Pre-fix, `verifyChain({ collectOrphans: true })` reported all 53 as
|
|
340
|
+
* indistinguishable "torn persist" orphans (a 53:0 signal-to-noise ratio; a
|
|
341
|
+
* genuine orphan today would be the 54th unlabeled line).
|
|
342
|
+
*
|
|
343
|
+
* The fix needs no new marker file or backfill tool: persist.js's writeRecord
|
|
344
|
+
* already stamps `record.integrity = { submission_digest, prev_digest, seq }`
|
|
345
|
+
* UNCONDITIONALLY, in memory, BEFORE it writes the record file and BEFORE it
|
|
346
|
+
* appends the ledger line (see persist.js — the stamp happens ahead of
|
|
347
|
+
* validateRecord() and the atomic file write). That ordering guarantees a
|
|
348
|
+
* genuine post-adoption torn write (file landed, ledger append lost) still
|
|
349
|
+
* carries a fully-formed `integrity` block on disk; only a record written
|
|
350
|
+
* before persist.js gained this stamping logic can lack one entirely. So
|
|
351
|
+
* "does this record carry an `integrity` block" is not a proxy for the
|
|
352
|
+
* adoption boundary — it IS the adoption boundary, already recorded per-record
|
|
353
|
+
* by the exact code change that introduced the chain, for free. A record
|
|
354
|
+
* missing `integrity` is bucketed into the returned `preAdoption` array
|
|
355
|
+
* (visible, does not fail `ok`); a record WITH an `integrity` block that is
|
|
356
|
+
* still un-ledgered remains a genuine `orphans` entry exactly as before.
|
|
357
|
+
*
|
|
358
|
+
* F-1907b2bc (wave 14, MEDIUM): the paragraph above treats "does this record
|
|
359
|
+
* carry an `integrity` block" as the WHOLE adoption boundary, but that
|
|
360
|
+
* equivalence only holds for records that actually passed through the real
|
|
361
|
+
* writeRecord(). A record dropped directly onto disk — bypassing writeRecord
|
|
362
|
+
* entirely — also has no `integrity` block, and pre-fix was silently
|
|
363
|
+
* absorbed into `preAdoption` with a "predates the chain" reason string that
|
|
364
|
+
* is provably FALSE whenever the dropped record's own claimed timestamp is on
|
|
365
|
+
* or after CHAIN_ADOPTION_DATE (persist.js has stamped `integrity`
|
|
366
|
+
* UNCONDITIONALLY on every record it writes since that date — see the
|
|
367
|
+
* constant's doc comment above — so a record genuinely written from then on
|
|
368
|
+
* could not lack one). The fix cross-checks a would-be `preAdoption` record's
|
|
369
|
+
* own `timing.finished_at`/`verification.verified_at` (the later of the two
|
|
370
|
+
* — see `latestClaimedTimestamp`) against CHAIN_ADOPTION_DATE before
|
|
371
|
+
* accepting the "predates the chain" narrative; a record with no verifiable
|
|
372
|
+
* timestamp of its own, or one on/after the cutoff, is bucketed into
|
|
373
|
+
* `orphans` instead, with a reason string honest about why. This closes the
|
|
374
|
+
* LOW-effort version of dropping a forged record (not bothering to fake a
|
|
375
|
+
* plausible pre-adoption date) but not a DILIGENT one that also forges an old
|
|
376
|
+
* timing/verification block — no purely offline, record-content-based check
|
|
377
|
+
* can tell a forged old date from a genuine one; see the threat-model
|
|
378
|
+
* paragraph at the top of this file.
|
|
379
|
+
*
|
|
218
380
|
* @param {string} repoRoot
|
|
219
381
|
* @param {Array<object>} entries - The ledger entries (already read).
|
|
220
|
-
* @returns {ChainOrphan[]}
|
|
382
|
+
* @returns {{ orphans: ChainOrphan[], preAdoption: ChainOrphan[] }} `orphans`
|
|
383
|
+
* are un-ledgered records stamped by chain-aware code — genuine torn
|
|
384
|
+
* writes. `preAdoption` are un-ledgered records with no `integrity` block —
|
|
385
|
+
* they predate the chain and were never ledgerable. Only `orphans` affects
|
|
386
|
+
* verifyChain's `ok`.
|
|
221
387
|
*/
|
|
222
388
|
function collectOrphanRecords(repoRoot, entries) {
|
|
223
|
-
// Index the ledger by run_id and by repo-relative path so a record matches if
|
|
224
|
-
// EITHER identity is present — a record is ledgered as long as its line exists.
|
|
225
|
-
const ledgerRunIds = new Set();
|
|
226
389
|
const ledgerPaths = new Set();
|
|
227
390
|
for (const e of entries) {
|
|
228
|
-
if (e.run_id) ledgerRunIds.add(e.run_id);
|
|
229
391
|
if (e.path) ledgerPaths.add(e.path);
|
|
230
392
|
}
|
|
231
393
|
|
|
232
394
|
const orphans = [];
|
|
395
|
+
const preAdoption = [];
|
|
233
396
|
const recordsDir = join(repoRoot, 'records');
|
|
234
397
|
for (const absPath of findJsonFiles(recordsDir)) {
|
|
235
398
|
const relPath = relative(repoRoot, absPath).split(sep).join('/');
|
|
@@ -240,7 +403,9 @@ function collectOrphanRecords(repoRoot, entries) {
|
|
|
240
403
|
record = JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
241
404
|
} catch {
|
|
242
405
|
// A record file we cannot even parse, that is also absent from the ledger,
|
|
243
|
-
// is still an orphan — report it with what little identity we have.
|
|
406
|
+
// is still an orphan — report it with what little identity we have. We
|
|
407
|
+
// cannot read `.integrity` to classify it as pre-adoption either, so this
|
|
408
|
+
// fails loud rather than being silently excused.
|
|
244
409
|
orphans.push({
|
|
245
410
|
run_id: null,
|
|
246
411
|
seq: null,
|
|
@@ -250,11 +415,67 @@ function collectOrphanRecords(repoRoot, entries) {
|
|
|
250
415
|
continue;
|
|
251
416
|
}
|
|
252
417
|
|
|
253
|
-
|
|
254
|
-
|
|
418
|
+
// A value that parses but is not a plain record object (null, an array, a
|
|
419
|
+
// bare primitive) is not something we can read `.integrity` off to
|
|
420
|
+
// classify as pre-adoption. Mirroring the not-valid-JSON branch above:
|
|
421
|
+
// when we cannot positively identify what the record actually IS, fail
|
|
422
|
+
// loud as a genuine orphan rather than silently excusing it into
|
|
423
|
+
// preAdoption — the adoption boundary is a claim we can only make about
|
|
424
|
+
// something shaped like a record.
|
|
425
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) {
|
|
426
|
+
orphans.push({
|
|
427
|
+
run_id: null,
|
|
428
|
+
seq: null,
|
|
429
|
+
path: relPath,
|
|
430
|
+
reason: `record file present on disk but absent from the ledger, and its JSON content is not a record object`,
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Adoption boundary — see the F-29134790 doc paragraph above. "No
|
|
436
|
+
// integrity block" is NECESSARY for pre-adoption but, on its own, not
|
|
437
|
+
// SUFFICIENT — see the F-1907b2bc doc paragraph above. Cross-check the
|
|
438
|
+
// record's own claimed timestamp against CHAIN_ADOPTION_DATE before
|
|
439
|
+
// accepting the "predates the chain" narrative.
|
|
440
|
+
if (record.integrity == null) {
|
|
441
|
+
const claimedAt = latestClaimedTimestamp(record);
|
|
442
|
+
const isPreAdoption = claimedAt !== null && claimedAt.getTime() < CHAIN_ADOPTION_DATE.getTime();
|
|
443
|
+
|
|
444
|
+
if (isPreAdoption) {
|
|
445
|
+
preAdoption.push({
|
|
446
|
+
run_id: record.run_id ?? null,
|
|
447
|
+
seq: null,
|
|
448
|
+
path: relPath,
|
|
449
|
+
reason:
|
|
450
|
+
`record predates the integrity chain (no integrity block — written ` +
|
|
451
|
+
`before tamper-evident chaining was introduced); expected for ` +
|
|
452
|
+
`historical records, not counted as an audit failure.`,
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
orphans.push({
|
|
458
|
+
run_id: record.run_id ?? null,
|
|
459
|
+
seq: null,
|
|
460
|
+
path: relPath,
|
|
461
|
+
reason: claimedAt === null
|
|
462
|
+
? `record has no integrity block, and no readable timing.finished_at ` +
|
|
463
|
+
`or verification.verified_at either — its "predates the chain" claim ` +
|
|
464
|
+
`cannot be supported by anything on the record itself, so it is ` +
|
|
465
|
+
`treated as a genuine orphan rather than silently excused as historical.`
|
|
466
|
+
: `record has no integrity block, but its own timing.finished_at / ` +
|
|
467
|
+
`verification.verified_at (latest: ${claimedAt.toISOString()}) is on or ` +
|
|
468
|
+
`after the chain-adoption date (${CHAIN_ADOPTION_DATE.toISOString()}) — ` +
|
|
469
|
+
`a record genuinely written by writeRecord() from that date onward ` +
|
|
470
|
+
`always carries an integrity block, so this one cannot genuinely ` +
|
|
471
|
+
`predate the chain. Re-run ingest for this record or investigate how ` +
|
|
472
|
+
`it reached disk without one.`,
|
|
473
|
+
});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
255
476
|
|
|
256
477
|
orphans.push({
|
|
257
|
-
run_id:
|
|
478
|
+
run_id: record.run_id ?? null,
|
|
258
479
|
seq: record.integrity?.seq ?? null,
|
|
259
480
|
path: relPath,
|
|
260
481
|
reason:
|
|
@@ -264,7 +485,7 @@ function collectOrphanRecords(repoRoot, entries) {
|
|
|
264
485
|
});
|
|
265
486
|
}
|
|
266
487
|
|
|
267
|
-
return orphans;
|
|
488
|
+
return { orphans, preAdoption };
|
|
268
489
|
}
|
|
269
490
|
|
|
270
491
|
/**
|
|
@@ -282,6 +503,13 @@ export function formatChainResult(result) {
|
|
|
282
503
|
];
|
|
283
504
|
if (Array.isArray(result.orphans)) {
|
|
284
505
|
lines.push(`reconciliation: 0 orphan record(s) on disk`);
|
|
506
|
+
// F-29134790: ok:true only guarantees zero GENUINE orphans — pre-adoption
|
|
507
|
+
// records (no integrity block) are excused from ok but still worth
|
|
508
|
+
// surfacing so an operator isn't left wondering why the disk record
|
|
509
|
+
// count exceeds the ledgered count.
|
|
510
|
+
if (Array.isArray(result.pre_adoption) && result.pre_adoption.length > 0) {
|
|
511
|
+
lines.push(` (${result.pre_adoption.length} pre-chain record(s) predate the integrity ledger and are excluded from reconciliation)`);
|
|
512
|
+
}
|
|
285
513
|
}
|
|
286
514
|
return lines;
|
|
287
515
|
}
|
|
@@ -312,5 +540,13 @@ export function formatChainResult(result) {
|
|
|
312
540
|
}
|
|
313
541
|
}
|
|
314
542
|
|
|
543
|
+
// F-29134790: pre-adoption records never make ok false, but when the result
|
|
544
|
+
// IS not-ok for some other reason (a break, or a genuine orphan), still name
|
|
545
|
+
// the excluded count so an operator triaging the failure isn't misled into
|
|
546
|
+
// thinking every un-ledgered record on disk is part of the break.
|
|
547
|
+
if (Array.isArray(result.pre_adoption) && result.pre_adoption.length > 0) {
|
|
548
|
+
lines.push(`reconciliation: ${result.pre_adoption.length} pre-chain record(s) predate the integrity ledger (excluded from reconciliation, not an audit failure)`);
|
|
549
|
+
}
|
|
550
|
+
|
|
315
551
|
return lines;
|
|
316
552
|
}
|