@cleocode/core 2026.6.3 → 2026.6.5

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 (74) hide show
  1. package/dist/docs/export-document.js +626 -310
  2. package/dist/docs/export-document.js.map +3 -3
  3. package/dist/llm/catalog-cache.d.ts +3 -0
  4. package/dist/llm/catalog-cache.d.ts.map +1 -1
  5. package/dist/llm/catalog-cache.js.map +1 -1
  6. package/dist/llm/catalog-model-resolver.d.ts +89 -0
  7. package/dist/llm/catalog-model-resolver.d.ts.map +1 -0
  8. package/dist/llm/catalog-model-resolver.js +158 -0
  9. package/dist/llm/catalog-model-resolver.js.map +1 -0
  10. package/dist/llm/cli-ops.d.ts +14 -0
  11. package/dist/llm/cli-ops.d.ts.map +1 -1
  12. package/dist/llm/cli-ops.js +35 -0
  13. package/dist/llm/cli-ops.js.map +1 -1
  14. package/dist/llm/index.d.ts +3 -0
  15. package/dist/llm/index.d.ts.map +1 -1
  16. package/dist/llm/index.js +3 -0
  17. package/dist/llm/index.js.map +1 -1
  18. package/dist/llm/oauth/pkce.js +27 -5
  19. package/dist/llm/oauth/pkce.js.map +1 -1
  20. package/dist/llm/plugin-facade.js +613 -325
  21. package/dist/llm/plugin-facade.js.map +3 -3
  22. package/dist/llm/provider-registry/builtin/openai.d.ts +11 -2
  23. package/dist/llm/provider-registry/builtin/openai.d.ts.map +1 -1
  24. package/dist/llm/provider-registry/builtin/openai.js +15 -3
  25. package/dist/llm/provider-registry/builtin/openai.js.map +1 -1
  26. package/dist/llm/system-resolver.d.ts +94 -0
  27. package/dist/llm/system-resolver.d.ts.map +1 -0
  28. package/dist/llm/system-resolver.js +165 -0
  29. package/dist/llm/system-resolver.js.map +1 -0
  30. package/dist/memory/dialectic-evaluator.d.ts +13 -6
  31. package/dist/memory/dialectic-evaluator.d.ts.map +1 -1
  32. package/dist/memory/dialectic-evaluator.js +18 -7
  33. package/dist/memory/dialectic-evaluator.js.map +1 -1
  34. package/dist/memory/llm-backend-resolver.d.ts +23 -3
  35. package/dist/memory/llm-backend-resolver.d.ts.map +1 -1
  36. package/dist/memory/llm-backend-resolver.js +135 -0
  37. package/dist/memory/llm-backend-resolver.js.map +1 -1
  38. package/dist/store/dual-scope-db.d.ts +20 -2
  39. package/dist/store/dual-scope-db.d.ts.map +1 -1
  40. package/dist/store/dual-scope-db.js +74 -7
  41. package/dist/store/dual-scope-db.js.map +1 -1
  42. package/dist/store/exodus/archive.d.ts +216 -0
  43. package/dist/store/exodus/archive.d.ts.map +1 -0
  44. package/dist/store/exodus/archive.js +314 -0
  45. package/dist/store/exodus/archive.js.map +1 -0
  46. package/dist/store/exodus/index.d.ts +1 -0
  47. package/dist/store/exodus/index.d.ts.map +1 -1
  48. package/dist/store/exodus/index.js +1 -0
  49. package/dist/store/exodus/index.js.map +1 -1
  50. package/dist/store/exodus/migrate.d.ts.map +1 -1
  51. package/dist/store/exodus/migrate.js +118 -24
  52. package/dist/store/exodus/migrate.js.map +1 -1
  53. package/dist/store/exodus/on-open.d.ts.map +1 -1
  54. package/dist/store/exodus/on-open.js +95 -34
  55. package/dist/store/exodus/on-open.js.map +1 -1
  56. package/dist/store/exodus/types.d.ts +10 -1
  57. package/dist/store/exodus/types.d.ts.map +1 -1
  58. package/dist/store/exodus/types.js.map +1 -1
  59. package/dist/store/exodus/verify-migration.d.ts.map +1 -1
  60. package/dist/store/exodus/verify-migration.js +12 -1
  61. package/dist/store/exodus/verify-migration.js.map +1 -1
  62. package/dist/store/sqlite.d.ts +16 -0
  63. package/dist/store/sqlite.d.ts.map +1 -1
  64. package/dist/store/sqlite.js +160 -39
  65. package/dist/store/sqlite.js.map +1 -1
  66. package/dist/validation/doctor/checks.d.ts +22 -0
  67. package/dist/validation/doctor/checks.d.ts.map +1 -1
  68. package/dist/validation/doctor/checks.js +67 -0
  69. package/dist/validation/doctor/checks.js.map +1 -1
  70. package/dist/validation/doctor/index.d.ts +1 -1
  71. package/dist/validation/doctor/index.d.ts.map +1 -1
  72. package/dist/validation/doctor/index.js +1 -1
  73. package/dist/validation/doctor/index.js.map +1 -1
  74. package/package.json +12 -12
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Exodus source-DB ARCHIVE + completion-marker subsystem (T11777).
3
+ *
4
+ * ## Why this exists (stranded-residue corruption trigger)
5
+ *
6
+ * The exodus engine migrates from SIX legacy source DBs (project: `tasks.db`,
7
+ * `brain.db`, `conduit.db`; global: `nexus.db`, `signaldock.db`, `skills.db`)
8
+ * but, historically, NEVER retired them — `on-open.ts` literally notes "the file
9
+ * is never unlinked." Every cutover therefore STRANDS the legacy files, and each
10
+ * stranded file re-arms the `tasks_tasks=0` auto-recover / exodus-on-open
11
+ * corruption trigger (DHQ-052 · T11662): on the next open the consolidated DB
12
+ * looks empty-with-legacy-present and the hook re-fires.
13
+ *
14
+ * This module closes the loop. After a migration's lossless validation passes
15
+ * (row-count parity + integrity — `verifyMigration` / `isDataContinuityOk`), the
16
+ * consumed source DBs are ARCHIVED (moved, never deleted) into a per-scope
17
+ * `_archive/` directory, and a committed COMPLETION MARKER records the cutover.
18
+ * The marker becomes the durable "this scope is already migrated" signal so a
19
+ * re-appearing or stranded legacy file can never re-trigger exodus-on-open.
20
+ *
21
+ * ## Archive destinations (per scope, via the paths SSoT)
22
+ *
23
+ * - project sources → `<cleoDir>/_archive/` (e.g. `.cleo/_archive/`)
24
+ * - global sources → `<cleoHome>/_archive/` (e.g. `~/.local/share/cleo/_archive/`)
25
+ *
26
+ * Both are resolved through `resolveCleoDir(cwd)` / `getCleoHome()` — never a
27
+ * hardcoded `~/.local/share` (Paths SSoT · Gate 2 · D009).
28
+ *
29
+ * ## Reversibility + idempotency invariants
30
+ *
31
+ * - **Reversible** — archiving is an atomic `rename` (fallback copy+unlink
32
+ * across filesystems). Nothing is ever deleted; an operator can move a DB
33
+ * back out of `_archive/` to roll the cutover back.
34
+ * - **Idempotent** — a source that is already absent (already archived, or a
35
+ * fresh install that never had it) is a silent no-op. Re-running over an
36
+ * already-archived fleet does nothing and never throws.
37
+ * - **Never blind-move** — only sources the caller asserts were actually
38
+ * consumed + validated by the migration are archived. A source whose
39
+ * migration did not run is left untouched.
40
+ * - **Emergency-archive reconciliation** — this box was emergency-archived
41
+ * (`.cleo/_archive-legacy-postcutover-*` already holds `tasks.db` +
42
+ * `conduit.db`). When the canonical destination already contains a file with
43
+ * the same name, the incoming file is parked under a timestamped sibling name
44
+ * rather than clobbering the prior archive.
45
+ *
46
+ * @module
47
+ * @task T11777 (exodus archives all 6 legacy DBs post-validation + completion marker)
48
+ * @epic T11249 (E6)
49
+ * @saga T11242 (SG-DB-SUBSTRATE-V2)
50
+ * @see packages/core/src/store/exodus/on-open.ts — wires this into the validated success path
51
+ * @see packages/core/src/store/exodus/plan.ts — buildSourceDescriptors (the 6 sources)
52
+ */
53
+ import { copyFileSync, existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
54
+ import { basename, join } from 'node:path';
55
+ import { getLogger } from '../../logger.js';
56
+ import { getCleoHome, resolveCleoDir } from '../../paths.js';
57
+ import { getCleoVersion } from '../../scaffold/ensure-config.js';
58
+ const log = getLogger('exodus-archive');
59
+ /** Per-scope archive directory name (sibling of the migrated DBs). */
60
+ const ARCHIVE_DIR_NAME = '_archive';
61
+ /** Per-scope completion-marker filename written next to the consolidated cleo.db. */
62
+ const MARKER_FILENAME_BY_SCOPE = {
63
+ project: 'exodus-complete',
64
+ global: 'exodus-complete',
65
+ };
66
+ /** SQLite sidecar suffixes archived alongside the main DB file. */
67
+ const SIDECAR_SUFFIXES = ['-wal', '-shm'];
68
+ /**
69
+ * Resolve the directory that holds a scope's legacy source DBs (and therefore
70
+ * its archive + completion marker): project → `<cleoDir>`, global →
71
+ * `<cleoHome>`. Always via the paths SSoT.
72
+ *
73
+ * @param scope - Target scope.
74
+ * @param cwd - Working directory used to resolve the project `.cleo/` dir.
75
+ * @returns Absolute path to the scope's base directory.
76
+ */
77
+ function scopeBaseDir(scope, cwd) {
78
+ return scope === 'project' ? resolveCleoDir(cwd) : getCleoHome();
79
+ }
80
+ /**
81
+ * Absolute path to a scope's `_archive/` directory.
82
+ *
83
+ * @param scope - Target scope.
84
+ * @param cwd - Working directory used to resolve the project `.cleo/` dir.
85
+ * @returns Absolute path to `<scopeBase>/_archive/`.
86
+ */
87
+ export function exodusArchiveDir(scope, cwd) {
88
+ return join(scopeBaseDir(scope, cwd), ARCHIVE_DIR_NAME);
89
+ }
90
+ /**
91
+ * Absolute path to a scope's exodus completion marker file.
92
+ *
93
+ * @param scope - Target scope.
94
+ * @param cwd - Working directory used to resolve the project `.cleo/` dir.
95
+ * @returns Absolute path to `<scopeBase>/exodus-complete`.
96
+ */
97
+ export function exodusMarkerPath(scope, cwd) {
98
+ return join(scopeBaseDir(scope, cwd), MARKER_FILENAME_BY_SCOPE[scope]);
99
+ }
100
+ /**
101
+ * Return `true` if a scope's exodus completion marker exists on disk.
102
+ *
103
+ * Resolution-safe: when the project `.cleo/` dir cannot be resolved (e.g. `cwd`
104
+ * is not inside a CLEO project — `resolveCleoDir` throws), this returns `false`
105
+ * (no marker) rather than propagating, so the on-open trigger gate degrades to
106
+ * the source-file path safely instead of crashing the open.
107
+ *
108
+ * @param scope - Target scope.
109
+ * @param cwd - Working directory used to resolve the project `.cleo/` dir.
110
+ * @returns Whether `<scopeBase>/exodus-complete` exists.
111
+ */
112
+ export function hasExodusCompleteMarker(scope, cwd) {
113
+ try {
114
+ return existsSync(exodusMarkerPath(scope, cwd));
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ /**
121
+ * Write a scope's exodus completion marker atomically (write-then-rename).
122
+ *
123
+ * Idempotent: re-writing simply refreshes the marker (same path). The marker is
124
+ * the SSoT trigger-gate consulted by {@link maybeRunExodusOnOpen} — once present,
125
+ * a stranded/re-appearing legacy file cannot re-arm the auto-migration.
126
+ *
127
+ * @param scope - Scope being certified as migrated.
128
+ * @param archivedSources - Logical names of the sources archived for this scope.
129
+ * @param cwd - Working directory used to resolve the project dir.
130
+ * @returns The marker's absolute path.
131
+ *
132
+ * @task T11777
133
+ */
134
+ export function writeExodusCompleteMarker(scope, archivedSources, cwd) {
135
+ const markerPath = exodusMarkerPath(scope, cwd);
136
+ const baseDir = scopeBaseDir(scope, cwd);
137
+ mkdirSync(baseDir, { recursive: true });
138
+ const marker = {
139
+ version: 1,
140
+ scope,
141
+ cleoVersion: getCleoVersion(),
142
+ completedAt: new Date().toISOString(),
143
+ archivedSources: [...archivedSources],
144
+ };
145
+ const tmpPath = `${markerPath}.tmp`;
146
+ writeFileSync(tmpPath, JSON.stringify(marker, null, 2) + '\n', 'utf8');
147
+ renameSync(tmpPath, markerPath);
148
+ log.info({ scope, markerPath, archivedSources }, 'exodus: wrote completion marker');
149
+ return markerPath;
150
+ }
151
+ /**
152
+ * Move a single file to `destDir`, atomically when possible.
153
+ *
154
+ * Uses `rename`; on a cross-filesystem `EXDEV` (or any rename failure) falls back
155
+ * to copy-then-unlink so the move still completes. When `destDir` already holds a
156
+ * file with the same name (e.g. a prior emergency archive), the incoming file is
157
+ * parked under a timestamped sibling name so the prior archive is never clobbered.
158
+ *
159
+ * @param srcPath - Absolute source file path (assumed to exist).
160
+ * @param destDir - Absolute archive directory (created if missing).
161
+ * @returns The absolute destination path the file landed at.
162
+ */
163
+ function moveFileInto(srcPath, destDir) {
164
+ mkdirSync(destDir, { recursive: true });
165
+ let dest = join(destDir, basename(srcPath));
166
+ if (existsSync(dest)) {
167
+ // Do not clobber a prior archive (e.g. emergency-archived tasks.db). Park
168
+ // the incoming file under a timestamped sibling name instead.
169
+ const stamp = new Date().toISOString().replace(/[:.]/g, '').replace(/Z$/, 'Z');
170
+ dest = join(destDir, `${basename(srcPath)}.${stamp}`);
171
+ }
172
+ try {
173
+ renameSync(srcPath, dest);
174
+ }
175
+ catch {
176
+ // Cross-filesystem (EXDEV) or other rename failure → copy + unlink fallback.
177
+ copyFileSync(srcPath, dest);
178
+ unlinkSync(srcPath);
179
+ }
180
+ return dest;
181
+ }
182
+ /**
183
+ * Archive ONE legacy source DB (and its `-wal` / `-shm` sidecars) into the
184
+ * scope's `_archive/` directory.
185
+ *
186
+ * Idempotent: if the main DB file is already absent, this is a no-op (the file
187
+ * was already archived, or never existed). Sidecars are archived best-effort and
188
+ * only when the main DB is present.
189
+ *
190
+ * @param source - The descriptor for the source DB to archive.
191
+ * @param cwd - Working directory used to resolve the project dir.
192
+ * @returns A {@link ArchivedSourceResult} describing what happened.
193
+ *
194
+ * @task T11777
195
+ */
196
+ export function archiveSourceDb(source, cwd) {
197
+ // Idempotent no-op: nothing to archive (already archived or fresh install).
198
+ if (!existsSync(source.path)) {
199
+ return { name: source.name, sourcePath: source.path, archivedTo: null, action: 'absent' };
200
+ }
201
+ const destDir = exodusArchiveDir(source.targetScope, cwd);
202
+ const archivedTo = moveFileInto(source.path, destDir);
203
+ // Archive sidecars alongside the DB (best-effort — they may not exist).
204
+ for (const suffix of SIDECAR_SUFFIXES) {
205
+ const sidecar = `${source.path}${suffix}`;
206
+ if (existsSync(sidecar)) {
207
+ try {
208
+ moveFileInto(sidecar, destDir);
209
+ }
210
+ catch (err) {
211
+ log.warn({ err, sidecar, sourceName: source.name }, 'exodus-archive: failed to archive sidecar (non-fatal)');
212
+ }
213
+ }
214
+ }
215
+ log.info({ sourceName: source.name, sourcePath: source.path, archivedTo, scope: source.targetScope }, 'exodus-archive: archived legacy source DB');
216
+ return {
217
+ name: source.name,
218
+ sourcePath: source.path,
219
+ archivedTo,
220
+ action: 'archived',
221
+ };
222
+ }
223
+ /**
224
+ * Archive every consumed legacy source DB AFTER a validated cutover and write a
225
+ * per-scope completion marker.
226
+ *
227
+ * **Never blind-moves**: only the descriptors passed in `consumed` are archived
228
+ * — the caller (the validated migrate/on-open success path) passes exactly the
229
+ * sources whose migration actually ran and passed parity. A completion marker is
230
+ * written for every scope represented in `consumed`, even if some of that scope's
231
+ * sources were already absent (already archived) — the marker certifies "this
232
+ * scope's cutover is done", which is true once parity passed.
233
+ *
234
+ * Idempotent + reversible (see module docs). Safe to call repeatedly.
235
+ *
236
+ * @param consumed - Source descriptors the migration consumed + validated.
237
+ * @param cwd - Working directory used to resolve the project dir.
238
+ * @returns A {@link ArchiveMigratedSourcesResult} with per-source + per-scope outcomes.
239
+ *
240
+ * @task T11777
241
+ */
242
+ export function archiveMigratedSources(consumed, cwd) {
243
+ const results = [];
244
+ const scopes = new Set();
245
+ for (const source of consumed) {
246
+ scopes.add(source.targetScope);
247
+ results.push(archiveSourceDb(source, cwd));
248
+ }
249
+ const markersWritten = [];
250
+ for (const scope of scopes) {
251
+ const archivedForScope = consumed.filter((s) => s.targetScope === scope).map((s) => s.name);
252
+ writeExodusCompleteMarker(scope, archivedForScope, cwd);
253
+ markersWritten.push(scope);
254
+ }
255
+ return { sources: results, markersWritten };
256
+ }
257
+ /**
258
+ * Detect stranded legacy source DBs: any of the six sources still present on
259
+ * disk for a scope whose exodus completion marker exists.
260
+ *
261
+ * Returns an empty array when no marker exists for either scope (a pre-cutover
262
+ * install where legacy DBs are still the live source of truth — NOT residue) or
263
+ * when every source for a marked scope has been archived.
264
+ *
265
+ * @param sources - The full legacy source descriptor list (from `buildExodusPlan`).
266
+ * @param cwd - Working directory used to resolve the project dir + markers.
267
+ * @returns The stranded entries (empty when clean).
268
+ *
269
+ * @task T11777
270
+ */
271
+ export function detectStrandedResidue(sources, cwd) {
272
+ const markedScopes = new Set();
273
+ for (const scope of ['project', 'global']) {
274
+ if (hasExodusCompleteMarker(scope, cwd))
275
+ markedScopes.add(scope);
276
+ }
277
+ if (markedScopes.size === 0)
278
+ return [];
279
+ const stranded = [];
280
+ for (const source of sources) {
281
+ if (!markedScopes.has(source.targetScope))
282
+ continue;
283
+ if (existsSync(source.path)) {
284
+ stranded.push({ name: source.name, path: source.path, scope: source.targetScope });
285
+ }
286
+ }
287
+ return stranded;
288
+ }
289
+ /**
290
+ * Archive stranded residue detected by {@link detectStrandedResidue}.
291
+ *
292
+ * This is the `--fix` action for the `cleo doctor exodus-residue` check. It reuses
293
+ * {@link archiveSourceDb} so the on-open success path and the doctor fix share one
294
+ * archive routine. Reversible (move, never delete) and idempotent.
295
+ *
296
+ * @param stranded - The stranded entries to archive.
297
+ * @param sources - The full source descriptor list (to map name → descriptor).
298
+ * @param cwd - Working directory used to resolve the project dir.
299
+ * @returns Per-source archive outcomes.
300
+ *
301
+ * @task T11777
302
+ */
303
+ export function archiveStrandedResidue(stranded, sources, cwd) {
304
+ const byName = new Map(sources.map((s) => [s.name, s]));
305
+ const results = [];
306
+ for (const entry of stranded) {
307
+ const descriptor = byName.get(entry.name);
308
+ if (descriptor === undefined)
309
+ continue;
310
+ results.push(archiveSourceDb(descriptor, cwd));
311
+ }
312
+ return results;
313
+ }
314
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../../../src/store/exodus/archive.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@
8
8
  * @task T11248 (E5 · SG-DB-SUBSTRATE-V2)
9
9
  * @saga T11242
10
10
  */
11
+ export { type ArchivedSourceResult, type ArchiveMigratedSourcesResult, archiveMigratedSources, archiveSourceDb, archiveStrandedResidue, detectStrandedResidue, type ExodusCompleteMarker, exodusArchiveDir, exodusMarkerPath, hasExodusCompleteMarker, type StrandedResidueEntry, writeExodusCompleteMarker, } from './archive.js';
11
12
  export { clearExodusJournal, runExodusMigrate } from './migrate.js';
12
13
  export { buildExodusPlan, deriveStagingDirName, sourcesPresent } from './plan.js';
13
14
  export { runExodusStatus } from './status.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/store/exodus/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,aAAa,EACb,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,4BAA4B,EAC5B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/store/exodus/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,EACjC,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,aAAa,EACb,KAAK,mBAAmB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,4BAA4B,EAC5B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -8,6 +8,7 @@
8
8
  * @task T11248 (E5 · SG-DB-SUBSTRATE-V2)
9
9
  * @saga T11242
10
10
  */
11
+ export { archiveMigratedSources, archiveSourceDb, archiveStrandedResidue, detectStrandedResidue, exodusArchiveDir, exodusMarkerPath, hasExodusCompleteMarker, writeExodusCompleteMarker, } from './archive.js';
11
12
  export { clearExodusJournal, runExodusMigrate } from './migrate.js';
12
13
  export { buildExodusPlan, deriveStagingDirName, sourcesPresent } from './plan.js';
13
14
  export { runExodusStatus } from './status.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/store/exodus/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,aAAa,GAEd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,4BAA4B,GAY7B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/store/exodus/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAGL,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EAErB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EAEvB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,aAAa,GAEd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,4BAA4B,GAY7B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/store/exodus/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgHG;AAkBH,OAAO,KAAK,EAEV,mBAAmB,EACnB,UAAU,EAMX,MAAM,YAAY,CAAC;AAiEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAiB9D;AAs3BD;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,UAAU,EAChB,iBAAiB,UAAQ,EACzB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA0I9B"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/store/exodus/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgHG;AAmBH,OAAO,KAAK,EAEV,mBAAmB,EACnB,UAAU,EAMX,MAAM,YAAY,CAAC;AAiEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAiB9D;AAw9BD;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,UAAU,EAChB,iBAAiB,UAAQ,EACzB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAoK9B"}
@@ -115,7 +115,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSy
115
115
  import { join } from 'node:path';
116
116
  import { getLogger } from '../../logger.js';
117
117
  import { getCleoVersion } from '../../scaffold/ensure-config.js';
118
- import { openDualScopeDb } from '../dual-scope-db.js';
118
+ import { openDualScopeDbAtPath } from '../dual-scope-db.js';
119
119
  import { openCleoDbSnapshot } from '../open-cleo-db.js';
120
120
  import { resolveConsolidatedTableName, resolveTableTargetScope } from './table-name-map.js';
121
121
  import { EXODUS_TARGET_SCHEMA_VERSION } from './types.js';
@@ -419,6 +419,35 @@ function enumNormExpr(targetTableName, col, srcRef) {
419
419
  const fn = ENUM_NORMALIZATIONS.get(key);
420
420
  return fn ? fn(srcRef) : null;
421
421
  }
422
+ const NUMERIC_CLAMPS = new Map([
423
+ // --- brain_weight_history.delta_weight (T11782) -------------------------
424
+ // +Inf → 1.0 (max canonical reinforcement), -Inf → -1.0 (max canonical
425
+ // depression), NaN → 0.0 (no-op delta). Finite values pass through.
426
+ [
427
+ 'brain_weight_history.delta_weight',
428
+ (src) => `CASE` +
429
+ ` WHEN ${src} = 9e999 THEN 1.0` +
430
+ ` WHEN ${src} = -9e999 THEN -1.0` +
431
+ ` WHEN ${src} != ${src} THEN 0.0` +
432
+ ` ELSE ${src}` +
433
+ ` END`,
434
+ ],
435
+ ]);
436
+ /**
437
+ * Return a SQL CASE expression that clamps non-finite legacy REAL values for
438
+ * `col` in `targetTableName` to a finite in-range value, or `null` when no
439
+ * clamp rule exists for this (table, column).
440
+ *
441
+ * @param targetTableName - Physical consolidated target table name.
442
+ * @param col - Column name.
443
+ * @param srcRef - SQL expression referencing the source column.
444
+ * @returns A SQL CASE expression string, or `null` if no rule applies.
445
+ */
446
+ function numericClampExpr(targetTableName, col, srcRef) {
447
+ const key = `${targetTableName}.${col}`;
448
+ const fn = NUMERIC_CLAMPS.get(key);
449
+ return fn ? fn(srcRef) : null;
450
+ }
422
451
  // ---------------------------------------------------------------------------
423
452
  // Epoch-to-ISO coercion layer (ROOT CAUSE 1 fix — T11546)
424
453
  // ---------------------------------------------------------------------------
@@ -558,12 +587,15 @@ function detectIsoGlobColumns(db, tableName, targetSchema = 'main') {
558
587
  *
559
588
  * 1. **Epoch→ISO-8601 coercion** (T11546): when the target has an ISO GLOB CHECK
560
589
  * and the source column is INTEGER-typed.
561
- * 2. **Enum-value normalization** (T11547): when `ENUM_NORMALIZATIONS` has an
590
+ * 2. **Non-finite numeric clamp** (T11782): when `NUMERIC_CLAMPS` has an entry
591
+ * for `(targetTableName, col)`, mapping `Inf`/`-Inf`/`NaN` to a finite in-range
592
+ * value so the row is not silently dropped.
593
+ * 3. **Enum-value normalization** (T11547): when `ENUM_NORMALIZATIONS` has an
562
594
  * entry for `(targetTableName, col)`, producing a SQL CASE expression that
563
595
  * maps legacy values to canonical enum members without losing semantics.
564
- * 3. **NOT NULL coalesce** (T11533): for non-epoch, non-normalized columns whose
596
+ * 4. **NOT NULL coalesce** (T11533): for non-epoch, non-normalized columns whose
565
597
  * target is NOT NULL with no schema default.
566
- * 4. **Plain column reference** otherwise.
598
+ * 5. **Plain column reference** otherwise.
567
599
  *
568
600
  * The epoch→ISO conversion uses `buildEpochToIsoExpr` which detects the epoch
569
601
  * scale (seconds vs milliseconds) per-row using a magnitude heuristic: values
@@ -605,7 +637,20 @@ function buildSelectExpr(attachAlias, legacyTable, targetTableName, col, srcType
605
637
  }
606
638
  return `${isoExpr} AS "${col}"`;
607
639
  }
608
- // Priority 2: Enum-value normalization (T11547) — maps legacy enum values to
640
+ // Priority 2: Non-finite numeric clamp (T11782) — coerce Inf/-Inf/NaN legacy
641
+ // REAL values to a finite in-range value so the row is not silently dropped.
642
+ // (delta_weight is NOT NULL without a default — but the clamp always produces
643
+ // a non-NULL finite value for non-NULL input, so no extra COALESCE is needed;
644
+ // a genuinely NULL source still flows to the NOT NULL coalesce below.)
645
+ const clampExpr = numericClampExpr(targetTableName, col, srcRef);
646
+ if (clampExpr !== null) {
647
+ if (isNotNullWithoutDefault) {
648
+ const defLiteral = typeDefaultLiteral(tgtInfo.type);
649
+ return `COALESCE(${clampExpr}, ${defLiteral}) AS "${col}"`;
650
+ }
651
+ return `${clampExpr} AS "${col}"`;
652
+ }
653
+ // Priority 3: Enum-value normalization (T11547) — maps legacy enum values to
609
654
  // canonical members so CHECK constraints accept them.
610
655
  const normExpr = enumNormExpr(targetTableName, col, srcRef);
611
656
  if (normExpr !== null) {
@@ -617,8 +662,8 @@ function buildSelectExpr(attachAlias, legacyTable, targetTableName, col, srcType
617
662
  }
618
663
  return `${normExpr} AS "${col}"`;
619
664
  }
620
- // Priority 3: Standard NOT NULL coalesce for non-epoch, non-normalized columns
621
- // (T11533 fix preserved).
665
+ // Priority 4: Standard NOT NULL coalesce for non-epoch, non-clamped,
666
+ // non-normalized columns (T11533 fix preserved).
622
667
  if (isNotNullWithoutDefault) {
623
668
  const defLiteral = typeDefaultLiteral(tgtInfo.type);
624
669
  return `COALESCE(${srcRef}, ${defLiteral}) AS "${col}"`;
@@ -767,13 +812,23 @@ function copyTableFromAttached(targetNativeDb, srcNativeDb, attachAlias, legacyT
767
812
  if (normalizedCols.length > 0) {
768
813
  log.info({ legacyTableName, targetTableName, sourceName, normalizedCols }, `Exodus: applying enum-value normalization for ${normalizedCols.length} column(s) (T11547)`);
769
814
  }
815
+ // --- Step 5d: Detect non-finite numeric-clamp columns (T11782) -----------
816
+ //
817
+ // Log which columns have a numeric clamp rule (Inf/-Inf/NaN → finite) so the
818
+ // recovery of otherwise-dropped rows (e.g. brain_weight_history.delta_weight)
819
+ // is traceable in the migration journal.
820
+ const clampedCols = sharedColumns.filter((col) => NUMERIC_CLAMPS.has(`${targetTableName}.${col}`));
821
+ if (clampedCols.length > 0) {
822
+ log.info({ legacyTableName, targetTableName, sourceName, clampedCols }, `Exodus: applying non-finite numeric clamp for ${clampedCols.length} column(s) (T11782)`);
823
+ }
770
824
  // --- Step 6: Build the SELECT expression list ---
771
825
  //
772
826
  // For each shared column, `buildSelectExpr` handles (priority order):
773
827
  // 1. Epoch→ISO coercion when target has ISO GLOB CHECK and source is INTEGER (T11546)
774
- // 2. Enum-value normalization for legacy values not in the consolidated CHECK (T11547)
775
- // 3. COALESCE for NOT NULL target columns without schema defaults (T11533)
776
- // 4. Plain column reference otherwise
828
+ // 2. Non-finite numeric clamp (Inf/-Inf/NaN finite in-range) (T11782)
829
+ // 3. Enum-value normalization for legacy values not in the consolidated CHECK (T11547)
830
+ // 4. COALESCE for NOT NULL target columns without schema defaults (T11533)
831
+ // 5. Plain column reference otherwise
777
832
  const selectExprs = sharedColumns.map((col) => {
778
833
  const srcType = srcTypeMap.get(col) ?? '';
779
834
  const tgtInfo = tgtColMap.get(col);
@@ -875,7 +930,7 @@ function checkSchemaVersion(journal, forceCrossVersion) {
875
930
  * @task T11531 (P0 attach-leak fix)
876
931
  */
877
932
  export async function runExodusMigrate(plan, forceCrossVersion = false, onProgress) {
878
- const { sources, stagingDir, diskPreflight, projectDbPath } = plan;
933
+ const { sources, stagingDir, diskPreflight, projectDbPath, globalDbPath } = plan;
879
934
  // AC8: disk pre-flight
880
935
  if (!diskPreflight) {
881
936
  return {
@@ -919,6 +974,17 @@ export async function runExodusMigrate(plan, forceCrossVersion = false, onProgre
919
974
  const backupPaths = [];
920
975
  const allTableResults = [];
921
976
  const lockedPaths = [];
977
+ // FIX D (T11782): the migrate engine opens the consolidated TARGET DBs on a
978
+ // DEDICATED, NON-cached connection (a second SQLite handle to the same file —
979
+ // WAL allows it) rather than the cached caller handle. The copy AND the
980
+ // parity-abort rollback operate ONLY on these dedicated connections, so a
981
+ // concurrent caller INSERT (`tasks.add`) on its OWN cached connection is
982
+ // physically OUTSIDE this migration's transaction and CANNOT be rolled back
983
+ // with an aborted migration. The handles are closed in the `finally` below to
984
+ // avoid a file-descriptor leak (a dedicated handle is never evicted by the
985
+ // singleton cache).
986
+ let projectHandle = null;
987
+ let globalHandle = null;
922
988
  try {
923
989
  // 1. Back up existing source DBs into staging dir and acquire advisory locks
924
990
  for (const src of sources) {
@@ -934,13 +1000,17 @@ export async function runExodusMigrate(plan, forceCrossVersion = false, onProgre
934
1000
  acquireAdvisoryLock(src.path);
935
1001
  lockedPaths.push(src.path);
936
1002
  }
937
- // 2. Open (or create) the consolidated target DBs via the chokepoint.
938
- // This runs Drizzle migrations to create the target schema.
939
- onProgress?.('Opening consolidated project-scope cleo.db (running migrations)…');
940
- // openDualScopeDb takes cwd, not a db path pass undefined to use process.cwd()
941
- const projectHandle = await openDualScopeDb('project');
942
- onProgress?.('Opening consolidated global-scope cleo.db (running migrations)…');
943
- const globalHandle = await openDualScopeDb('global');
1003
+ // 2. Open the consolidated target DBs on a DEDICATED connection via the
1004
+ // chokepoint (FIX D). This runs Drizzle migrations to create the target
1005
+ // schema on an isolated handle, NOT the cached caller handle.
1006
+ onProgress?.('Opening DEDICATED project-scope cleo.db connection (running migrations)…');
1007
+ projectHandle = await openDualScopeDbAtPath('project', projectDbPath, undefined, {
1008
+ dedicated: true,
1009
+ });
1010
+ onProgress?.('Opening DEDICATED global-scope cleo.db connection (running migrations)…');
1011
+ globalHandle = await openDualScopeDbAtPath('global', globalDbPath, undefined, {
1012
+ dedicated: true,
1013
+ });
944
1014
  // Extract the raw DatabaseSync from the Drizzle wrapper ($client pattern).
945
1015
  function extractNativeDb(handle) {
946
1016
  const drizzleHandle = handle.db;
@@ -969,8 +1039,6 @@ export async function runExodusMigrate(plan, forceCrossVersion = false, onProgre
969
1039
  // Final journal update
970
1040
  journal.updatedAt = new Date().toISOString();
971
1041
  writeJournal(stagingDir, journal);
972
- projectHandle.close();
973
- globalHandle.close();
974
1042
  return { ok: true, tables: allTableResults, stagingDir, backupPaths };
975
1043
  }
976
1044
  catch (err) {
@@ -979,6 +1047,21 @@ export async function runExodusMigrate(plan, forceCrossVersion = false, onProgre
979
1047
  return { ok: false, tables: allTableResults, stagingDir, backupPaths, error };
980
1048
  }
981
1049
  finally {
1050
+ // FIX D (T11782): always close the DEDICATED migrate connections (success OR
1051
+ // failure) so the second SQLite handle does not leak a file descriptor. A
1052
+ // dedicated handle is never cached, so this is the only thing that closes it.
1053
+ try {
1054
+ projectHandle?.close();
1055
+ }
1056
+ catch {
1057
+ // ignore double-close
1058
+ }
1059
+ try {
1060
+ globalHandle?.close();
1061
+ }
1062
+ catch {
1063
+ // ignore double-close
1064
+ }
982
1065
  // Release advisory locks
983
1066
  for (const p of lockedPaths) {
984
1067
  releaseAdvisoryLock(p);
@@ -1105,10 +1188,21 @@ async function migrateScope(scope, sources, targetNativeDb, journal, stagingDir,
1105
1188
  skipped = true;
1106
1189
  }
1107
1190
  else if (copyResult.reason) {
1108
- // No-swallow error: all rows dropped by a constraint (T11546).
1109
- // The table is NOT skipped (copy was attempted) but the result
1110
- // must be surfaced as an error, not a silent 0-row success.
1111
- status = 'skipped'; // Mark skipped so journal doesn't say "done" on 0 rows
1191
+ // No-swallow error: rows dropped by a constraint (T11546). The
1192
+ // copy WAS attempted (skipped stays false) and the deficit MUST be
1193
+ // surfaced never a silent 0-row "done". (T11782 · FIX C.)
1194
+ //
1195
+ // Record `partial` rather than `skipped`: the table is neither
1196
+ // intentionally excluded nor cleanly complete. `partial` keeps the
1197
+ // journal honest (a resume re-copies; it never masquerades as
1198
+ // `done`) WITHOUT, by itself, tripping a scope-wide rollback at
1199
+ // this layer. A genuine deficit on a data-bearing BASE table still
1200
+ // ABORTS the cutover downstream: the parity gate
1201
+ // (`isDataContinuityOk` via `verifyMigration`) compares row counts
1202
+ // and fails on any `targetCount < sourceCount` deficit regardless
1203
+ // of journal status. With FIX B (Inf clamp) this branch should
1204
+ // rarely fire — it is belt-and-suspenders.
1205
+ status = 'partial';
1112
1206
  errorMsg = copyResult.reason;
1113
1207
  // skipped stays false — the distinction is the reason field (data loss vs intentional skip)
1114
1208
  }