@cleocode/contracts 2026.5.108 → 2026.5.110
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/__tests__/db-inventory.test.d.ts +25 -0
- package/dist/__tests__/db-inventory.test.d.ts.map +1 -0
- package/dist/__tests__/db-inventory.test.js +148 -0
- package/dist/__tests__/db-inventory.test.js.map +1 -0
- package/dist/__tests__/memory-wire-shapes.test.js.map +1 -1
- package/dist/__tests__/task-type-narrowing.test.d.ts +14 -0
- package/dist/__tests__/task-type-narrowing.test.d.ts.map +1 -0
- package/dist/__tests__/task-type-narrowing.test.js +51 -0
- package/dist/__tests__/task-type-narrowing.test.js.map +1 -0
- package/dist/backup-verify.d.ts +136 -0
- package/dist/backup-verify.d.ts.map +1 -0
- package/dist/backup-verify.js +29 -0
- package/dist/backup-verify.js.map +1 -0
- package/dist/brain.d.ts +103 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/db-inventory.d.ts +200 -0
- package/dist/db-inventory.d.ts.map +1 -0
- package/dist/db-inventory.js +76 -0
- package/dist/db-inventory.js.map +1 -0
- package/dist/db-inventory.json +165 -0
- package/dist/db-recovery.d.ts +141 -0
- package/dist/db-recovery.d.ts.map +1 -0
- package/dist/db-recovery.js +29 -0
- package/dist/db-recovery.js.map +1 -0
- package/dist/doctor.d.ts +605 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/observe.d.ts +8 -0
- package/dist/memory/observe.d.ts.map +1 -1
- package/dist/operations/tasks.d.ts +4 -4
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/task.d.ts +13 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js.map +1 -1
- package/package.json +5 -4
- package/src/__tests__/db-inventory.test.ts +184 -0
- package/src/__tests__/memory-wire-shapes.test.ts +1 -0
- package/src/__tests__/task-type-narrowing.test.ts +56 -0
- package/src/backup-verify.ts +140 -0
- package/src/brain.ts +114 -0
- package/src/db-inventory.json +165 -0
- package/src/db-inventory.ts +234 -0
- package/src/db-recovery.ts +144 -0
- package/src/doctor.ts +634 -0
- package/src/index.ts +46 -0
- package/src/memory/observe.ts +8 -0
- package/src/operations/tasks.ts +4 -4
- package/src/task.ts +13 -2
package/src/doctor.ts
CHANGED
|
@@ -286,6 +286,491 @@ export interface SagaAuditResult {
|
|
|
286
286
|
driftCount: number;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// DB-Substrate Survey (T10307 — Saga T10281 SG-BRAIN-DB-RESILIENCE / Epic T10282)
|
|
291
|
+
// ============================================================================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Per-database substrate-audit findings.
|
|
295
|
+
*
|
|
296
|
+
* Produced by `surveyProjectDbSubstrate` for every entry in the
|
|
297
|
+
* `DB_INVENTORY` SSoT. Fields that depend on the file existing on disk
|
|
298
|
+
* (`integrityOK`, `rowCounts`, `lastWriteMs`, `sizeBytes`) are `null` when
|
|
299
|
+
* the file is missing. `error` is non-null when integrity_check or the
|
|
300
|
+
* open / row-count round-trip threw — corrupt DBs surface here so the
|
|
301
|
+
* envelope's `summary.corrupt` counter increments.
|
|
302
|
+
*
|
|
303
|
+
* `suggestedFix` is a stable, machine-readable repair command (typically
|
|
304
|
+
* `cleo backup recover <role>` — the verb introduced by T10304). Always
|
|
305
|
+
* populated when `integrityOK === false` so the operator has a one-line
|
|
306
|
+
* remediation path.
|
|
307
|
+
*
|
|
308
|
+
* @task T10307
|
|
309
|
+
* @task T10312 — added `quarantinedTo`, `integrityCheckMs`, `timedOut`
|
|
310
|
+
* for auto-quarantine + bounded-timeout behaviour.
|
|
311
|
+
* @epic T10282
|
|
312
|
+
* @saga T10281
|
|
313
|
+
*/
|
|
314
|
+
export interface DbSubstrateEntry {
|
|
315
|
+
/** Absolute on-disk path the inventory template resolved to. */
|
|
316
|
+
filePath: string;
|
|
317
|
+
/** `true` iff the file currently exists on disk. */
|
|
318
|
+
exists: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* Result of `PRAGMA integrity_check`. `true` when the pragma returned
|
|
321
|
+
* exactly one `'ok'` row; `false` when it returned anything else or
|
|
322
|
+
* the call threw; `null` when the file did not exist (no integrity
|
|
323
|
+
* check attempted).
|
|
324
|
+
*/
|
|
325
|
+
integrityOK: boolean | null;
|
|
326
|
+
/**
|
|
327
|
+
* Row counts for up to 3 representative tables (alphabetically first
|
|
328
|
+
* non-meta tables). `null` when the file is missing or the count
|
|
329
|
+
* round-trip threw.
|
|
330
|
+
*/
|
|
331
|
+
rowCounts: Readonly<Record<string, number>> | null;
|
|
332
|
+
/** `fs.statSync(filePath).mtimeMs`, or `null` when the file is missing. */
|
|
333
|
+
lastWriteMs: number | null;
|
|
334
|
+
/** `fs.statSync(filePath).size`, or `null` when the file is missing. */
|
|
335
|
+
sizeBytes: number | null;
|
|
336
|
+
/**
|
|
337
|
+
* Stable error string surfaced from a failed integrity_check or open.
|
|
338
|
+
* `null` when the survey succeeded (including when the file simply
|
|
339
|
+
* does not exist — that is `exists: false`, not an error).
|
|
340
|
+
*/
|
|
341
|
+
error: string | null;
|
|
342
|
+
/**
|
|
343
|
+
* Suggested repair command when `integrityOK === false`. `null`
|
|
344
|
+
* otherwise. Typically `cleo backup recover <role>` per T10304.
|
|
345
|
+
*
|
|
346
|
+
* When auto-quarantine fires (T10312) the suggestedFix is augmented
|
|
347
|
+
* with the quarantine path in the same string for human readability,
|
|
348
|
+
* while the machine-readable path lives in {@link quarantinedTo}.
|
|
349
|
+
*/
|
|
350
|
+
suggestedFix: string | null;
|
|
351
|
+
/**
|
|
352
|
+
* Drizzle migration coverage cross-check (T10311). `null` when:
|
|
353
|
+
* - the inventory entry has `migrationsDir === null` (derived or
|
|
354
|
+
* reserved roles); OR
|
|
355
|
+
* - the DB file does not exist; OR
|
|
356
|
+
* - the DB exists but has no `__drizzle_migrations` table yet (e.g.
|
|
357
|
+
* reserved opener that never ran migrations).
|
|
358
|
+
*
|
|
359
|
+
* Otherwise carries a populated {@link DbSubstrateMigrationCoverage}
|
|
360
|
+
* with the orphan-row / missing-file diffs.
|
|
361
|
+
*
|
|
362
|
+
* @task T10311
|
|
363
|
+
*/
|
|
364
|
+
migrationCoverage: DbSubstrateMigrationCoverage | null;
|
|
365
|
+
/**
|
|
366
|
+
* Pragma drift entries — one per pragma whose actual value (read from
|
|
367
|
+
* a raw read-only snapshot of this DB, no pragma application) differs
|
|
368
|
+
* from the SSoT expectation in `specs/sqlite-pragmas.json`.
|
|
369
|
+
*
|
|
370
|
+
* @remarks
|
|
371
|
+
* `null` when the DB does not exist on disk (no snapshot to query) OR
|
|
372
|
+
* the integrity_check failed (a corrupt DB cannot be reliably queried
|
|
373
|
+
* for pragmas). Empty array `[]` means the DB matches the canonical
|
|
374
|
+
* pragma SSoT exactly.
|
|
375
|
+
*
|
|
376
|
+
* @task T10310
|
|
377
|
+
*/
|
|
378
|
+
pragmaDrift: PragmaDriftItem[] | null;
|
|
379
|
+
/**
|
|
380
|
+
* Absolute path to the quarantine directory when `inspectDbFile`
|
|
381
|
+
* auto-quarantined a corrupt DB. `null` when the entry is healthy OR
|
|
382
|
+
* when auto-quarantine was disabled by the caller.
|
|
383
|
+
*
|
|
384
|
+
* Quarantine naming: `<projectRoot>/.cleo/quarantine/<role>-malformed-<iso>/`.
|
|
385
|
+
* The corrupt DB lands at `<quarantineDir>/<basename>.malformed`, and
|
|
386
|
+
* any sidecar `-wal` / `-shm` files are preserved alongside it.
|
|
387
|
+
*
|
|
388
|
+
* @task T10312
|
|
389
|
+
*/
|
|
390
|
+
quarantinedTo: string | null;
|
|
391
|
+
/**
|
|
392
|
+
* Wall-clock duration of the `PRAGMA integrity_check` call in
|
|
393
|
+
* milliseconds, or `null` when the file did not exist OR the open
|
|
394
|
+
* itself threw before the pragma could run.
|
|
395
|
+
*
|
|
396
|
+
* @task T10312
|
|
397
|
+
*/
|
|
398
|
+
integrityCheckMs: number | null;
|
|
399
|
+
/**
|
|
400
|
+
* `true` when the integrity_check wall-clock duration exceeded the
|
|
401
|
+
* configured `integrityCheckTimeoutMs`. Because `node:sqlite` is fully
|
|
402
|
+
* synchronous and offers no interrupt(), the check itself runs to
|
|
403
|
+
* completion — `timedOut: true` flags the DB as slow and is treated
|
|
404
|
+
* as an integrity failure for downstream gating (the entry's
|
|
405
|
+
* `integrityOK` is forced to `false` and `summary.corrupt` increments).
|
|
406
|
+
*
|
|
407
|
+
* @task T10312
|
|
408
|
+
*/
|
|
409
|
+
timedOut: boolean;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* One pragma drift item surfaced in {@link DbSubstrateEntry.pragmaDrift}.
|
|
414
|
+
*
|
|
415
|
+
* @remarks
|
|
416
|
+
* Compared values are normalised to lower-case strings before equality
|
|
417
|
+
* is asserted, matching SQLite's pragma-output conventions
|
|
418
|
+
* (`journal_mode` returns `wal` not `WAL`, `synchronous` returns
|
|
419
|
+
* `2` for `NORMAL`, etc. — see {@link PRAGMA_VALUE_NORMALISERS}).
|
|
420
|
+
*
|
|
421
|
+
* The triple `(pragma, expected, actual)` is stable across runs so
|
|
422
|
+
* downstream consumers can deduplicate / aggregate drift reports.
|
|
423
|
+
*
|
|
424
|
+
* @task T10310
|
|
425
|
+
* @epic T10283
|
|
426
|
+
* @saga T10281
|
|
427
|
+
*/
|
|
428
|
+
export interface PragmaDriftItem {
|
|
429
|
+
/** Name of the pragma (e.g. `journal_mode`, `busy_timeout`). */
|
|
430
|
+
pragma: string;
|
|
431
|
+
/**
|
|
432
|
+
* Expected value as declared in `specs/sqlite-pragmas.json`. Always a
|
|
433
|
+
* string — values are stored as strings in the SSoT so a future
|
|
434
|
+
* `0x12345` application_id literal does not lose its hex form to a
|
|
435
|
+
* number cast.
|
|
436
|
+
*/
|
|
437
|
+
expected: string;
|
|
438
|
+
/**
|
|
439
|
+
* Actual value as returned by `PRAGMA <name>` on a raw read-only
|
|
440
|
+
* snapshot. `null` when the pragma query threw (e.g. an exotic pragma
|
|
441
|
+
* name not implemented by this SQLite build).
|
|
442
|
+
*/
|
|
443
|
+
actual: string | null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* One orphan row in `__drizzle_migrations` — a journal entry whose hash
|
|
448
|
+
* does not match any migration file currently on disk for this role.
|
|
449
|
+
*
|
|
450
|
+
* @remarks
|
|
451
|
+
* Orphans indicate one of:
|
|
452
|
+
* - The DB was last written by a newer CLEO version whose migrations
|
|
453
|
+
* have since been deleted/renamed; OR
|
|
454
|
+
* - A hand-edited journal entry (manual SQL bootstrap that bypassed
|
|
455
|
+
* Drizzle); OR
|
|
456
|
+
* - A migration file was deleted from disk while its journal row
|
|
457
|
+
* remained — a charter violation that breaks T9686-class
|
|
458
|
+
* reconciliation.
|
|
459
|
+
*
|
|
460
|
+
* @task T10311
|
|
461
|
+
*/
|
|
462
|
+
export interface DbSubstrateMigrationOrphan {
|
|
463
|
+
/** SHA-256 hash recorded in `__drizzle_migrations.hash`. */
|
|
464
|
+
hash: string;
|
|
465
|
+
/**
|
|
466
|
+
* `__drizzle_migrations.created_at` (Drizzle convention: ms-since-epoch
|
|
467
|
+
* derived from the folder timestamp prefix). `null` when the column was
|
|
468
|
+
* present but `NULL`, or the column did not exist on this DB.
|
|
469
|
+
*/
|
|
470
|
+
createdAt: number | null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* One missing-file entry — a migration file present in `migrationsDir`
|
|
475
|
+
* whose hash is NOT recorded in `__drizzle_migrations`.
|
|
476
|
+
*
|
|
477
|
+
* @remarks
|
|
478
|
+
* Missing-file entries indicate the migration has not been applied yet
|
|
479
|
+
* (which is benign for a fresh DB) OR was applied without a journal write
|
|
480
|
+
* (the bug class `reconcileJournal` Scenario 3 was designed to catch).
|
|
481
|
+
*
|
|
482
|
+
* @task T10311
|
|
483
|
+
*/
|
|
484
|
+
export interface DbSubstrateMigrationMissing {
|
|
485
|
+
/** Migration directory name (e.g. `20260318205539_initial`). */
|
|
486
|
+
name: string;
|
|
487
|
+
/** SHA-256 hash of the migration's `migration.sql` (Drizzle convention). */
|
|
488
|
+
hash: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Per-DB Drizzle migration coverage diff produced by
|
|
493
|
+
* `cleo doctor db-substrate`.
|
|
494
|
+
*
|
|
495
|
+
* @remarks
|
|
496
|
+
* The substrate audit cross-references `__drizzle_migrations` rows (the
|
|
497
|
+
* applied set) with files in `migrationsDir` (the expected set). Both
|
|
498
|
+
* directions are surfaced so operators can distinguish:
|
|
499
|
+
*
|
|
500
|
+
* - **Orphan rows** (`orphanRows.length > 0`): the DB has applied
|
|
501
|
+
* migrations that the local checkout does not provide. Risky — a
|
|
502
|
+
* `reconcileJournal` Scenario 2A signal.
|
|
503
|
+
* - **Missing files** (`missingFiles.length > 0`): the local checkout
|
|
504
|
+
* has migrations the DB has not applied. Drizzle's `migrate()` will
|
|
505
|
+
* pick these up on next open; benign for a fresh DB, suspicious for
|
|
506
|
+
* a live one.
|
|
507
|
+
*
|
|
508
|
+
* Healthy coverage: `orphanRows.length === 0 && missingFiles.length === 0`
|
|
509
|
+
* AND `applied === expected`.
|
|
510
|
+
*
|
|
511
|
+
* @task T10311
|
|
512
|
+
*/
|
|
513
|
+
export interface DbSubstrateMigrationCoverage {
|
|
514
|
+
/** Number of rows in `__drizzle_migrations`. */
|
|
515
|
+
applied: number;
|
|
516
|
+
/** Number of migration directories in `migrationsDir`. */
|
|
517
|
+
expected: number;
|
|
518
|
+
/**
|
|
519
|
+
* Rows in `__drizzle_migrations` whose hash does not match any
|
|
520
|
+
* migration file on disk. Always present (may be empty).
|
|
521
|
+
*/
|
|
522
|
+
orphanRows: DbSubstrateMigrationOrphan[];
|
|
523
|
+
/**
|
|
524
|
+
* Migration files whose hash is not recorded in
|
|
525
|
+
* `__drizzle_migrations`. Always present (may be empty).
|
|
526
|
+
*/
|
|
527
|
+
missingFiles: DbSubstrateMigrationMissing[];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Warning kinds surfaced by `surveyDbSubstrate` via `envelope.meta.warnings`.
|
|
532
|
+
*
|
|
533
|
+
* @remarks
|
|
534
|
+
* Surfacing as warnings (not violations) means they do NOT drive a
|
|
535
|
+
* non-zero exit code on their own — operators should review and remediate,
|
|
536
|
+
* but the substrate is otherwise healthy.
|
|
537
|
+
*
|
|
538
|
+
* - `orphan-project-root` — a `.cleo/` directory found at a project-PARENT
|
|
539
|
+
* path (e.g. `/mnt/projects/.cleo/`). Regression class T9550.
|
|
540
|
+
* - `nested-nexus-duplicate` — `~/.local/share/cleo/nexus/{nexus,signaldock}.db`
|
|
541
|
+
* exists alongside the canonical flat `nexus.db` / `signaldock.db` files,
|
|
542
|
+
* indicating a structural duplicate from an older XDG-path resolution.
|
|
543
|
+
*
|
|
544
|
+
* @task T10307
|
|
545
|
+
*/
|
|
546
|
+
export type DbSubstrateWarningKind = 'orphan-project-root' | 'nested-nexus-duplicate';
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* One warning entry surfaced in the envelope's `meta.warnings` list.
|
|
550
|
+
*
|
|
551
|
+
* @task T10307
|
|
552
|
+
* @task T10308 — added `parentWorkspace` for orphan-project-root attribution.
|
|
553
|
+
*/
|
|
554
|
+
export interface DbSubstrateWarning {
|
|
555
|
+
/** Machine-readable warning class. */
|
|
556
|
+
kind: DbSubstrateWarningKind;
|
|
557
|
+
/** Absolute path of the offending file or directory. */
|
|
558
|
+
path: string;
|
|
559
|
+
/**
|
|
560
|
+
* `fs.statSync(path).mtimeMs` for the offending file/directory.
|
|
561
|
+
* `null` when stat() threw (e.g. ephemeral race during scan).
|
|
562
|
+
*/
|
|
563
|
+
lastWriteMs: number | null;
|
|
564
|
+
/**
|
|
565
|
+
* For `orphan-project-root` warnings only: the `workspace` field read
|
|
566
|
+
* from `<path>/.context-state.json` when present. Identifies which
|
|
567
|
+
* outside workspace last wrote into this orphan `.cleo/` directory
|
|
568
|
+
* (audit trail for the T9550 regression class). `null` when the
|
|
569
|
+
* file is absent, unreadable, or carries no `workspace` field.
|
|
570
|
+
*
|
|
571
|
+
* @task T10308
|
|
572
|
+
*/
|
|
573
|
+
parentWorkspace?: string | null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Survey result for one CLEO project (i.e. one `.cleo/` directory) plus
|
|
578
|
+
* the global tier of databases owned by it.
|
|
579
|
+
*
|
|
580
|
+
* @remarks
|
|
581
|
+
* `dbs` is keyed by the canonical role names from `DB_INVENTORY`. Every
|
|
582
|
+
* entry is populated regardless of whether the file exists — the caller
|
|
583
|
+
* can iterate every role and decide how to surface absent rows.
|
|
584
|
+
*
|
|
585
|
+
* @task T10307
|
|
586
|
+
*/
|
|
587
|
+
export interface DbSubstrateProjectSurvey {
|
|
588
|
+
/** Absolute path to the project root that was surveyed. */
|
|
589
|
+
projectRoot: string;
|
|
590
|
+
/**
|
|
591
|
+
* Stable identifier for the project — currently the `base64url(path)`
|
|
592
|
+
* truncated to 32 chars, matching the convention used by `cleo nexus`
|
|
593
|
+
* project resolution.
|
|
594
|
+
*/
|
|
595
|
+
projectId: string;
|
|
596
|
+
/** Per-role survey result, keyed by canonical role name. */
|
|
597
|
+
dbs: Readonly<Record<string, DbSubstrateEntry>>;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Aggregate counters surfaced in `envelope.data.summary`.
|
|
602
|
+
*
|
|
603
|
+
* @task T10307
|
|
604
|
+
*/
|
|
605
|
+
export interface DbSubstrateSummary {
|
|
606
|
+
/** Total inventory entries surveyed across all projects + global tier. */
|
|
607
|
+
totalDbs: number;
|
|
608
|
+
/** Entries where `exists && integrityOK === true`. */
|
|
609
|
+
healthy: number;
|
|
610
|
+
/** Entries where `!exists`. */
|
|
611
|
+
missing: number;
|
|
612
|
+
/** Entries where `exists && integrityOK === false`. */
|
|
613
|
+
corrupt: number;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Top-level result payload returned by `surveyDbSubstrate`.
|
|
618
|
+
*
|
|
619
|
+
* @remarks
|
|
620
|
+
* `scope === 'project'` means the survey covered exactly one project plus
|
|
621
|
+
* the global tier. `scope === 'fleet'` means the survey walked every
|
|
622
|
+
* `.cleo/` directory discoverable from a fleet-root search path.
|
|
623
|
+
*
|
|
624
|
+
* Carried as the `data` field of the LAFS envelope; the corresponding
|
|
625
|
+
* `meta` field carries `meta.warnings` per `DbSubstrateWarning` and the
|
|
626
|
+
* canonical `operation: 'doctor.db-substrate.run'` identifier.
|
|
627
|
+
*
|
|
628
|
+
* @task T10307
|
|
629
|
+
*/
|
|
630
|
+
export interface DbSubstrateAuditResult {
|
|
631
|
+
/** Survey scope. `'project'` = current project; `'fleet'` = many projects. */
|
|
632
|
+
scope: 'project' | 'fleet';
|
|
633
|
+
/** Per-project surveys. One entry for `scope='project'`; ≥1 for `scope='fleet'`. */
|
|
634
|
+
projects: DbSubstrateProjectSurvey[];
|
|
635
|
+
/** Aggregate counters across all surveyed entries. */
|
|
636
|
+
summary: DbSubstrateSummary;
|
|
637
|
+
/**
|
|
638
|
+
* Structural warnings the survey detected outside of any inventory
|
|
639
|
+
* entry — orphan project-root `.cleo/` directories, nested-nexus
|
|
640
|
+
* duplicates, etc. ALWAYS present (may be empty).
|
|
641
|
+
*/
|
|
642
|
+
warnings: DbSubstrateWarning[];
|
|
643
|
+
/**
|
|
644
|
+
* Cross-DB orphan-row reports — one entry per invariant the survey
|
|
645
|
+
* was able to check. ALWAYS present (may be empty when no DBs exist,
|
|
646
|
+
* or when every invariant returned zero orphans).
|
|
647
|
+
*
|
|
648
|
+
* Surfaced to the envelope's `meta.crossDbOrphans` array by the
|
|
649
|
+
* `cleo doctor db-substrate` command via `pushWarning`. Per-invariant
|
|
650
|
+
* sample row IDs are capped at the first 5 to keep envelopes small.
|
|
651
|
+
*
|
|
652
|
+
* @task T10323
|
|
653
|
+
*/
|
|
654
|
+
crossDbOrphans: DbCrossDbOrphanReport[];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Canonical identifiers for the cross-DB referential invariants surfaced by
|
|
659
|
+
* `cleo doctor db-substrate` (T10323). The catalogue is owned by the T10320
|
|
660
|
+
* ADR draft — every ID here MUST round-trip with the ADR enumeration.
|
|
661
|
+
*
|
|
662
|
+
* | ID | Source | Target |
|
|
663
|
+
* |----|---------------------------------------|---------------------------------------------|
|
|
664
|
+
* | I1 | `brain_memory_links.task_id` | `tasks.id` (tasks.db) |
|
|
665
|
+
* | I2 | `manifest.db blob_attachments.doc_slug` | `tasks.id` (tasks.db) |
|
|
666
|
+
* | I3 | `nexus.db project_registry.project_id`| `.cleo/project-context.json` (or computed) |
|
|
667
|
+
* | I4 | `llmtxt.db documents.doc_slug` (or any `session_id` column) | `tasks.id` / `sessions.id` |
|
|
668
|
+
* | I5 | `conduit.db dead_letters.job_id` | `tasks.id` / `brain_*.id` |
|
|
669
|
+
*
|
|
670
|
+
* Invariants I1 / I2 / I3 / I5 always run when both source + target DBs
|
|
671
|
+
* exist; I4 runs only when the source schema actually declares a candidate
|
|
672
|
+
* column (the live llmtxt schema has not yet introduced `session_id` — the
|
|
673
|
+
* check stays schema-aware to avoid false positives).
|
|
674
|
+
*
|
|
675
|
+
* @task T10323
|
|
676
|
+
* @epic T10285
|
|
677
|
+
* @saga T10281
|
|
678
|
+
*/
|
|
679
|
+
export type DbCrossDbInvariantId = 'I1' | 'I2' | 'I3' | 'I4' | 'I5';
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* One cross-DB orphan-row finding produced by `walkCrossDbInvariants`.
|
|
683
|
+
*
|
|
684
|
+
* @remarks
|
|
685
|
+
* Every reported invariant carries a bounded sample (first 5 orphan keys)
|
|
686
|
+
* so operators can immediately see WHICH rows triggered the finding. The
|
|
687
|
+
* underlying SQL queries are capped at LIMIT 100 — a sample of 5 is
|
|
688
|
+
* sufficient for triage, and the full count is reported via `orphanCount`.
|
|
689
|
+
*
|
|
690
|
+
* `suggestedFix` is the canonical repair command (or hint) for the
|
|
691
|
+
* invariant class. When the invariant has no programmatic fix it carries
|
|
692
|
+
* a human-readable instruction.
|
|
693
|
+
*
|
|
694
|
+
* `skipped` indicates the invariant could not be checked (e.g. the source
|
|
695
|
+
* DB is missing, the column has not yet been added to the schema, or the
|
|
696
|
+
* snapshot opener threw). When `skipped === true`, `orphanCount === 0`
|
|
697
|
+
* and `sample === []`.
|
|
698
|
+
*
|
|
699
|
+
* @task T10323
|
|
700
|
+
*/
|
|
701
|
+
export interface DbCrossDbOrphanReport {
|
|
702
|
+
/** Canonical invariant identifier — see {@link DbCrossDbInvariantId}. */
|
|
703
|
+
invariant: DbCrossDbInvariantId;
|
|
704
|
+
/**
|
|
705
|
+
* Human-readable description of which (source → target) reference class
|
|
706
|
+
* this invariant tracks. Stable text — operators should be able to grep
|
|
707
|
+
* this in audit logs.
|
|
708
|
+
*/
|
|
709
|
+
description: string;
|
|
710
|
+
/**
|
|
711
|
+
* Total number of orphan rows detected by the bounded query. When the
|
|
712
|
+
* query hit its `LIMIT 100` cap, this value is the cap; the actual
|
|
713
|
+
* orphan population may be larger.
|
|
714
|
+
*/
|
|
715
|
+
orphanCount: number;
|
|
716
|
+
/**
|
|
717
|
+
* First N (≤ 5) orphan identifiers — usually the foreign-key column
|
|
718
|
+
* value. Order matches the source DB's natural read order (no ORDER BY).
|
|
719
|
+
* `[]` when `orphanCount === 0` OR `skipped === true`.
|
|
720
|
+
*/
|
|
721
|
+
sample: string[];
|
|
722
|
+
/**
|
|
723
|
+
* Canonical repair command or hint. Stable text — wired into release
|
|
724
|
+
* notes and ADR-068 evidence atoms.
|
|
725
|
+
*/
|
|
726
|
+
suggestedFix: string;
|
|
727
|
+
/**
|
|
728
|
+
* `true` when the invariant could not be checked (missing DB, missing
|
|
729
|
+
* column, opener threw). `orphanCount === 0` and `sample === []` when
|
|
730
|
+
* skipped. Carries the skip reason for triage.
|
|
731
|
+
*/
|
|
732
|
+
skipped: boolean;
|
|
733
|
+
/**
|
|
734
|
+
* Free-form reason populated when `skipped === true`. Empty string
|
|
735
|
+
* otherwise. Stable text — operators rely on this for diagnostics.
|
|
736
|
+
*/
|
|
737
|
+
skipReason: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Caller-supplied tuning knobs for a substrate survey.
|
|
742
|
+
*
|
|
743
|
+
* @remarks
|
|
744
|
+
* - `integrityCheckTimeoutMs` bounds the wall-clock duration the survey
|
|
745
|
+
* will wait for `PRAGMA integrity_check` on a single DB. Because
|
|
746
|
+
* `node:sqlite` is fully synchronous, the check cannot be aborted
|
|
747
|
+
* mid-flight — the timer is consulted AFTER the call returns, and a
|
|
748
|
+
* DB that exceeded the budget is flagged via {@link DbSubstrateEntry.timedOut}.
|
|
749
|
+
* - `autoQuarantine` (default `true`) controls whether
|
|
750
|
+
* `inspectDbFile` moves a corrupt DB and its `-wal`/`-shm` sidecars
|
|
751
|
+
* into `<projectRoot>/.cleo/quarantine/<role>-malformed-<iso>/`. Set
|
|
752
|
+
* to `false` for read-only diagnostic surveys.
|
|
753
|
+
*
|
|
754
|
+
* @task T10312
|
|
755
|
+
* @epic T10283
|
|
756
|
+
* @saga T10281
|
|
757
|
+
*/
|
|
758
|
+
export interface DbSubstrateSurveyOptions {
|
|
759
|
+
/**
|
|
760
|
+
* Wall-clock budget for the `PRAGMA integrity_check` call on each
|
|
761
|
+
* DB, in milliseconds. Default: 60000 (60 s).
|
|
762
|
+
*
|
|
763
|
+
* Pass `0` to disable the timeout (the survey waits indefinitely).
|
|
764
|
+
*/
|
|
765
|
+
integrityCheckTimeoutMs?: number;
|
|
766
|
+
/**
|
|
767
|
+
* `true` (default) → auto-quarantine corrupt DBs into
|
|
768
|
+
* `<projectRoot>/.cleo/quarantine/<role>-malformed-<iso>/`.
|
|
769
|
+
* `false` → leave the corrupt DB in place; only report it.
|
|
770
|
+
*/
|
|
771
|
+
autoQuarantine?: boolean;
|
|
772
|
+
}
|
|
773
|
+
|
|
289
774
|
/**
|
|
290
775
|
* One line appended to `.cleo/audit/worktree-prune.jsonl` per prune
|
|
291
776
|
* operation. Extends the existing audit-log schema (timestamp +
|
|
@@ -318,3 +803,152 @@ export interface PruneAuditEntry {
|
|
|
318
803
|
/** Whether this entry represents a dry-run plan (no actual removal). */
|
|
319
804
|
dryRun: boolean;
|
|
320
805
|
}
|
|
806
|
+
|
|
807
|
+
// ============================================================================
|
|
808
|
+
// Legacy-Backup Walker (T10309 — Saga T10281 SG-BRAIN-DB-RESILIENCE / Epic T10282)
|
|
809
|
+
// ============================================================================
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Origin hint inferred from a legacy backup file's name.
|
|
813
|
+
*
|
|
814
|
+
* Identifies which legacy migration produced the file. Used by the operator
|
|
815
|
+
* to decide whether the artefact is safe to delete. Derived purely from the
|
|
816
|
+
* filename (and parent-directory name) — no DB inspection is performed.
|
|
817
|
+
*
|
|
818
|
+
* Codes:
|
|
819
|
+
* - `pre-cleo-migration` — `*-pre-cleo.db.bak` files written by the
|
|
820
|
+
* pre-cleo→cleo migration when the SDK still used legacy file shapes.
|
|
821
|
+
* - `brain-dup-fix` — `brain.db.PRE-DUP-FIX-*` written by the BRAIN
|
|
822
|
+
* dedup repair (T7xxx series).
|
|
823
|
+
* - `pre-untrack` — `*.pre-untrack-*` written by T5158 when the four
|
|
824
|
+
* runtime files were `git rm --cached`-ed in 2026-04-07.
|
|
825
|
+
* - `quarantine-snapshot` — files captured under
|
|
826
|
+
* `<projectRoot>/.cleo/quarantine/` by historical quarantine sweeps
|
|
827
|
+
* (lafs-, studio-, adapters-, core-, runtime-, cleo-os- timestamped
|
|
828
|
+
* directories). NEVER auto-pruned (may be active forensic artefacts).
|
|
829
|
+
* - `brain-malformed` — files under `.cleo/quarantine/brain-malformed-*`
|
|
830
|
+
* (2026-05-23 P0 brain.db malformation incident). Treated as
|
|
831
|
+
* `quarantine-snapshot` for retention.
|
|
832
|
+
* - `db-backup-rotation` — files under `.cleo/backups/sqlite/` older
|
|
833
|
+
* than the 10-snapshot rotation cap (overflow candidates).
|
|
834
|
+
* - `unknown` — file matches the suffix pattern but no specific origin
|
|
835
|
+
* could be inferred from the path. Defaults to safe retention.
|
|
836
|
+
*
|
|
837
|
+
* @task T10309
|
|
838
|
+
*/
|
|
839
|
+
export type LegacyBackupOriginHint =
|
|
840
|
+
| 'pre-cleo-migration'
|
|
841
|
+
| 'brain-dup-fix'
|
|
842
|
+
| 'pre-untrack'
|
|
843
|
+
| 'quarantine-snapshot'
|
|
844
|
+
| 'brain-malformed'
|
|
845
|
+
| 'db-backup-rotation'
|
|
846
|
+
| 'unknown';
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Retention recommendation for one legacy backup file.
|
|
850
|
+
*
|
|
851
|
+
* `keep` — file is younger than the soft retention window (default 30
|
|
852
|
+
* days), OR the file lives under a path that is unconditionally
|
|
853
|
+
* preserved (quarantine artefacts).
|
|
854
|
+
*
|
|
855
|
+
* `compress` — file is 30-90 days old and large enough that gzip-ing it
|
|
856
|
+
* reduces disk pressure. The current implementation surfaces this
|
|
857
|
+
* recommendation but the `--prune` flag does NOT compress (it only
|
|
858
|
+
* deletes); a future T10311+ task will wire the compressor.
|
|
859
|
+
*
|
|
860
|
+
* `delete` — file is older than the hard retention window (default 90
|
|
861
|
+
* days) AND does NOT live under `.cleo/quarantine/`. The `--prune`
|
|
862
|
+
* verb removes these (or simulates removal in dry-run mode).
|
|
863
|
+
*
|
|
864
|
+
* @task T10309
|
|
865
|
+
*/
|
|
866
|
+
export type LegacyBackupRecommendation = 'keep' | 'compress' | 'delete';
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* One legacy backup artefact discovered by the walker.
|
|
870
|
+
*
|
|
871
|
+
* Carries enough provenance for the operator to act without re-reading
|
|
872
|
+
* the file from disk. `ageDays` is rounded DOWN to match the retention
|
|
873
|
+
* window semantics (a file exactly 30.5 days old is `30` days, still
|
|
874
|
+
* inside the keep window).
|
|
875
|
+
*
|
|
876
|
+
* @task T10309
|
|
877
|
+
*/
|
|
878
|
+
export interface LegacyBackupEntry {
|
|
879
|
+
/** Absolute path to the legacy backup file. */
|
|
880
|
+
path: string;
|
|
881
|
+
/** `fs.statSync(path).size`. */
|
|
882
|
+
sizeBytes: number;
|
|
883
|
+
/** `fs.statSync(path).mtimeMs`. */
|
|
884
|
+
mtimeMs: number;
|
|
885
|
+
/** Whole-day age of the file at scan time (`floor((now - mtimeMs) / 86_400_000)`). */
|
|
886
|
+
ageDays: number;
|
|
887
|
+
/** Inferred origin (which legacy migration created this artefact). */
|
|
888
|
+
originHint: LegacyBackupOriginHint;
|
|
889
|
+
/** Retention verdict for this file. */
|
|
890
|
+
recommendation: LegacyBackupRecommendation;
|
|
891
|
+
/**
|
|
892
|
+
* Free-form reason explaining the recommendation. Stable enough to
|
|
893
|
+
* surface to operators; do NOT machine-parse — switch on
|
|
894
|
+
* `originHint` + `recommendation` instead.
|
|
895
|
+
*/
|
|
896
|
+
reason: string;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Result returned by the legacy-backup walker (T10309).
|
|
901
|
+
*
|
|
902
|
+
* Lists every discovered artefact plus aggregate counters. When
|
|
903
|
+
* `--prune` was requested the `pruned` and `kept` arrays partition the
|
|
904
|
+
* total set; otherwise both arrays are empty and `entries` is the only
|
|
905
|
+
* populated list.
|
|
906
|
+
*
|
|
907
|
+
* @remarks
|
|
908
|
+
* `softRetentionDays` and `hardRetentionDays` echo the configured
|
|
909
|
+
* retention thresholds back to the operator so the envelope is
|
|
910
|
+
* self-documenting.
|
|
911
|
+
*
|
|
912
|
+
* @task T10309
|
|
913
|
+
*/
|
|
914
|
+
export interface LegacyBackupScanResult {
|
|
915
|
+
/** Absolute path to the project root that was scanned. */
|
|
916
|
+
projectRoot: string;
|
|
917
|
+
/** Absolute path to the CLEO home (`<projectRoot>/../.local/share/cleo` style) that was scanned. */
|
|
918
|
+
cleoHome: string;
|
|
919
|
+
/** All artefacts discovered, sorted by `path` ascending. */
|
|
920
|
+
entries: LegacyBackupEntry[];
|
|
921
|
+
/** Total bytes across `entries`. */
|
|
922
|
+
totalBytes: number;
|
|
923
|
+
/** Soft retention window in days (files younger than this are always `keep`). */
|
|
924
|
+
softRetentionDays: number;
|
|
925
|
+
/** Hard retention window in days (files older than this are `delete`). */
|
|
926
|
+
hardRetentionDays: number;
|
|
927
|
+
/**
|
|
928
|
+
* `true` when this result represents a `--prune` invocation; `false`
|
|
929
|
+
* when it is a pure scan. Drives whether `pruned`/`kept` are
|
|
930
|
+
* populated.
|
|
931
|
+
*/
|
|
932
|
+
prune: boolean;
|
|
933
|
+
/**
|
|
934
|
+
* `true` when `--prune` was requested but `--dry-run` was active
|
|
935
|
+
* (the implementation defaults `--prune` to dry-run). `false` for
|
|
936
|
+
* actual deletions or when `prune === false`.
|
|
937
|
+
*/
|
|
938
|
+
dryRun: boolean;
|
|
939
|
+
/**
|
|
940
|
+
* Entries that were (or would be) deleted. Always empty when
|
|
941
|
+
* `prune === false`.
|
|
942
|
+
*/
|
|
943
|
+
pruned: LegacyBackupEntry[];
|
|
944
|
+
/**
|
|
945
|
+
* Entries the prune operation skipped (recommendation !== 'delete'
|
|
946
|
+
* or path under quarantine). Always empty when `prune === false`.
|
|
947
|
+
*/
|
|
948
|
+
kept: LegacyBackupEntry[];
|
|
949
|
+
/**
|
|
950
|
+
* Errors encountered while removing pruneable files. Empty in
|
|
951
|
+
* dry-run mode and on pure scans.
|
|
952
|
+
*/
|
|
953
|
+
errors: Array<{ path: string; error: string }>;
|
|
954
|
+
}
|