@dzhechkov/harness-core 0.8.29 → 0.8.31

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.
Files changed (138) hide show
  1. package/.dz-manifest.json +242 -102
  2. package/README.md +271 -0
  3. package/dist/agentdb-index.d.ts +45 -0
  4. package/dist/agentdb-index.d.ts.map +1 -1
  5. package/dist/agentdb-index.js +345 -85
  6. package/dist/agentdb-index.js.map +1 -1
  7. package/dist/agentdb-reindex-marker.d.ts +170 -0
  8. package/dist/agentdb-reindex-marker.d.ts.map +1 -0
  9. package/dist/agentdb-reindex-marker.js +293 -0
  10. package/dist/agentdb-reindex-marker.js.map +1 -0
  11. package/dist/agentdb-snapshot-rotation.d.ts +169 -0
  12. package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
  13. package/dist/agentdb-snapshot-rotation.js +322 -0
  14. package/dist/agentdb-snapshot-rotation.js.map +1 -0
  15. package/dist/agentdb-snapshot.d.ts +126 -0
  16. package/dist/agentdb-snapshot.d.ts.map +1 -0
  17. package/dist/agentdb-snapshot.js +195 -0
  18. package/dist/agentdb-snapshot.js.map +1 -0
  19. package/dist/apply-leg.d.ts +202 -0
  20. package/dist/apply-leg.d.ts.map +1 -0
  21. package/dist/apply-leg.js +975 -0
  22. package/dist/apply-leg.js.map +1 -0
  23. package/dist/book-kb.d.ts.map +1 -1
  24. package/dist/book-kb.js +57 -1
  25. package/dist/book-kb.js.map +1 -1
  26. package/dist/brain.d.ts +3 -0
  27. package/dist/brain.d.ts.map +1 -1
  28. package/dist/brain.js +1 -1
  29. package/dist/brain.js.map +1 -1
  30. package/dist/cadence.d.ts +30 -1
  31. package/dist/cadence.d.ts.map +1 -1
  32. package/dist/cadence.js +66 -2
  33. package/dist/cadence.js.map +1 -1
  34. package/dist/guard.d.ts +14 -0
  35. package/dist/guard.d.ts.map +1 -1
  36. package/dist/guard.js +36 -0
  37. package/dist/guard.js.map +1 -1
  38. package/dist/index.d.ts +23 -8
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +18 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/integration-probe-worker.d.ts.map +1 -1
  43. package/dist/integration-probe-worker.js +4 -6
  44. package/dist/integration-probe-worker.js.map +1 -1
  45. package/dist/integrations-verify.d.ts.map +1 -1
  46. package/dist/integrations-verify.js +3 -1
  47. package/dist/integrations-verify.js.map +1 -1
  48. package/dist/managed-hooks.d.ts +11 -2
  49. package/dist/managed-hooks.d.ts.map +1 -1
  50. package/dist/managed-hooks.js +33 -7
  51. package/dist/managed-hooks.js.map +1 -1
  52. package/dist/mutation-gate.d.ts +35 -1
  53. package/dist/mutation-gate.d.ts.map +1 -1
  54. package/dist/mutation-gate.js +49 -9
  55. package/dist/mutation-gate.js.map +1 -1
  56. package/dist/operations.d.ts.map +1 -1
  57. package/dist/operations.js +94 -0
  58. package/dist/operations.js.map +1 -1
  59. package/dist/patterns.d.ts.map +1 -1
  60. package/dist/patterns.js +23 -4
  61. package/dist/patterns.js.map +1 -1
  62. package/dist/publish.d.ts +21 -0
  63. package/dist/publish.d.ts.map +1 -1
  64. package/dist/publish.js.map +1 -1
  65. package/dist/repository-origin.d.ts +1 -1
  66. package/dist/repository-origin.d.ts.map +1 -1
  67. package/dist/repository-origin.js +7 -1
  68. package/dist/repository-origin.js.map +1 -1
  69. package/dist/round-exec.d.ts +41 -0
  70. package/dist/round-exec.d.ts.map +1 -0
  71. package/dist/round-exec.js +45 -0
  72. package/dist/round-exec.js.map +1 -0
  73. package/dist/round.d.ts +109 -0
  74. package/dist/round.d.ts.map +1 -0
  75. package/dist/round.js +148 -0
  76. package/dist/round.js.map +1 -0
  77. package/dist/run-records.d.ts +8 -0
  78. package/dist/run-records.d.ts.map +1 -1
  79. package/dist/run-records.js +69 -3
  80. package/dist/run-records.js.map +1 -1
  81. package/dist/setup.d.ts +8 -0
  82. package/dist/setup.d.ts.map +1 -1
  83. package/dist/setup.js +166 -3
  84. package/dist/setup.js.map +1 -1
  85. package/dist/sqlite-read-helpers.d.ts +60 -0
  86. package/dist/sqlite-read-helpers.d.ts.map +1 -0
  87. package/dist/sqlite-read-helpers.js +118 -0
  88. package/dist/sqlite-read-helpers.js.map +1 -0
  89. package/dist/statusline.d.ts +22 -3
  90. package/dist/statusline.d.ts.map +1 -1
  91. package/dist/statusline.js +43 -7
  92. package/dist/statusline.js.map +1 -1
  93. package/dist/store-counts.d.ts +41 -3
  94. package/dist/store-counts.d.ts.map +1 -1
  95. package/dist/store-counts.js +305 -59
  96. package/dist/store-counts.js.map +1 -1
  97. package/dist/store-guard.d.ts +2 -2
  98. package/dist/store-guard.d.ts.map +1 -1
  99. package/dist/store-guard.js +6 -0
  100. package/dist/store-guard.js.map +1 -1
  101. package/dist/usage.d.ts +55 -0
  102. package/dist/usage.d.ts.map +1 -1
  103. package/dist/usage.js +184 -33
  104. package/dist/usage.js.map +1 -1
  105. package/dist/vector-tier.d.ts +18 -1
  106. package/dist/vector-tier.d.ts.map +1 -1
  107. package/dist/vector-tier.js +26 -2
  108. package/dist/vector-tier.js.map +1 -1
  109. package/package.json +7 -6
  110. package/sbom.json +451 -101
  111. package/src/agentdb-index.ts +315 -32
  112. package/src/agentdb-reindex-marker.ts +330 -0
  113. package/src/agentdb-snapshot-rotation.ts +393 -0
  114. package/src/agentdb-snapshot.ts +222 -0
  115. package/src/apply-leg.ts +1046 -0
  116. package/src/book-kb.ts +64 -1
  117. package/src/brain.ts +11 -2
  118. package/src/cadence.ts +87 -2
  119. package/src/guard.ts +46 -0
  120. package/src/index.ts +58 -5
  121. package/src/integration-probe-worker.ts +4 -6
  122. package/src/integrations-verify.ts +3 -1
  123. package/src/managed-hooks.ts +32 -7
  124. package/src/mutation-gate.ts +87 -8
  125. package/src/operations.ts +89 -0
  126. package/src/patterns.ts +23 -4
  127. package/src/publish.ts +23 -0
  128. package/src/repository-origin.ts +7 -1
  129. package/src/round-exec.ts +85 -0
  130. package/src/round.ts +228 -0
  131. package/src/run-records.ts +77 -2
  132. package/src/setup.ts +198 -3
  133. package/src/sqlite-read-helpers.ts +128 -0
  134. package/src/statusline.ts +61 -9
  135. package/src/store-counts.ts +391 -61
  136. package/src/store-guard.ts +8 -2
  137. package/src/usage.ts +243 -29
  138. package/src/vector-tier.ts +45 -4
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Small, shared helpers for the READ-ONLY sqlite paths (`book-kb.ts`, `agentdb-index.ts`,
3
+ * `patterns.ts`) — feature `readonly-residuals`.
4
+ *
5
+ * Two residuals fixed here (`features/readonly-residuals/01_requirements.md`):
6
+ *
7
+ * FR-1. `applyReadonlyPragmas` closes a handle that was already opened by
8
+ * `openSqliteReadOnly` (`@dzhechkov/memory`) if the follow-up `busy_timeout` pragma throws.
9
+ * Before this helper existed, `book-kb.ts`/`agentdb-index.ts` called `db.pragma(...)`
10
+ * directly on the handle's `db`, outside any `try` — a pragma failure left BOTH the native
11
+ * connection and a tmp-copy directory (ADR-001 ladder rung 2) leaked, because `handle.cleanup()`
12
+ * is only wired into the `close()` this code never reaches.
13
+ *
14
+ * FR-2. `classifySqliteReadFailure` tells apart "the native module isn't installed here"
15
+ * (silent JSON fallback, unchanged behaviour) from "the store file itself is unreadable"
16
+ * (corrupt file, permission failure, …) — the second case used to be swallowed by the same
17
+ * bare `catch {}` in `patterns.ts`, so a corrupted `patterns.sqlite` silently looked like
18
+ * "fewer lessons" instead of "the store broke". `warnOnce` prints that second case exactly
19
+ * once per process, on stderr, without changing the JSON-fallback data path.
20
+ */
21
+
22
+ /** Minimal shape `applyReadonlyPragmas` needs from an `openSqliteReadOnly` handle. */
23
+ export interface PragmaTarget {
24
+ readonly db: { pragma: (sql: string) => void; close: () => void };
25
+ readonly cleanup: () => void;
26
+ }
27
+
28
+ function closeQuietly(db: { close: () => void }): void {
29
+ try {
30
+ db.close();
31
+ } catch {
32
+ // already failing on the caller's side — a close failure here must not mask the real cause
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Run `fn()` and swallow any exception it throws — used for `handle.cleanup()` in the
38
+ * pragma-failure path (fix round 1, MEDIUM #2): a throwing `cleanup()` must not replace the
39
+ * original pragma error the caller is already in the middle of rethrowing.
40
+ */
41
+ function cleanupQuietly(fn: () => void): void {
42
+ try {
43
+ fn();
44
+ } catch {
45
+ // the pragma failure is the error of record — a cleanup failure here must not mask it
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Run `db.pragma('busy_timeout = 5000')` on an already-opened read-only handle. If the pragma
51
+ * throws, the connection is closed and `handle.cleanup()` is called (removing a tmp-copy
52
+ * directory, ADR-001 rung 2) BEFORE rethrowing — so a pragma failure never leaks either the
53
+ * native connection or the temporary copy. Both `close()` and `cleanup()` are individually
54
+ * guarded (`closeQuietly` / `cleanupQuietly`, fix round 1, MEDIUM #2) so that EITHER one throwing
55
+ * still lets the original pragma cause reach the caller with the db path attached, and both are
56
+ * still attempted exactly once each regardless of which one (if any) throws.
57
+ */
58
+ export function applyReadonlyPragmas(handle: PragmaTarget, path: string): void {
59
+ try {
60
+ handle.db.pragma('busy_timeout = 5000'); // harmless on a readonly connection — kept for parity with the writer opener
61
+ } catch (err) {
62
+ closeQuietly(handle.db);
63
+ cleanupQuietly(() => handle.cleanup());
64
+ const cause = err instanceof Error ? err.message : String(err);
65
+ throw new Error(`failed to prepare ${path} for reading: ${cause}`);
66
+ }
67
+ }
68
+
69
+ /** Which of the two known catch-all reasons a store-opener failure names. */
70
+ export type SqliteReadFailureKind = 'native-unavailable' | 'store-unreadable';
71
+
72
+ /**
73
+ * Loader-specific signatures of "the `better-sqlite3` native module itself could not be loaded"
74
+ * (fix round 1, MEDIUM #1 — narrows the classifier after Codex review found the previous bare
75
+ * `/better-sqlite3/.test(msg)` swallowed `better-sqlite3: file is not a database`, a genuine
76
+ * corruption, as if the module were merely absent). Each pattern names a load-time failure mode,
77
+ * never a runtime/data failure that merely happens to mention the package name.
78
+ */
79
+ const NATIVE_LOADER_PATTERNS: readonly RegExp[] = [
80
+ /Cannot find module ['"]?better-sqlite3/,
81
+ /better-sqlite3 is unavailable/,
82
+ /Could not locate the bindings file/,
83
+ /was compiled against a different Node\.js version/,
84
+ /invalid ELF header/,
85
+ /NODE_MODULE_VERSION/,
86
+ ];
87
+
88
+ /**
89
+ * Classify an error caught around `SqliteBackend.openReadOnly` (or an equivalent opener):
90
+ * `'native-unavailable'` only when the failure is genuinely about `better-sqlite3` failing to
91
+ * LOAD — either `code === 'MODULE_NOT_FOUND'` with `better-sqlite3` named in the message or
92
+ * `requireStack` (a bare `MODULE_NOT_FOUND` for an unrelated module is NOT this — it is a
93
+ * different bug and must not be hidden as "native unavailable"), or the message matches one of
94
+ * the {@link NATIVE_LOADER_PATTERNS} native-module-loader signatures (missing module, ABI/version
95
+ * mismatch, corrupt/foreign binary). The existing silent JSON fallback is correct here, nothing
96
+ * changes. Anything else — a corrupt file (`file is not a database`), a permission failure, a
97
+ * genuinely unreadable store — is `'store-unreadable'`: still falls back to JSON (FR-2 does not
98
+ * change returned data), but the caller is expected to warn once via {@link warnOnce}.
99
+ */
100
+ export function classifySqliteReadFailure(err: unknown): SqliteReadFailureKind {
101
+ const code = (err as { code?: string } | undefined)?.code;
102
+ const msg = err instanceof Error ? err.message : String(err);
103
+ const requireStackRaw = (err as { requireStack?: unknown } | undefined)?.requireStack;
104
+ const requireStack = Array.isArray(requireStackRaw) ? requireStackRaw.join('\n') : '';
105
+
106
+ if (code === 'MODULE_NOT_FOUND' && (/better-sqlite3/.test(msg) || /better-sqlite3/.test(requireStack))) {
107
+ return 'native-unavailable';
108
+ }
109
+ if (NATIVE_LOADER_PATTERNS.some((re) => re.test(msg))) return 'native-unavailable';
110
+ return 'store-unreadable';
111
+ }
112
+
113
+ const warnedKeys = new Set<string>();
114
+
115
+ /**
116
+ * Write `text` to stderr the first time this exact `key` is seen in this process, and never
117
+ * again. Used to surface a `'store-unreadable'` classification exactly once per store path per
118
+ * process, instead of once per read call.
119
+ */
120
+ export function warnOnce(key: string, text: string): void {
121
+ if (warnedKeys.has(key)) return;
122
+ warnedKeys.add(key);
123
+ try {
124
+ process.stderr.write(`${text}\n`);
125
+ } catch {
126
+ // a closed/broken stderr must not break the caller's fallback
127
+ }
128
+ }
package/src/statusline.ts CHANGED
@@ -70,15 +70,30 @@ export interface StatuslineData {
70
70
  readonly patterns: number;
71
71
  /** Absent on parity; missing/unreadable mirror is explicitly unavailable. */
72
72
  readonly patternMirror?:
73
- | { readonly state: 'different'; readonly lexical: number; readonly vector: number }
73
+ | {
74
+ readonly state: 'in-sync' | 'different';
75
+ readonly lexicalMirrorable: number;
76
+ readonly vector: number;
77
+ readonly excluded: { readonly class: number; readonly noise: number };
78
+ }
74
79
  | { readonly state: 'unavailable' };
80
+ /** Vector-mirror inventory; absence is explicit instead of being encoded as a missing field. */
81
+ readonly mirror: {
82
+ readonly available: boolean;
83
+ readonly rows: number;
84
+ readonly lessons: number;
85
+ readonly pending: number;
86
+ readonly source: 'agentdb';
87
+ };
75
88
  /** Exact lexical-tier availability split; omitted when the enhanced readonly count cannot be established. */
76
89
  readonly patternBreakdown?: {
77
- readonly source: 'lexical';
90
+ readonly source: 'lexical' | 'lexical+mirror';
78
91
  readonly active: number;
79
92
  readonly quarantined: number;
80
93
  /** True once quarantine contains at least one third of the lexical pool. */
81
94
  readonly attention: boolean;
95
+ /** Exact label drift by dzId; present only when both tiers expose readable identities. */
96
+ readonly tierParity?: { readonly lexicalOnly: number; readonly mirrorOnly: number };
82
97
  /** Absolute lexical/vector quarantine-label delta, present only above the tolerated drift threshold. */
83
98
  readonly tierDelta?: number;
84
99
  };
@@ -103,6 +118,8 @@ export interface StatuslineData {
103
118
  /** Render-ready details for a non-healthy store verdict. */
104
119
  export interface StatuslineStoreHealth {
105
120
  readonly verdict: Exclude<StoreHealthVerdict, 'ok' | 'no-mark'>;
121
+ /** Explanation for a health verdict that cannot be inferred from counts alone. */
122
+ readonly reason?: string;
106
123
  /** Previous maximum for the affected store tier. */
107
124
  readonly previousMax?: number;
108
125
  /** Snapshot directory used by cold-start recovery guidance. */
@@ -643,6 +660,8 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
643
660
  ? [basename(rows.vectorSourcePath)] : []),
644
661
  ],
645
662
  };
663
+ } else if (health.verdict === 'busy') {
664
+ storeHealth = { verdict: health.verdict, reason: health.reason };
646
665
  } else if (health.verdict === 'source-changed') {
647
666
  storeHealth = { verdict: health.verdict };
648
667
  }
@@ -659,14 +678,38 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
659
678
  // зеркалом не пользуется, он горел бы всегда, а вечно горящий показатель не несёт сведений.
660
679
  // Зеркало ЕСТЬ, но прочитать или разложить его не удалось: это отказ инструмента, и он горит.
661
680
  const mirrorLessons = storeRows?.vectorLessonRows;
681
+ const mirrorRows = storeRows?.vectorRows;
662
682
  const mirrorAbsent = storeRows !== undefined && storeRows.vectorSourcePath === undefined;
683
+ const lexicalMirrorable = storeRows?.lexicalMirrorableRows;
684
+ const excluded = {
685
+ class: storeRows?.lexicalExcludedClassRows ?? 0,
686
+ noise: storeRows?.lexicalExcludedNoiseRows ?? 0,
687
+ };
688
+ const mirrorAvailable = storeRows?.vectorSourcePath !== undefined
689
+ && typeof mirrorRows === 'number'
690
+ && mirrorLessons !== undefined;
663
691
  const patternMirror: StatuslineData['patternMirror'] = mirrorAbsent
664
692
  ? undefined
665
- : storeRows === undefined || storeRows.vectorRows === 'unreadable' || mirrorLessons === undefined
693
+ : storeRows === undefined || storeRows.vectorRows === 'unreadable' || storeRows.vectorRows === 'busy'
694
+ || mirrorLessons === undefined || lexicalMirrorable === undefined
666
695
  ? { state: 'unavailable' }
667
- : mirrorLessons !== patterns
668
- ? { state: 'different', lexical: patterns, vector: mirrorLessons }
669
- : undefined;
696
+ : {
697
+ state: mirrorLessons !== lexicalMirrorable ? 'different' : 'in-sync',
698
+ lexicalMirrorable,
699
+ vector: mirrorLessons,
700
+ excluded,
701
+ };
702
+ const mirror: StatuslineData['mirror'] = {
703
+ available: mirrorAvailable,
704
+ rows: typeof mirrorRows === 'number' ? mirrorRows : 0,
705
+ lessons: mirrorAvailable ? mirrorLessons : 0,
706
+ pending: mirrorAbsent && lexicalMirrorable !== undefined
707
+ ? lexicalMirrorable
708
+ : mirrorAvailable && lexicalMirrorable !== undefined
709
+ ? Math.max(0, lexicalMirrorable - mirrorLessons)
710
+ : 0,
711
+ source: 'agentdb',
712
+ };
670
713
 
671
714
  let patternBreakdown: StatuslineData['patternBreakdown'];
672
715
  try {
@@ -674,14 +717,22 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
674
717
  && typeof storeRows.lexicalRows === 'number'
675
718
  && typeof storeRows.lexicalQuarantinedRows === 'number') {
676
719
  const quarantined = storeRows.lexicalQuarantinedRows;
677
- const tierDelta = typeof storeRows.vectorQuarantinedRows === 'number'
678
- ? Math.abs(quarantined - storeRows.vectorQuarantinedRows)
720
+ const tierDelta = mirrorAvailable
721
+ && typeof storeRows.lexicalMirrorableQuarantinedRows === 'number'
722
+ && typeof storeRows.vectorQuarantinedRows === 'number'
723
+ ? Math.abs(storeRows.lexicalMirrorableQuarantinedRows - storeRows.vectorQuarantinedRows)
679
724
  : undefined;
680
725
  patternBreakdown = {
681
- source: 'lexical',
726
+ source: mirrorAvailable ? 'lexical+mirror' : 'lexical',
682
727
  active: storeRows.lexicalRows - quarantined,
683
728
  quarantined,
684
729
  attention: quarantined > 0 && quarantined * 3 >= storeRows.lexicalRows,
730
+ ...(storeRows.quarantineTierParity === undefined ? {} : {
731
+ tierParity: {
732
+ lexicalOnly: storeRows.quarantineTierParity.lexicalOnly,
733
+ mirrorOnly: storeRows.quarantineTierParity.mirrorOnly,
734
+ },
735
+ }),
685
736
  ...(tierDelta !== undefined && tierDelta > QUARANTINE_TIER_DRIFT_TOLERANCE ? { tierDelta } : {}),
686
737
  };
687
738
  }
@@ -719,6 +770,7 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
719
770
 
720
771
  return {
721
772
  patterns,
773
+ mirror,
722
774
  ...(patternMirror !== undefined ? { patternMirror } : {}),
723
775
  ...(patternBreakdown !== undefined ? { patternBreakdown } : {}),
724
776
  ...(usedPatterns !== undefined ? { usedPatterns } : {}),