@dzhechkov/harness-core 0.8.30 → 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 (129) hide show
  1. package/.dz-manifest.json +233 -93
  2. package/README.md +269 -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/repository-origin.d.ts +1 -1
  63. package/dist/repository-origin.d.ts.map +1 -1
  64. package/dist/repository-origin.js +7 -1
  65. package/dist/repository-origin.js.map +1 -1
  66. package/dist/round-exec.d.ts +41 -0
  67. package/dist/round-exec.d.ts.map +1 -0
  68. package/dist/round-exec.js +45 -0
  69. package/dist/round-exec.js.map +1 -0
  70. package/dist/round.d.ts +109 -0
  71. package/dist/round.d.ts.map +1 -0
  72. package/dist/round.js +148 -0
  73. package/dist/round.js.map +1 -0
  74. package/dist/run-records.d.ts +8 -0
  75. package/dist/run-records.d.ts.map +1 -1
  76. package/dist/run-records.js +69 -3
  77. package/dist/run-records.js.map +1 -1
  78. package/dist/setup.d.ts +8 -0
  79. package/dist/setup.d.ts.map +1 -1
  80. package/dist/setup.js +166 -3
  81. package/dist/setup.js.map +1 -1
  82. package/dist/sqlite-read-helpers.d.ts +60 -0
  83. package/dist/sqlite-read-helpers.d.ts.map +1 -0
  84. package/dist/sqlite-read-helpers.js +118 -0
  85. package/dist/sqlite-read-helpers.js.map +1 -0
  86. package/dist/statusline.d.ts +20 -3
  87. package/dist/statusline.d.ts.map +1 -1
  88. package/dist/statusline.js +40 -7
  89. package/dist/statusline.js.map +1 -1
  90. package/dist/store-counts.d.ts +30 -0
  91. package/dist/store-counts.d.ts.map +1 -1
  92. package/dist/store-counts.js +189 -0
  93. package/dist/store-counts.js.map +1 -1
  94. package/dist/usage.d.ts +55 -0
  95. package/dist/usage.d.ts.map +1 -1
  96. package/dist/usage.js +184 -33
  97. package/dist/usage.js.map +1 -1
  98. package/dist/vector-tier.d.ts +18 -1
  99. package/dist/vector-tier.d.ts.map +1 -1
  100. package/dist/vector-tier.js +26 -2
  101. package/dist/vector-tier.js.map +1 -1
  102. package/package.json +6 -6
  103. package/sbom.json +442 -92
  104. package/src/agentdb-index.ts +315 -32
  105. package/src/agentdb-reindex-marker.ts +330 -0
  106. package/src/agentdb-snapshot-rotation.ts +393 -0
  107. package/src/agentdb-snapshot.ts +222 -0
  108. package/src/apply-leg.ts +1046 -0
  109. package/src/book-kb.ts +64 -1
  110. package/src/brain.ts +11 -2
  111. package/src/cadence.ts +87 -2
  112. package/src/guard.ts +46 -0
  113. package/src/index.ts +58 -5
  114. package/src/integration-probe-worker.ts +4 -6
  115. package/src/integrations-verify.ts +3 -1
  116. package/src/managed-hooks.ts +32 -7
  117. package/src/mutation-gate.ts +87 -8
  118. package/src/operations.ts +89 -0
  119. package/src/patterns.ts +23 -4
  120. package/src/repository-origin.ts +7 -1
  121. package/src/round-exec.ts +85 -0
  122. package/src/round.ts +228 -0
  123. package/src/run-records.ts +77 -2
  124. package/src/setup.ts +198 -3
  125. package/src/sqlite-read-helpers.ts +128 -0
  126. package/src/statusline.ts +57 -9
  127. package/src/store-counts.ts +245 -1
  128. package/src/usage.ts +243 -29
  129. package/src/vector-tier.ts +45 -4
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Consistent sqlite snapshot + restore for the pre-reindex undo point (feature
3
+ * `snapshot-wal-consistency`, FR-1..FR-4; fix round AM-1..AM-4).
4
+ *
5
+ * A WAL-mode sqlite database's committed data can live in TWO files: the main `dbFile` and its
6
+ * `-wal` sidecar (frames not yet checkpointed into the main file). The snapshot this module
7
+ * replaces was a bare `copyFileSync(dbFile, backupPath)` — one file, no `-wal` — so a reindex that
8
+ * ran while any writer held the WAL open (autocheckpoint disabled, or simply a live connection
9
+ * between commits) copied a "backup" that was MISSING committed rows. MEASURED (scratch repro,
10
+ * 2026-09-13, `node` + real `better-sqlite3`): a schema + one row checkpointed, then a second row
11
+ * inserted on a connection kept open (`wal_autocheckpoint = 0`) — `copyFileSync` alone yields a
12
+ * backup with 1 row; `VACUUM INTO` on a fresh read-only connection to the SAME live db yields 2.
13
+ * The live db's main file hash and `-wal` size are unchanged before/after the `VACUUM INTO` (the
14
+ * read-only connection never checkpoints or writes) — the safety property NFR-2 names.
15
+ *
16
+ * {@link snapshotSqliteDatabase} (FR-1/FR-2) takes the snapshot through `VACUUM INTO` on a fresh
17
+ * READ-ONLY connection — one self-contained output file with every committed transaction, WAL
18
+ * frames included. If `VACUUM INTO` cannot run (older sqlite, a locked/foreign file, no free disk)
19
+ * — or a caller forces it for a test — it falls back to copying `dbFile` plus its `-wal` sibling
20
+ * (present and non-empty) to `backupPath`/`backupPath-wal`; still strictly better than the old
21
+ * single-file copy, and the fallback is never silent: `method`/`note` name it honestly so a report
22
+ * downstream (`reindexAgentdbRows`'s `snapshotMethod`) never claims a guarantee it did not get.
23
+ *
24
+ * **AM-1 (fix round).** Codex review, Grade C: reusing a `backupPath` left a stale `-wal`/`-shm`
25
+ * sidecar from an EARLIER, unrelated snapshot family sitting next to the new one; `restoreSqliteSnapshot`
26
+ * used to decide whether to restore `-wal` by checking whether `backupPath-wal` merely EXISTS, so that
27
+ * stale sidecar could ride along onto a `vacuum-into` restore that never produced a matching `-wal` of
28
+ * its own. Fixed two ways: `snapshotSqliteDatabase` now clears any `backupPath-wal`/`-shm` sidecar
29
+ * BEFORE either strategy writes anything, and again right after a successful `VACUUM INTO` (whose
30
+ * output is one self-contained file and must never be shadowed); `restoreSqliteSnapshot` now takes the
31
+ * `method` the caller already knows and restores `-wal` ONLY when `method === 'copy+wal'` — never by
32
+ * re-deriving it from file presence.
33
+ *
34
+ * **AM-2 (fix round).** The old WAL-size probe was `existsSync` + best-effort `statSync` inside a
35
+ * `catch { walSize = 0 }` — ANY stat failure (`EACCES`, `EIO`, a raced deletion) was silently read as
36
+ * "no WAL", so a snapshot could report the honest-sounding `method: 'copy'` while actually having
37
+ * skipped a WAL it could not even check. {@link statSizeIfExists} now calls `statSync` directly and
38
+ * treats only a confirmed `ENOENT` as "does not exist" — every other error propagates and aborts the
39
+ * whole snapshot, so `reindexAgentdbRows` reports "snapshot failed", never a falsely-successful `copy`.
40
+ *
41
+ * **AM-4 (fix round).** `snapshotSqliteDatabase` used to happily write into (overwrite) an existing
42
+ * `backupPath`, silently discarding whatever it held — including, combined with AM-1's stale-sidecar
43
+ * bug, a backup that looked complete but carried a wrong-generation `-wal`. It now refuses up front
44
+ * (before `VACUUM INTO` or any copy) when `backupPath` already exists and is non-empty.
45
+ *
46
+ * {@link restoreSqliteSnapshot} (FR-4) is the paired rollback. MEASURED (same repro): copying the
47
+ * old main file back over `dbFile` WITHOUT removing a `-wal` left over from the aborted operation
48
+ * — reopening the "restored" db returned ZERO rows, not the restored one, because sqlite replayed
49
+ * the stale WAL frames (a since-superseded DELETE) on top of the reverted main file. Removing
50
+ * `dbFile-wal`/`dbFile-shm` as part of the restore fixed this: the reopened db then returned
51
+ * exactly the restored row. The caller MUST close its own write connection to `dbFile` before
52
+ * calling this — restoring a file sqlite still holds open is undefined behaviour.
53
+ *
54
+ * **AM-3 (fix round).** `restoreSqliteSnapshot` used to return `void` — a failing `copyFileSync`/
55
+ * `rmSync` inside it threw straight out of the best-effort `rollback()` wrapper in
56
+ * `agentdb-index.ts`, which swallowed it in an empty `catch` and reported nothing: "absence of a
57
+ * receipt is not success" applies to a rollback exactly as much as to a forward operation. It now
58
+ * returns `{ ok: true } | { ok: false, error }` so the caller can name a failed rollback as failed,
59
+ * never as restored.
60
+ *
61
+ * @packageDocumentation
62
+ */
63
+ import { copyFileSync, existsSync, rmSync, statSync } from 'node:fs';
64
+ /** Escape `path` as a single-quoted sqlite string literal (double any embedded `'`). */
65
+ function sqlQuote(path) {
66
+ return `'${path.replace(/'/g, "''")}'`;
67
+ }
68
+ /**
69
+ * AM-2: the size of `path`, or `undefined` when it does not exist — but ONLY on a confirmed
70
+ * `ENOENT`. Any other stat error (`EACCES`, `EIO`, a raced deletion mid-call, …) propagates to the
71
+ * caller instead of being folded into "does not exist": a snapshot that cannot even determine
72
+ * whether a `-wal` sidecar exists must abort, not silently report the weaker `copy` method as if it
73
+ * had made an informed choice.
74
+ */
75
+ function statSizeIfExists(path) {
76
+ try {
77
+ return statSync(path).size;
78
+ }
79
+ catch (err) {
80
+ if (err?.code === 'ENOENT')
81
+ return undefined;
82
+ throw err;
83
+ }
84
+ }
85
+ /**
86
+ * AM-4: refuse to snapshot into an existing, non-empty `backupPath` — silently overwriting it would
87
+ * discard whatever it held (possibly itself a valid undo point) with no trace. An existing but EMPTY
88
+ * file is not a real prior snapshot and is allowed through untouched.
89
+ */
90
+ function assertBackupTargetFree(backupPath) {
91
+ const size = statSizeIfExists(backupPath);
92
+ if (size !== undefined && size > 0) {
93
+ throw new Error(`snapshot target exists and is non-empty: ${backupPath}`);
94
+ }
95
+ }
96
+ /**
97
+ * AM-1: remove any `-wal`/`-shm` sidecar already sitting at `backupPath` — a leftover from an
98
+ * earlier, unrelated snapshot family that happened to reuse this name. Called BEFORE either strategy
99
+ * below writes anything, and again immediately after a successful `VACUUM INTO` (whose output is one
100
+ * self-contained file that must never be shadowed by a stale sidecar from a previous attempt at the
101
+ * same path).
102
+ */
103
+ function clearBackupSidecars(backupPath) {
104
+ rmSync(`${backupPath}-wal`, { force: true });
105
+ rmSync(`${backupPath}-shm`, { force: true });
106
+ }
107
+ function copyWithWal(dbFile, backupPath, note) {
108
+ // AM-2: statSync directly; only a confirmed ENOENT means "no -wal to copy" — any other stat error
109
+ // propagates out of this function (and out of snapshotSqliteDatabase) as a thrown exception.
110
+ // Lead edit after re-review: the stat runs BEFORE the main file is copied, so a stat failure leaves
111
+ // no half-written target behind (which AM-4 would otherwise refuse on the retry).
112
+ const walSize = statSizeIfExists(`${dbFile}-wal`) ?? 0;
113
+ copyFileSync(dbFile, backupPath);
114
+ if (walSize > 0) {
115
+ copyFileSync(`${dbFile}-wal`, `${backupPath}-wal`);
116
+ return { method: 'copy+wal', note };
117
+ }
118
+ return { method: 'copy', note };
119
+ }
120
+ /**
121
+ * Take a consistent snapshot of `dbFile` into `backupPath` (FR-1/FR-2). Default: open `dbFile`
122
+ * READ-ONLY and run `VACUUM INTO <backupPath>` — one output file holding every committed
123
+ * transaction (including `-wal` frames), with no write to the live database (NFR-2 — MEASURED:
124
+ * main-file hash and `-wal` size are unchanged across the call). On any failure — an older sqlite
125
+ * without `VACUUM INTO`, a locked file, no disk space — or when `opts.strategy` forces it (tests),
126
+ * falls back to `copyFileSync(dbFile, backupPath)` plus a copy of `dbFile-wal` to `backupPath-wal`
127
+ * when the WAL sibling exists and is non-empty; `method` is `'copy+wal'` when the sibling was
128
+ * copied, `'copy'` when there was none to copy. `note` names the fallback reason (FR-2) — set even
129
+ * for a forced-strategy test call, so a caller never has to guess why the fast path was skipped.
130
+ * The read-only connection opened for `VACUUM INTO` is always closed before returning.
131
+ *
132
+ * Throws (no snapshot taken, or an incomplete one left in a fully-cleared state) when: `backupPath`
133
+ * already names an existing, non-empty file (AM-4); or a `-wal` stat probe hits a non-ENOENT error
134
+ * (AM-2, inside the fallback path). Never silently overwrites, never mis-reports a lesser guarantee
135
+ * as a stronger one.
136
+ */
137
+ export function snapshotSqliteDatabase(Database, dbFile, backupPath, opts = {}) {
138
+ assertBackupTargetFree(backupPath); // AM-4 — before any write, on either strategy
139
+ clearBackupSidecars(backupPath); // AM-1 — clean slate before either strategy writes anything
140
+ if (opts.strategy === 'copy+wal') {
141
+ return copyWithWal(dbFile, backupPath, 'snapshotStrategy=copy+wal forced by caller');
142
+ }
143
+ try {
144
+ const db = new Database(dbFile, { readonly: true });
145
+ try {
146
+ db.exec(`VACUUM INTO ${sqlQuote(backupPath)}`);
147
+ }
148
+ finally {
149
+ db.close();
150
+ }
151
+ clearBackupSidecars(backupPath); // AM-1 — VACUUM INTO's output never has a matching -wal of its own
152
+ return { method: 'vacuum-into' };
153
+ }
154
+ catch (err) {
155
+ return copyWithWal(dbFile, backupPath, `VACUUM INTO failed: ${err instanceof Error ? err.message : String(err)}`);
156
+ }
157
+ }
158
+ /**
159
+ * Restore `dbFile` from a snapshot taken by {@link snapshotSqliteDatabase} (FR-4). The caller MUST
160
+ * close its write connection to `dbFile` BEFORE calling this — this restore is a plain file copy,
161
+ * not a sqlite-mediated rollback, and a live handle can reintroduce exactly the frames being undone.
162
+ *
163
+ * `method` is the EXACT {@link SnapshotMethod} the paired `snapshotSqliteDatabase` call returned
164
+ * (AM-1) — never re-derived from whether `backupPath-wal` happens to exist on disk, which a stale
165
+ * sidecar from an unrelated earlier snapshot at the same path could satisfy. Copies `backupPath`
166
+ * over `dbFile`; when `method === 'copy+wal'`, also restores `backupPath-wal` to `dbFile-wal`.
167
+ * Otherwise (`'vacuum-into'` or `'copy'`, neither of which produced a matching `-wal` of its own) any
168
+ * LIVE `dbFile-wal` — left over from the operation being undone — is removed instead: MEASURED,
169
+ * leaving it in place made a reopened "restored" db replay that WAL's frames (a since-superseded
170
+ * write) on top of the reverted main file, returning zero rows instead of the restored set.
171
+ * `dbFile-shm` is always removed — its offsets are valid only for the `-wal` that no longer matches.
172
+ *
173
+ * Returns `{ ok: true }`, or `{ ok: false, error }` (AM-3) when any step throws — a caller that
174
+ * reported a failed restore as a successful one would leave the store worse than before rollback was
175
+ * attempted, with nothing on record to say so.
176
+ */
177
+ export function restoreSqliteSnapshot(dbFile, backupPath, method) {
178
+ try {
179
+ if (!existsSync(backupPath))
180
+ return { ok: false, error: `snapshot missing at ${backupPath}` };
181
+ copyFileSync(backupPath, dbFile);
182
+ if (method === 'copy+wal') {
183
+ copyFileSync(`${backupPath}-wal`, `${dbFile}-wal`);
184
+ }
185
+ else {
186
+ rmSync(`${dbFile}-wal`, { force: true });
187
+ }
188
+ rmSync(`${dbFile}-shm`, { force: true });
189
+ return { ok: true };
190
+ }
191
+ catch (err) {
192
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
193
+ }
194
+ }
195
+ //# sourceMappingURL=agentdb-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentdb-snapshot.js","sourceRoot":"","sources":["../src/agentdb-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAwBrE,wFAAwF;AACxF,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AACzC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACxE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,sBAAsB,CAAC,UAAkB;IAChD,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,4CAA4C,UAAU,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,UAAkB;IAC7C,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,CAAC,GAAG,UAAU,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,MAAc,EAAE,UAAkB,EAAE,IAAY;IACnE,kGAAkG;IAClG,6FAA6F;IAC7F,oGAAoG;IACpG,kFAAkF;IAClF,MAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IACvD,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,YAAY,CAAC,GAAG,MAAM,MAAM,EAAE,GAAG,UAAU,MAAM,CAAC,CAAC;QACnD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAwB,EACxB,MAAc,EACd,UAAkB,EAClB,OAAkD,EAAE;IAEpD,sBAAsB,CAAC,UAAU,CAAC,CAAC,CAAC,8CAA8C;IAClF,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC,4DAA4D;IAC7F,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,4CAA4C,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,EAAE,CAAC,IAAI,CAAC,eAAe,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QACjD,CAAC;gBAAS,CAAC;YACT,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,CAAC;QACD,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAC,mEAAmE;QACpG,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,UAAkB,EAAE,MAAsB;IAC9F,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,uBAAuB,UAAU,EAAE,EAAE,CAAC;QAC9F,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC1B,YAAY,CAAC,GAAG,UAAU,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAChF,CAAC;AACH,CAAC"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Apply-leg module (feature `setup-installs-apply-leg`, ADR-001 Decisions 1 and 2).
3
+ *
4
+ * The self-learning loop has three legs — COLLECT (session hooks → store), RANK (`dz teach` /
5
+ * `dz recall` / `dz consolidate`), APPLY (a `UserPromptSubmit` hook injects learned lessons back
6
+ * into the prompt via a resident embedding daemon). `dz setup` shipped the first two legs; the
7
+ * third lived ONLY as hand-committed files in this hub's own `.claude/helpers/` — a consumer who
8
+ * ran `dz setup --target claude-code --memory agentdb` got session hooks and a memory store, but no
9
+ * recall injection at all (00_complexity_assessment.md, MEASURED 2026-09-12: clean scratch installs
10
+ * on 0.8.10 and 0.8.22, with and without `--memory agentdb`, wrote no `UserPromptSubmit` entry).
11
+ *
12
+ * This module is the SOURCE of that third leg: the two helper files as versioned template
13
+ * generators (the same shape as `AGENTDB_WRITER_VERSION`/`generateAgentdbWriter` in `setup.ts`),
14
+ * the hook-registry entries `runSetup` merges into `settings.json`, and `applyLegStatus` — the ONE
15
+ * measurement `dz doctor` and `dz parity` both read, so neither can drift into declaring the leg
16
+ * present from a static capability table again (ADR-001 Decision 3, the "приборы врут" defect).
17
+ *
18
+ * Decision 2 (why `coreDistDir` is baked in, first candidate): a hook file lives in a CONSUMER
19
+ * project's `.claude/helpers/`, where `require('@dzhechkov/harness-core')` cannot resolve — Node
20
+ * resolves a bare specifier upward from the file, and the package is not above it in a global
21
+ * install. The old hard-coded `/usr/lib/node_modules/...` candidate assumed one specific npm
22
+ * prefix; a project set up from an nvm or `/usr/local` install found nothing. The fix mirrors
23
+ * `claude-hooks-assets.ts`'s destructive-guard hook: bake the INSTALLING copy's absolute `dist`
24
+ * directory into the generated body as the FIRST candidate (proven first at C-5), keep the
25
+ * project-local candidates as fallbacks for a consumer that vendors its own harness-core.
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+ /**
30
+ * Version stamped into BOTH generated helper files as `// dz-apply-leg-version: N` (line 2, right
31
+ * after the shebang). Bump on ANY change to {@link recallHookSource} or {@link embedDaemonSource}'s
32
+ * output — `runSetup` regenerates a deployed helper whose stamp is older, without requiring
33
+ * `--force` (the same self-healing contract as `AGENTDB_WRITER_VERSION`).
34
+ *
35
+ * Bumped 2→3 (fix round 1, review Codex C, MEDIUM finding "hub twins непереносимы"):
36
+ * `recallHookSource`'s `loadCoreModule` candidate list is now built with a conditional spread so a
37
+ * `null` `coreDistDir` (the hub's own portable marker, C-6/finding-6) degrades cleanly to the
38
+ * project-relative fallbacks — the generated BYTES changed for every caller, hub and consumer alike.
39
+ */
40
+ export declare const APPLY_LEG_VERSION = 3;
41
+ /**
42
+ * Parse the `dz-apply-leg-version` stamp from a deployed helper file. Unlike
43
+ * `writerVersionOf` (which floors an absent stamp at `0`), this returns `-1` for "no stamp at
44
+ * all" per the plan contract (T1) — `0` is reserved for a future explicit `version 0` helper, and
45
+ * collapsing "never installed" into the same number as "installed at v0" would make
46
+ * `applyLegStatus` unable to tell the two apart.
47
+ */
48
+ export declare function applyLegVersionOf(content: string): number;
49
+ /**
50
+ * Read back the `coreDistDir` baked into a deployed `recall-hook.cjs` by {@link recallHookSource}
51
+ * (fix round 1, review Codex C, MEDIUM finding "переезд ядра"): `undefined` when the file carries no
52
+ * recognisable `const CORE_DIST_DIR = …;` line at all (e.g. a pre-feature or hand-edited file),
53
+ * `null` for the hub's own portable marker, otherwise the baked absolute path. `runSetup` compares
54
+ * this against the CURRENT `coreDistDir` so a project whose npm/nvm relocated `harness-core` gets
55
+ * its helper rewritten even when {@link APPLY_LEG_VERSION} did not change — a version-only staleness
56
+ * check missed exactly this case (MEASURED: a stale baked path degrades the hook to permanent
57
+ * silence, never a thrown error, so nothing else would ever surface it).
58
+ */
59
+ export declare function bakedCoreDistDirOf(content: string): string | null | undefined;
60
+ /**
61
+ * Generate `.claude/helpers/recall-hook.cjs`. `coreDistDir` is baked in as the FIRST resolve
62
+ * candidate in `loadCoreModule` (Decision 2, AC-6/C-5) — an absolute path, supplied by the
63
+ * installer (`dz setup`'s CLI caller resolves its own installed `@dzhechkov/harness-core`).
64
+ *
65
+ * `coreDistDir === null` is the HUB's OWN portable marker (fix round 1, review Codex C, MEDIUM
66
+ * finding "hub twins непереносимы"): a checked-in file that bakes an ABSOLUTE checkout path fails
67
+ * the twins byte-identity test in every clone but the one it was generated from. The hub instead
68
+ * regenerates its two `.claude/helpers/*` files with `null`, which renders `const CORE_DIST_DIR =
69
+ * null;` and — since `loadCoreModule`'s candidate list below SKIPS a falsy `CORE_DIST_DIR` — falls
70
+ * straight through to the project-relative fallback candidates, the SAME `<project>/packages/
71
+ * @dzhechkov/harness-core/dist/<file>` path the hub's own `coreDistDir` would have baked anyway
72
+ * (`PROJECT` resolves from `CLAUDE_PROJECT_DIR`/`cwd()` at hook RUNTIME, not at generation time, so
73
+ * it is correct in any clone). A real consumer install still gets its installer's absolute path
74
+ * baked in as before — this is additive, not a behavior change for that case.
75
+ *
76
+ * Behaviourally identical to the pre-existing hand-committed hub file except for: the version
77
+ * stamp (new, line 2) and the candidate list in `loadCoreModule` (baked path first when present,
78
+ * `/usr/lib/...` dropped — FR-3).
79
+ */
80
+ export declare function recallHookSource(coreDistDir: string | null): string;
81
+ /** `setTimeout`'s delay argument is a 32-bit signed int in Node — see {@link resolveIdleMs}. */
82
+ export declare const IDLE_MS_INT32_MAX = 2147483647;
83
+ /** Result of resolving a raw `DZ_EMBED_IDLE_MS` value into the daemon's actual idle-exit timer. */
84
+ export interface ResolvedIdleMs {
85
+ /** `null` ⇒ the idle exit is disabled outright (0/NaN/negative). Otherwise the clamped ms value. */
86
+ readonly value: number | null;
87
+ /** True ⇒ `value` was clamped down from a raw value above {@link IDLE_MS_INT32_MAX}. */
88
+ readonly clamped: boolean;
89
+ }
90
+ /**
91
+ * Pure mirror of the `IDLE_MS` resolution inlined into {@link embedDaemonSource}'s generated body
92
+ * (AM-10, dz-harness-hub issue #10 defect 7). Extracted (fix round 1, review Codex C, LOW finding)
93
+ * so the clamp/disable rule is unit-testable directly — the review noted the only prior proof was a
94
+ * LIVE process observed via its first stderr log line, which shows the log fired but not that the
95
+ * underlying arithmetic is right for every input class.
96
+ *
97
+ * A template string cannot `import` this function into the daemon's own runtime (it is generated
98
+ * text, not compiled code), so {@link embedDaemonSource} keeps its OWN copy of the same three rules —
99
+ * `apply-leg-twins.test.ts` pins the generated body to the same `IDLE_MS_INT32_MAX` constant this
100
+ * module exports, so the two cannot drift apart silently.
101
+ */
102
+ export declare function resolveIdleMs(raw: number): ResolvedIdleMs;
103
+ /**
104
+ * Generate `.claude/helpers/dz-embed-daemon.mjs`. Behaviourally identical to the pre-existing
105
+ * hand-committed hub file except for: the version stamp (new, line 2) and `resolveDeps()`, which
106
+ * now tries `@huggingface/transformers` before falling back to `@xenova/transformers` — AM-3,
107
+ * dz-harness-hub issue #10 defect 3: `agentdb >= 3.0.0-alpha` depends on the former, and an older
108
+ * agentdb install still carries the latter, so probing only one name silently starved the daemon
109
+ * on either side of that agentdb version boundary.
110
+ */
111
+ export declare function embedDaemonSource(): string;
112
+ /** One Claude Code hook-registry entry in the matcher-group shape (no matcher — session/prompt events don't need one). */
113
+ export interface ApplyLegHookEntry {
114
+ readonly hooks: readonly {
115
+ readonly type: 'command';
116
+ readonly command: string;
117
+ }[];
118
+ }
119
+ /**
120
+ * The two hook-registry entries `runSetup` merges into `.claude/settings.json` (FR-1). Commands
121
+ * match the hub's own `.claude/settings.json` verbatim (`grep`-diffed against it at authoring time):
122
+ * the recall hook is invoked with a swallowed non-zero exit (`|| true`) so a broken hook body never
123
+ * fails a prompt, and the embed daemon is spawned detached via `nohup` + a backgrounding `sh -c`
124
+ * so `SessionStart` never waits on model load.
125
+ */
126
+ export declare function applyLegHookEntries(): {
127
+ readonly userPromptSubmit: ApplyLegHookEntry;
128
+ readonly sessionStart: ApplyLegHookEntry;
129
+ };
130
+ /** Version + presence of one deployed helper file. */
131
+ export interface ApplyLegHelperStatus {
132
+ readonly path: string;
133
+ readonly exists: boolean;
134
+ /** {@link applyLegVersionOf} of the deployed file; `-1` when absent, unstamped, or unreadable. */
135
+ readonly version: number;
136
+ /** True only when the file EXISTS but could not be read (EISDIR, EACCES, …) — distinct from absent. */
137
+ readonly unreadable?: boolean;
138
+ }
139
+ /** Whether a hook-registry entry naming this helper was found in `.claude/settings.json`. */
140
+ export interface ApplyLegHookPresence {
141
+ readonly present: boolean;
142
+ }
143
+ /**
144
+ * Why {@link ApplyLegStatus.installed} is `false`. Absent when `installed` is `true`.
145
+ * - `'missing-helpers'` — at least one helper FILE does not exist.
146
+ * - `'stale-version'` — both helpers exist and are readable, but at least one carries a
147
+ * `dz-apply-leg-version` below {@link APPLY_LEG_VERSION}.
148
+ * - `'hooks-missing'` — both helpers exist and are current, but `.claude/settings.json` does not
149
+ * structurally wire one or both of them under the right event (fix round 1, HIGH finding 1).
150
+ * - `'unreadable'` — a helper file EXISTS but could not be read (fix round 1, Q3 finding): reported
151
+ * distinctly rather than silently folding into `missing-helpers`, which would tell a consumer to
152
+ * re-run `dz setup` when the real problem is a broken/permission-denied file in the way.
153
+ */
154
+ export type ApplyLegNotInstalledReason = 'missing-helpers' | 'stale-version' | 'hooks-missing' | 'unreadable';
155
+ /** The ONE measurement `dz doctor` and `dz parity` both read (ADR-001 Decision 3) — never a static capability declaration. */
156
+ export interface ApplyLegStatus {
157
+ /**
158
+ * True only when BOTH helper files exist, are READABLE, are at {@link APPLY_LEG_VERSION} or newer,
159
+ * AND both hook entries are structurally wired under the correct event — a partial or stale install
160
+ * is not installed (fix round 1, HIGH findings 1 and 2).
161
+ */
162
+ readonly installed: boolean;
163
+ /** Present exactly when `installed` is `false` — see {@link ApplyLegNotInstalledReason}. */
164
+ readonly reason?: ApplyLegNotInstalledReason;
165
+ readonly helpers: {
166
+ readonly recallHook: ApplyLegHelperStatus;
167
+ readonly embedDaemon: ApplyLegHelperStatus;
168
+ };
169
+ readonly hooks: {
170
+ readonly userPromptSubmit: ApplyLegHookPresence;
171
+ readonly sessionStart: ApplyLegHookPresence;
172
+ };
173
+ /** From `.dz/config.json`'s `memory.backend`; `'unknown'` when absent/unreadable/unrecognised. */
174
+ readonly backend: 'agentdb' | 'jsonl' | 'unknown';
175
+ }
176
+ /**
177
+ * ONE definition of "this command invokes our helper", shared by the installer (`setup.ts`
178
+ * `addIfMissing`) and the diagnostics (`applyLegStatus`) so they can never disagree (re-review
179
+ * Codex, third pass). A bare mention (`echo .claude/helpers/recall-hook.cjs`) is not an invocation:
180
+ * the helper path must follow a `node` word — directly, or inside the daemon's
181
+ * `sh -c 'nohup node "…"'` spawn. Forward slashes only: every command dz writes uses them.
182
+ */
183
+ export declare function hookCommandInvokes(command: string, markerPath: string): boolean;
184
+ /**
185
+ * Measure the apply leg's actual state in `root` — plain `fs` reads, no injection (mirrors
186
+ * `writerVersionOf`'s own read style): a diagnostic that needed dependency injection to be testable
187
+ * would be a diagnostic nobody could point at a REAL project either. Never throws (fix round 1, Q3):
188
+ * an unreadable helper is a named fact in the return value, not an exception that would otherwise
189
+ * blow through `dz doctor`'s outer try/catch (silence) or crash `dz parity` outright.
190
+ */
191
+ export declare function applyLegStatus(root: string): ApplyLegStatus;
192
+ /**
193
+ * Human remedy text shared VERBATIM between `dz doctor` and `dz parity` for the two reasons this
194
+ * fix round adds machinery for (fix round 1, HIGH finding 2 / Q3 finding 7): `stale-version` and
195
+ * `unreadable`. Both instruments read the SAME {@link ApplyLegStatus}; routing them through the same
196
+ * text-producing function is what makes "тексты совпадают по причине" a structural guarantee rather
197
+ * than two hand-written strings that happen to agree today. The `missing-helpers`/`hooks-missing`
198
+ * wording is deliberately NOT unified here — `dz doctor` and `dz parity` already carried different
199
+ * (pre-existing, tested) phrasing for that case before this fix round, and unifying it was not asked.
200
+ */
201
+ export declare function applyLegReasonMessage(status: ApplyLegStatus): string;
202
+ //# sourceMappingURL=apply-leg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-leg.d.ts","sourceRoot":"","sources":["../src/apply-leg.ts"],"names":[],"mappings":"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"}