@cleocode/core 2026.6.5 → 2026.6.6

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 (51) hide show
  1. package/dist/docs/export-document.js +189 -137
  2. package/dist/docs/export-document.js.map +2 -2
  3. package/dist/llm/api-mode.d.ts +64 -0
  4. package/dist/llm/api-mode.d.ts.map +1 -0
  5. package/dist/llm/api-mode.js +72 -0
  6. package/dist/llm/api-mode.js.map +1 -0
  7. package/dist/llm/api.d.ts.map +1 -1
  8. package/dist/llm/api.js +6 -37
  9. package/dist/llm/api.js.map +1 -1
  10. package/dist/llm/auxiliary-fallback.d.ts.map +1 -1
  11. package/dist/llm/auxiliary-fallback.js +24 -38
  12. package/dist/llm/auxiliary-fallback.js.map +1 -1
  13. package/dist/llm/model-runner.d.ts +127 -0
  14. package/dist/llm/model-runner.d.ts.map +1 -0
  15. package/dist/llm/model-runner.js +282 -0
  16. package/dist/llm/model-runner.js.map +1 -0
  17. package/dist/llm/plugin-facade.js +29391 -1473
  18. package/dist/llm/plugin-facade.js.map +3 -3
  19. package/dist/llm/role-executor.d.ts +6 -5
  20. package/dist/llm/role-executor.d.ts.map +1 -1
  21. package/dist/llm/role-executor.js +40 -86
  22. package/dist/llm/role-executor.js.map +1 -1
  23. package/dist/llm/role-resolver.d.ts +28 -1
  24. package/dist/llm/role-resolver.d.ts.map +1 -1
  25. package/dist/llm/role-resolver.js +10 -0
  26. package/dist/llm/role-resolver.js.map +1 -1
  27. package/dist/llm/session-factory.d.ts +4 -6
  28. package/dist/llm/session-factory.d.ts.map +1 -1
  29. package/dist/llm/session-factory.js +6 -72
  30. package/dist/llm/session-factory.js.map +1 -1
  31. package/dist/llm/system-resolver.d.ts.map +1 -1
  32. package/dist/llm/system-resolver.js +6 -0
  33. package/dist/llm/system-resolver.js.map +1 -1
  34. package/dist/llm/tool-loop.d.ts.map +1 -1
  35. package/dist/llm/tool-loop.js +9 -32
  36. package/dist/llm/tool-loop.js.map +1 -1
  37. package/dist/release/reconcile.d.ts +61 -0
  38. package/dist/release/reconcile.d.ts.map +1 -1
  39. package/dist/release/reconcile.js +187 -2
  40. package/dist/release/reconcile.js.map +1 -1
  41. package/dist/store/exodus/column-transforms.d.ts +248 -0
  42. package/dist/store/exodus/column-transforms.d.ts.map +1 -0
  43. package/dist/store/exodus/column-transforms.js +444 -0
  44. package/dist/store/exodus/column-transforms.js.map +1 -0
  45. package/dist/store/exodus/migrate.d.ts.map +1 -1
  46. package/dist/store/exodus/migrate.js +5 -267
  47. package/dist/store/exodus/migrate.js.map +1 -1
  48. package/dist/store/exodus/verify-migration.d.ts.map +1 -1
  49. package/dist/store/exodus/verify-migration.js +61 -3
  50. package/dist/store/exodus/verify-migration.js.map +1 -1
  51. package/package.json +12 -12
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Shared column-value transform layer for the exodus migration.
3
+ *
4
+ * The exodus copy (`migrate.ts`) does not move source values byte-for-byte into
5
+ * the consolidated `cleo.db`: a handful of columns are TRANSFORMED on the way in
6
+ * so they satisfy the consolidated schema's CHECK constraints (which the legacy
7
+ * runtime DBs did not carry). Three transform classes exist:
8
+ *
9
+ * 1. **Epoch INTEGER → ISO-8601 TEXT** ({@link buildEpochToIsoExpr}) — for any
10
+ * target column carrying an ISO-GLOB CHECK ({@link detectIsoGlobColumns}).
11
+ * A per-row magnitude heuristic ({@link EPOCH_SECONDS_THRESHOLD}) classifies
12
+ * seconds vs milliseconds.
13
+ * 2. **Legacy enum → canonical member** ({@link ENUM_NORMALIZATIONS} +
14
+ * {@link enumNormExpr}) — maps pre-tightening enum aliases (e.g.
15
+ * `tasks_commits.conventional_type` `'style'`/`'merge'` → `'chore'`).
16
+ * 3. **Non-finite REAL → finite** ({@link NUMERIC_CLAMPS} +
17
+ * {@link numericClampExpr}) — `Inf`/`-Inf`/`NaN` → a finite in-range value
18
+ * (`brain_weight_history.delta_weight`).
19
+ *
20
+ * ## Why this module exists (T11809 · AC2)
21
+ *
22
+ * Before T11809 these transforms lived ONLY in `migrate.ts`. The parity verifier
23
+ * (`verify-migration.ts`) digested RAW source values against the TRANSFORMED
24
+ * target values, so every coerced column (epoch INTEGER vs ISO TEXT, legacy enum
25
+ * vs canonical, Inf vs clamped) produced a `hashMatch === false` even on a
26
+ * perfectly lossless migration — a false-negative that aborted the cutover and
27
+ * lost the batch writes accumulated during the migrating open.
28
+ *
29
+ * Extracting the transform logic into ONE shared module lets the verifier digest
30
+ * the SOURCE side **through the same transforms migrate applied**
31
+ * ({@link buildDigestExpr}), so equal logical data digests equal. `migrate.ts`
32
+ * re-imports these primitives so its copy behaviour stays byte-identical; the
33
+ * verifier imports {@link buildDigestExpr} for the digest-oriented variant.
34
+ *
35
+ * @task T11809 (exodus verify applies source-side coercion — hashMatch on equal data)
36
+ * @task T11546 (epoch→ISO coercion — original)
37
+ * @task T11547 (enum normalization — original)
38
+ * @task T11782 (non-finite numeric clamp — original)
39
+ * @epic T11249 (E6)
40
+ * @saga T11242
41
+ */
42
+ // ---------------------------------------------------------------------------
43
+ // NOT NULL default-literal helper
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Determine a safe SQL literal default for a NOT NULL column with no schema
47
+ * default, given its SQLite type affinity.
48
+ *
49
+ * Used by `migrate.ts` to coalesce NULL source values for target-only NOT NULL
50
+ * columns so that rows are not silently dropped by `INSERT OR IGNORE` when a
51
+ * source value is NULL (T11533 ROOT CAUSE 2 fix).
52
+ *
53
+ * @param colType - Raw `type` string from `PRAGMA table_info` (e.g. `"INTEGER"`,
54
+ * `"TEXT"`, `"REAL"`, `"BLOB"`, or compound forms like `"text NOT NULL"`).
55
+ * @returns A SQL literal string suitable for embedding in a `COALESCE()` call.
56
+ */
57
+ export function typeDefaultLiteral(colType) {
58
+ const upper = colType.toUpperCase();
59
+ if (upper.includes('INT'))
60
+ return '0';
61
+ if (upper.includes('REAL') || upper.includes('FLOAT') || upper.includes('DOUBLE'))
62
+ return '0.0';
63
+ if (upper.includes('BLOB'))
64
+ return "x''";
65
+ // TEXT and any other affinity (SQLite permissive) → empty string
66
+ return "''";
67
+ }
68
+ /**
69
+ * Per-(targetTable, column) normalization rules that map legacy enum values to
70
+ * the canonical enum accepted by the consolidated schema CHECK constraints.
71
+ *
72
+ * Each entry is a function that, given the `srcRef` SQL expression for the
73
+ * column, returns a SQL CASE expression that produces the canonical value.
74
+ * Rows with already-canonical values pass through unchanged (the ELSE branch).
75
+ *
76
+ * ## Brain enum normalizations REMOVED (T11647)
77
+ *
78
+ * The brain memory family no longer participates in enum normalization. Its
79
+ * consolidated exodus target now matches the LEGACY RUNTIME shape, which carries
80
+ * NO SQL CHECK constraints (the `text({ enum })` unions are enforced at the
81
+ * application layer only). With no CHECK to satisfy, coercing brain enum values
82
+ * would be data corruption, not a fix — so every brain enum value is copied
83
+ * VERBATIM. The historical brain rules (`brain_observations.{source_type,type}`,
84
+ * `brain_decisions.{confirmation_state,decision_category,confidence,outcome,
85
+ * decided_by}`) were deleted. The TASKS-domain rules below remain because those
86
+ * consolidated tables keep their CHECK constraints.
87
+ *
88
+ * ## nexus/signaldock enum-drift audit (T11809 · AC1)
89
+ *
90
+ * A real-data audit of `nexus.db` (106 MB) and `signaldock.db` (280 KB) against
91
+ * the consolidated CHECK enums found ZERO out-of-enum values for every
92
+ * CHECK-constrained column that has source data: `nexus_nodes.kind` (24,482
93
+ * rows), `nexus_relations.type` (39,163 rows), `nexus_contracts.type` (0 rows),
94
+ * `nexus_sigils.role` (8 rows), `agent_registry_agents.status` (19 rows),
95
+ * `agent_registry_users.role` (0 rows). No new normalization entry was required.
96
+ * The reported "nexus/signaldock drop rows" symptom was in fact the AC2 verify
97
+ * false-negative (epoch→ISO coercion makes the source digest differ from the
98
+ * target digest), which fixing {@link buildDigestExpr} resolves — NOT a CHECK
99
+ * drop. See the T11809 return notes for the full per-column row counts.
100
+ *
101
+ * Lookup key: `${targetTable}.${column}` (lowercase, dotted).
102
+ *
103
+ * @since T11547 (P0 data-loss fix)
104
+ * @since T11548 (P0 final enum coverage)
105
+ * @since T11647 (brain target = runtime shape — brain enum coercions removed)
106
+ */
107
+ export const ENUM_NORMALIZATIONS = new Map([
108
+ // --- task_commits.link_source -------------------------------------------
109
+ // 'commit-message' → 'commit-subject' (pre-T9506 legacy value)
110
+ [
111
+ 'tasks_task_commits.link_source',
112
+ (src) => `CASE ${src} WHEN 'commit-message' THEN 'commit-subject' ELSE ${src} END`,
113
+ ],
114
+ // --- architecture_decisions.status (case + date-suffix normalization) ----
115
+ // 'Accepted', 'ACCEPTED', 'approved', 'Accepted (2026-04-18)', … → 'accepted'
116
+ // 'Proposed', 'PROPOSED' → 'proposed'
117
+ // 'Superseded', 'SUPERSEDED' → 'superseded'
118
+ [
119
+ 'tasks_architecture_decisions.status',
120
+ (src) => `CASE` +
121
+ ` WHEN lower(${src}) = 'accepted' OR lower(${src}) LIKE 'accepted %' OR lower(${src}) = 'approved' THEN 'accepted'` +
122
+ ` WHEN lower(${src}) = 'proposed' THEN 'proposed'` +
123
+ ` WHEN lower(${src}) = 'superseded' THEN 'superseded'` +
124
+ ` WHEN lower(${src}) = 'deprecated' THEN 'deprecated'` +
125
+ ` ELSE ${src}` +
126
+ ` END`,
127
+ ],
128
+ // --- brain_* enum normalizations REMOVED (T11647) -----------------------
129
+ // The brain memory family now lands in the consolidated cleo.db in its LEGACY
130
+ // RUNTIME shape — INTEGER epoch timestamps and, critically, NO SQL CHECK
131
+ // constraints (the `text({ enum })` unions are enforced only at the
132
+ // application layer, exactly as the runtime `drizzle-brain` tables are). With
133
+ // no brain CHECK constraint to satisfy, exodus MUST copy every brain enum
134
+ // value VERBATIM — coercing them (e.g. source_type 'observer-compressed'/
135
+ // 'sleep-consolidation' → 'agent', type 'observation'/'proposal'/'pattern' →
136
+ // nearest) would now be unnecessary data CORRUPTION, not a constraint fix.
137
+ // The previous brain entries (brain_observations.{source_type,type},
138
+ // brain_decisions.{confirmation_state,decision_category,confidence,outcome,
139
+ // decided_by}) are therefore deleted. The non-brain entries below still apply
140
+ // because those consolidated tables retain their CHECK constraints.
141
+ // --- tasks_token_usage.transport (T11548 → REMOVED T11649) ---------------
142
+ // NO normalization. 'mcp' is a first-class transport origin (MCP-gateway
143
+ // requests) and is preserved verbatim. The consolidated CHECK enum was WIDENED
144
+ // to include 'mcp' (canonical TOKEN_USAGE_TRANSPORTS SSoT + forward migration
145
+ // 20260602000002_t11649-token-usage-transport-mcp), so the value lands without
146
+ // coercion. The earlier 'mcp' → 'agent' mapping was a silent semantic alteration
147
+ // of ~194 rows (count-preserving, NOT integrity-preserving) — see T11649.
148
+ // (brain_decisions.{decision_category,confidence} normalizations removed —
149
+ // T11647: brain target = runtime shape with no CHECK; copy values verbatim.)
150
+ // --- tasks_commits.conventional_type (T11548 + T11578) -------------------
151
+ // The consolidated CHECK enum is feat/fix/chore/docs/refactor/test/build/ci/
152
+ // perf/revert/breaking. Real git history carries non-conventional subjects:
153
+ // - 'style' → 'chore' (pre-T11548 mapping; no 'style' in enum).
154
+ // - 'merge'/'release' → 'chore' (T11578): merge + release commits are
155
+ // maintenance-class; the precise semantic is preserved by the dedicated
156
+ // `is_merge_commit` / `is_release_commit` boolean columns, so collapsing
157
+ // `conventional_type` to the maintenance catch-all 'chore' is lossless at
158
+ // the row grain. Without this the 'merge'/'release' rows violate the CHECK,
159
+ // `INSERT OR IGNORE` drops the WHOLE commits table, and the exodus-on-open
160
+ // data-continuity gate aborts the cutover (T11578 CI regression).
161
+ // - any OTHER out-of-enum value → 'chore' (defensive: future non-conventional
162
+ // subjects must never re-break the zero-deficit gate; the boolean flags and
163
+ // raw subject text remain the precise provenance).
164
+ [
165
+ 'tasks_commits.conventional_type',
166
+ (src) => `CASE` +
167
+ ` WHEN ${src} IS NULL THEN NULL` +
168
+ ` WHEN ${src} IN ('feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'build', 'ci', 'perf', 'revert', 'breaking') THEN ${src}` +
169
+ ` ELSE 'chore'` +
170
+ ` END`,
171
+ ],
172
+ // --- tasks_task_relations.relation_type (T11548) -------------------------
173
+ // 'grouped-by' → 'groups' (enum: related/blocks/duplicates/absorbs/fixes/extends/
174
+ // supersedes/groups). 4 rows.
175
+ [
176
+ 'tasks_task_relations.relation_type',
177
+ (src) => `CASE ${src} WHEN 'grouped-by' THEN 'groups' ELSE ${src} END`,
178
+ ],
179
+ // --- tasks_lifecycle_stages.stage_name (T11548) --------------------------
180
+ // Legacy camelCase / past-tense values → canonical snake_case stage names.
181
+ // 'implemented' → 'implementation', 'qaPassed' → 'validation',
182
+ // 'testsPassed' → 'testing'. 3 rows.
183
+ [
184
+ 'tasks_lifecycle_stages.stage_name',
185
+ (src) => `CASE ${src}` +
186
+ ` WHEN 'implemented' THEN 'implementation'` +
187
+ ` WHEN 'qaPassed' THEN 'validation'` +
188
+ ` WHEN 'testsPassed' THEN 'testing'` +
189
+ ` ELSE ${src}` +
190
+ ` END`,
191
+ ],
192
+ // --- tasks_architecture_decisions.gate_status (T11548) ------------------
193
+ // 'passed (T5313 consensus)' → 'passed', 'approved' → 'passed'
194
+ // (enum: pending/passed/failed/waived). 2 rows.
195
+ [
196
+ 'tasks_architecture_decisions.gate_status',
197
+ (src) => `CASE` +
198
+ ` WHEN ${src} LIKE 'passed%' THEN 'passed'` +
199
+ ` WHEN ${src} = 'approved' THEN 'passed'` +
200
+ ` ELSE ${src}` +
201
+ ` END`,
202
+ ],
203
+ // --- tasks_evidence_ac_bindings.binding_type (T11548) -------------------
204
+ // Values with a 'validator:...' prefix → 'direct'
205
+ // (enum: direct/satisfies/coverage). 3 rows.
206
+ // Strip the namespace prefix introduced before the enum was tightened.
207
+ [
208
+ 'tasks_evidence_ac_bindings.binding_type',
209
+ (src) => `CASE` + ` WHEN ${src} LIKE 'validator:%' THEN 'direct'` + ` ELSE ${src}` + ` END`,
210
+ ],
211
+ // (brain_decisions.{outcome,decided_by} normalizations removed — T11647:
212
+ // brain target = runtime shape with no CHECK; legacy values like 'accepted',
213
+ // 'rejected', 'prime' now survive VERBATIM instead of being coerced.)
214
+ ]);
215
+ /**
216
+ * Return a SQL CASE expression that normalises legacy enum values for `col` in
217
+ * `targetTableName` to the canonical values accepted by the consolidated CHECK,
218
+ * or return `null` when no normalization rule exists for this (table, column).
219
+ *
220
+ * @param targetTableName - Physical consolidated target table name.
221
+ * @param col - Column name.
222
+ * @param srcRef - SQL expression referencing the source column.
223
+ * @returns A SQL CASE expression string, or `null` if no rule applies.
224
+ */
225
+ export function enumNormExpr(targetTableName, col, srcRef) {
226
+ const key = `${targetTableName}.${col}`;
227
+ const fn = ENUM_NORMALIZATIONS.get(key);
228
+ return fn ? fn(srcRef) : null;
229
+ }
230
+ /**
231
+ * Per-(targetTable, column) numeric-clamp rules that coerce non-finite legacy
232
+ * REAL values (`Inf` / `-Inf` / `NaN`) to a finite in-range value so the row is
233
+ * NOT silently dropped by `INSERT OR IGNORE`.
234
+ *
235
+ * ## Why this exists (T11782)
236
+ *
237
+ * 188,926 of 697,780 legacy `brain_weight_history` rows carry
238
+ * `delta_weight = Inf`/`-Inf` (the R-STDP plasticity writer saturated the delta
239
+ * before the value was clamped at write time). SQLite stores ±Inf as the IEEE-754
240
+ * float, but the consolidated `brain_weight_history.delta_weight` column is a
241
+ * plain `real NOT NULL` with NO CHECK — so a verbatim copy would land the Inf
242
+ * value. The historical deficit, however, was that those rows tripped a constraint
243
+ * elsewhere in the copy chain and `INSERT OR IGNORE` dropped them, yielding a
244
+ * deficit that fired the parity-gate abort. Clamping the non-finite value to a
245
+ * finite member of the column's domain guarantees every row lands.
246
+ *
247
+ * The clamp mirrors the {@link ENUM_NORMALIZATIONS} shape: each entry is a
248
+ * function `(srcRef) => CASE …`. Finite values pass through unchanged via the
249
+ * ELSE branch. The `col != col` self-comparison is the canonical SQL NaN guard
250
+ * (NaN is the only value not equal to itself); `9e999` is the SQLite literal that
251
+ * evaluates to `+Infinity` (and `-9e999` to `-Infinity`).
252
+ *
253
+ * Lookup key: `${targetTable}.${column}` (lowercase, dotted).
254
+ *
255
+ * @since T11782 (P0 — brain_weight_history Inf recovery)
256
+ */
257
+ export const NUMERIC_CLAMPS = new Map([
258
+ // --- brain_weight_history.delta_weight (T11782) -------------------------
259
+ // +Inf → 1.0 (max canonical reinforcement), -Inf → -1.0 (max canonical
260
+ // depression), NaN → 0.0 (no-op delta). Finite values pass through.
261
+ [
262
+ 'brain_weight_history.delta_weight',
263
+ (src) => `CASE` +
264
+ ` WHEN ${src} = 9e999 THEN 1.0` +
265
+ ` WHEN ${src} = -9e999 THEN -1.0` +
266
+ ` WHEN ${src} != ${src} THEN 0.0` +
267
+ ` ELSE ${src}` +
268
+ ` END`,
269
+ ],
270
+ ]);
271
+ /**
272
+ * Return a SQL CASE expression that clamps non-finite legacy REAL values for
273
+ * `col` in `targetTableName` to a finite in-range value, or `null` when no
274
+ * clamp rule exists for this (table, column).
275
+ *
276
+ * @param targetTableName - Physical consolidated target table name.
277
+ * @param col - Column name.
278
+ * @param srcRef - SQL expression referencing the source column.
279
+ * @returns A SQL CASE expression string, or `null` if no rule applies.
280
+ */
281
+ export function numericClampExpr(targetTableName, col, srcRef) {
282
+ const key = `${targetTableName}.${col}`;
283
+ const fn = NUMERIC_CLAMPS.get(key);
284
+ return fn ? fn(srcRef) : null;
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Epoch-to-ISO coercion layer (ROOT CAUSE 1 fix — T11546)
288
+ // ---------------------------------------------------------------------------
289
+ /**
290
+ * Regex to detect ISO GLOB CHECK constraints in DDL SQL.
291
+ * Matches: `CHECK ("colname" IS NULL OR "colname" GLOB '[0-9]...')`
292
+ * Uses `\[0-9` to match the literal `[0-9` at the start of the GLOB pattern.
293
+ */
294
+ const ISO_CHECK_REGEX = /CHECK\s*\(\s*"([^"]+)"\s+IS\s+NULL\s+OR\s+"[^"]+"\s+GLOB\s+'\[0-9/gi;
295
+ /**
296
+ * Magnitude threshold distinguishing epoch SECONDS from epoch MILLISECONDS.
297
+ *
298
+ * A Unix epoch value for years 2020–2100 is roughly 1.6e9 – 4.1e9 seconds,
299
+ * or 1.6e12 – 4.1e12 milliseconds. The safe boundary is 1e11 (100 billion):
300
+ * any value BELOW 1e11 is in seconds (even year 2100 seconds ≈ 4.1e9 < 1e11);
301
+ * any value AT OR ABOVE 1e11 is in milliseconds (year 2020 ms ≈ 1.6e12 > 1e11).
302
+ *
303
+ * This constant is embedded directly in the generated SQL CASE expression so
304
+ * it is evaluated per-row — each row's epoch is classified independently.
305
+ */
306
+ export const EPOCH_SECONDS_THRESHOLD = 100_000_000_000; // 1e11
307
+ /**
308
+ * Build a SQL expression that converts an INTEGER epoch column to ISO-8601 TEXT,
309
+ * automatically detecting whether the stored value is in seconds or milliseconds
310
+ * using a magnitude heuristic (T11549 correctness fix).
311
+ *
312
+ * ## Heuristic
313
+ *
314
+ * A per-row CASE checks whether the column value is below {@link EPOCH_SECONDS_THRESHOLD}
315
+ * (100 billion). If so, the value is treated as seconds and passed directly to
316
+ * `strftime(..., 'unixepoch')`. If at or above the threshold, it is divided by
317
+ * 1000.0 first (milliseconds → seconds).
318
+ *
319
+ * This replaces the previous per-source heuristic which failed when individual
320
+ * columns within a source DB used a different epoch unit than the majority of that
321
+ * source's columns. The specific bug: `nexus.user_profile.{first_observed_at,
322
+ * last_reinforced_at}` stores SECONDS (value ≈ 1.78e9) but the nexus source was
323
+ * labeled `milliseconds`, causing these values to be divided by 1000 and converted
324
+ * to a 1970 date.
325
+ *
326
+ * ## NULL handling
327
+ *
328
+ * A NULL source value is preserved as NULL so it passes the `IS NULL` branch of
329
+ * the ISO GLOB CHECK constraint on the target column.
330
+ *
331
+ * @param srcRef - SQL expression referencing the source column value.
332
+ * @returns A SQL CASE expression producing an ISO-8601 TEXT timestamp.
333
+ */
334
+ export function buildEpochToIsoExpr(srcRef) {
335
+ return (`CASE` +
336
+ ` WHEN ${srcRef} IS NULL THEN NULL` +
337
+ ` WHEN ${srcRef} < ${EPOCH_SECONDS_THRESHOLD}` +
338
+ ` THEN strftime('%Y-%m-%dT%H:%M:%fZ', ${srcRef}, 'unixepoch')` +
339
+ ` ELSE strftime('%Y-%m-%dT%H:%M:%fZ', ${srcRef}/1000.0, 'unixepoch')` +
340
+ ` END`);
341
+ }
342
+ /**
343
+ * Parse the DDL for a given table from `sqlite_master` and return the set of
344
+ * column names that have an ISO GLOB CHECK constraint.
345
+ *
346
+ * Reads the raw DDL text and uses a regex to extract column names appearing in
347
+ * `CHECK ("colname" IS NULL OR "colname" GLOB '[0-9]...')` patterns. This is
348
+ * robust to Drizzle's generated CHECK format (all CHECK constraints generated
349
+ * by T11363 follow this exact pattern).
350
+ *
351
+ * @param db - Target DB with the consolidated schema.
352
+ * @param tableName - Physical table name (consolidated, e.g. `conduit_messages`).
353
+ * @param targetSchema - Schema name the target table lives in (`'main'`, or an
354
+ * ATTACH alias for cross-scope routing — ADR-090 nexus graph residency, T11539).
355
+ * @returns Set of column names that require ISO GLOB validation.
356
+ */
357
+ export function detectIsoGlobColumns(db, tableName, targetSchema = 'main') {
358
+ const escapedTable = tableName.replace(/'/g, "''");
359
+ const row = db
360
+ .prepare(`SELECT sql FROM "${targetSchema}".sqlite_master WHERE type='table' AND name='${escapedTable}'`)
361
+ .get();
362
+ if (!row?.sql)
363
+ return new Set();
364
+ const isoColumns = new Set();
365
+ // Pattern: CHECK ("colname" IS NULL OR "colname" GLOB '[0-9]...')
366
+ // The column name appears TWICE — we capture the first occurrence.
367
+ // Use matchAll to avoid the biome no-assign-in-expressions rule.
368
+ ISO_CHECK_REGEX.lastIndex = 0; // reset before reuse (global regex stateful)
369
+ for (const match of row.sql.matchAll(ISO_CHECK_REGEX)) {
370
+ isoColumns.add(match[1]);
371
+ }
372
+ return isoColumns;
373
+ }
374
+ // ---------------------------------------------------------------------------
375
+ // Digest-oriented transform expression (T11809 · AC2)
376
+ // ---------------------------------------------------------------------------
377
+ /**
378
+ * Return `true` when the source column's type affinity is INTEGER-like, so an
379
+ * epoch→ISO coercion applies when the matching target column carries an ISO GLOB
380
+ * CHECK. Empty affinity and `NUMERIC` are treated as integer-like (matching the
381
+ * historical `buildSelectExpr` behaviour — legacy epoch columns sometimes carry
382
+ * no declared type or a `NUMERIC` affinity).
383
+ *
384
+ * @param srcType - Raw `type` string from the source `PRAGMA table_info`.
385
+ * @returns `true` if the source column should be considered an INTEGER epoch.
386
+ */
387
+ function isIntegerSourceType(srcType) {
388
+ const upper = srcType.toUpperCase();
389
+ return upper.includes('INT') || upper === '' || upper === 'NUMERIC';
390
+ }
391
+ /**
392
+ * Build the SQL expression a column's SOURCE value must pass through so it
393
+ * matches the canonical value the migration STORES in the target — for use by
394
+ * the parity verifier's content digest (T11809 · AC2).
395
+ *
396
+ * This is the digest-oriented sibling of migrate's `buildSelectExpr`. It applies
397
+ * the SAME value transforms the migration actually performs — and ONLY those:
398
+ *
399
+ * 1. **Epoch→ISO-8601** ({@link buildEpochToIsoExpr}) — when the target has an
400
+ * ISO GLOB CHECK and the source column is INTEGER-typed.
401
+ * 2. **Non-finite numeric clamp** ({@link numericClampExpr}).
402
+ * 3. **Enum-value normalization** ({@link enumNormExpr}).
403
+ * 4. **Plain column reference** otherwise.
404
+ *
405
+ * Crucially it does NOT add the NOT-NULL `COALESCE(..., type_default)` wrapping
406
+ * that `buildSelectExpr` uses: that wrapping only ever fires on a NULL source
407
+ * value that the target stores as a type-default, which is a NULL→default value
408
+ * CHANGE the digest should not paper over (a genuine NULL→'' divergence remains a
409
+ * real, visible content difference, not a coercion artifact). The verifier
410
+ * intentionally omits it so the digest reflects the canonical value transforms
411
+ * (epoch/enum/clamp) WITHOUT masking a true NULL→default substitution.
412
+ *
413
+ * The returned expression is a bare SQL value expression (no `AS "col"` alias)
414
+ * suitable for embedding directly in a `SELECT <expr> ... ORDER BY ...` digest
415
+ * query. When no transform applies, a plain quoted column reference is returned.
416
+ *
417
+ * @param targetTableName - Physical consolidated target table name (transform
418
+ * lookup key).
419
+ * @param col - Column name (present in BOTH source and target).
420
+ * @param srcType - Raw `type` string from the source `PRAGMA table_info`.
421
+ * @param isoGlobCols - Set of target columns carrying an ISO GLOB CHECK.
422
+ * @returns A SQL value expression that maps the raw source value to the canonical
423
+ * value the target stores.
424
+ */
425
+ export function buildDigestExpr(targetTableName, col, srcType, isoGlobCols) {
426
+ const srcRef = `"${col}"`;
427
+ // Priority 1: Epoch→ISO coercion — applies when the target has an ISO GLOB
428
+ // CHECK and the source column is INTEGER (epoch) typed. Mirrors migrate's
429
+ // per-row magnitude heuristic exactly.
430
+ if (isoGlobCols.has(col) && isIntegerSourceType(srcType)) {
431
+ return buildEpochToIsoExpr(srcRef);
432
+ }
433
+ // Priority 2: Non-finite numeric clamp (Inf/-Inf/NaN → finite in-range).
434
+ const clampExpr = numericClampExpr(targetTableName, col, srcRef);
435
+ if (clampExpr !== null)
436
+ return clampExpr;
437
+ // Priority 3: Enum-value normalization (legacy value → canonical member).
438
+ const normExpr = enumNormExpr(targetTableName, col, srcRef);
439
+ if (normExpr !== null)
440
+ return normExpr;
441
+ // Priority 4: plain column reference (no transform migrate would have applied).
442
+ return srcRef;
443
+ }
444
+ //# sourceMappingURL=column-transforms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-transforms.js","sourceRoot":"","sources":["../../../src/store/exodus/column-transforms.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/store/exodus/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgHG;AA4BH,OAAO,KAAK,EAEV,mBAAmB,EACnB,UAAU,EAMX,MAAM,YAAY,CAAC;AAiEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAiB9D;AAqkBD;;;;;;;;;;;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"}