@cleocode/cleo 2026.4.80 → 2026.4.83

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.
@@ -0,0 +1,185 @@
1
+ -- T877: Pipeline stage invariants — structural fix replacing the two TS
2
+ -- backfills (T869 pipeline-stage-from-lifecycle, T871 terminal-stage).
3
+ --
4
+ -- ==========================================================================
5
+ -- Problem
6
+ -- ==========================================================================
7
+ -- Two one-shot TS backfills were living in packages/core/src/lifecycle/:
8
+ -- * backfill-pipeline-stage.ts — syncs `tasks.pipeline_stage` with the
9
+ -- highest `lifecycle_stages.stage_name` for each task (pre-T832 drift).
10
+ -- * backfill-terminal-pipeline-stage.ts — sets `pipeline_stage` to
11
+ -- 'contribution' or 'cancelled' for terminal status rows (pre-T871).
12
+ --
13
+ -- Both are band-aids for real structural invariants the database itself was
14
+ -- not enforcing. This migration moves them into the migration stream AND
15
+ -- adds triggers so the invariants hold going forward.
16
+ --
17
+ -- ==========================================================================
18
+ -- Solution
19
+ -- ==========================================================================
20
+ -- 1. SQL-native one-shot data fix: run the same logic the TS backfills did,
21
+ -- but as UPDATEs inside this migration so drizzle's journal marks it
22
+ -- done atomically. No more "did the backfill run?" question — if this
23
+ -- migration is in __drizzle_migrations, the data is consistent.
24
+ --
25
+ -- 2. SQLite triggers enforce the invariants on every INSERT/UPDATE to
26
+ -- `tasks`. These are the structural equivalent of CHECK constraints but
27
+ -- without requiring a table rebuild (SQLite cannot add CHECK to an
28
+ -- existing table non-destructively). Triggers RAISE ABORT on violation.
29
+ --
30
+ -- Invariants enforced:
31
+ -- A. status='done' -> pipeline_stage IN ('contribution','cancelled')
32
+ -- B. status='cancelled' -> pipeline_stage='cancelled'
33
+ --
34
+ -- These match the runtime behaviour in packages/core/src/tasks/{complete,cancel-ops}.ts
35
+ -- (T871) so legitimate writes are never blocked.
36
+ --
37
+ -- @task T877
38
+ -- @epic T876 (owner-labelled T900)
39
+
40
+ -- --------------------------------------------------------------------------
41
+ -- Part 1: Data fix — terminal-stage alignment (replaces T871 backfill)
42
+ -- --------------------------------------------------------------------------
43
+ -- For every status='done' row whose pipeline_stage is NULL or a non-terminal
44
+ -- intermediate stage, set pipeline_stage='contribution'.
45
+ UPDATE `tasks`
46
+ SET `pipeline_stage` = 'contribution',
47
+ `updated_at` = datetime('now')
48
+ WHERE `status` = 'done'
49
+ AND (
50
+ `pipeline_stage` IS NULL
51
+ OR `pipeline_stage` IN (
52
+ 'research','consensus','architecture_decision','specification',
53
+ 'decomposition','implementation','validation','testing','release'
54
+ )
55
+ );
56
+ --> statement-breakpoint
57
+
58
+ -- For every status='cancelled' row whose pipeline_stage is NULL or a non-terminal
59
+ -- intermediate stage, set pipeline_stage='cancelled'.
60
+ UPDATE `tasks`
61
+ SET `pipeline_stage` = 'cancelled',
62
+ `updated_at` = datetime('now')
63
+ WHERE `status` = 'cancelled'
64
+ AND (
65
+ `pipeline_stage` IS NULL
66
+ OR `pipeline_stage` IN (
67
+ 'research','consensus','architecture_decision','specification',
68
+ 'decomposition','implementation','validation','testing','release',
69
+ 'contribution'
70
+ )
71
+ );
72
+ --> statement-breakpoint
73
+
74
+ -- --------------------------------------------------------------------------
75
+ -- Part 2: Data fix — lifecycle → pipeline_stage sync (replaces T869 backfill)
76
+ -- --------------------------------------------------------------------------
77
+ -- For every task whose highest completed/in_progress/skipped lifecycle_stages
78
+ -- row sits AHEAD of its current pipeline_stage, advance pipeline_stage to
79
+ -- that stage. Matches `backfillPipelineStageFromLifecycle` exactly.
80
+ --
81
+ -- Stage order (1-based) is expressed via CASE so we do not need a helper
82
+ -- table. Only tasks still in intermediate stages are touched — terminal
83
+ -- rows handled above.
84
+ UPDATE `tasks`
85
+ SET `pipeline_stage` = (
86
+ SELECT highest.stage_name
87
+ FROM (
88
+ SELECT
89
+ lp.task_id AS task_id,
90
+ ls.stage_name AS stage_name,
91
+ ls.sequence AS sequence
92
+ FROM `lifecycle_pipelines` lp
93
+ JOIN `lifecycle_stages` ls ON ls.pipeline_id = lp.id
94
+ WHERE ls.status IN ('completed','in_progress','skipped')
95
+ ) highest
96
+ WHERE highest.task_id = tasks.id
97
+ ORDER BY highest.sequence DESC
98
+ LIMIT 1
99
+ ),
100
+ `updated_at` = datetime('now')
101
+ WHERE `tasks`.`id` IN (
102
+ SELECT lp.task_id
103
+ FROM `lifecycle_pipelines` lp
104
+ JOIN `lifecycle_stages` ls ON ls.pipeline_id = lp.id
105
+ WHERE ls.status IN ('completed','in_progress','skipped')
106
+ GROUP BY lp.task_id
107
+ )
108
+ AND (
109
+ `tasks`.`pipeline_stage` IS NULL
110
+ OR (
111
+ -- only advance when the lifecycle stage is strictly ahead
112
+ (SELECT MAX(ls.sequence)
113
+ FROM `lifecycle_pipelines` lp
114
+ JOIN `lifecycle_stages` ls ON ls.pipeline_id = lp.id
115
+ WHERE lp.task_id = tasks.id
116
+ AND ls.status IN ('completed','in_progress','skipped'))
117
+ >
118
+ (CASE `tasks`.`pipeline_stage`
119
+ WHEN 'research' THEN 1
120
+ WHEN 'consensus' THEN 2
121
+ WHEN 'architecture_decision' THEN 3
122
+ WHEN 'specification' THEN 4
123
+ WHEN 'decomposition' THEN 5
124
+ WHEN 'implementation' THEN 6
125
+ WHEN 'validation' THEN 7
126
+ WHEN 'testing' THEN 8
127
+ WHEN 'release' THEN 9
128
+ WHEN 'contribution' THEN 10
129
+ WHEN 'cancelled' THEN 11
130
+ ELSE 0
131
+ END)
132
+ )
133
+ )
134
+ -- Never overwrite a terminal stage with an intermediate one.
135
+ AND (`tasks`.`pipeline_stage` IS NULL
136
+ OR `tasks`.`pipeline_stage` NOT IN ('contribution','cancelled'));
137
+ --> statement-breakpoint
138
+
139
+ -- --------------------------------------------------------------------------
140
+ -- Part 3: Triggers — enforce invariants on every INSERT/UPDATE
141
+ -- --------------------------------------------------------------------------
142
+ -- Invariant A: status='done' requires pipeline_stage IN ('contribution','cancelled').
143
+ -- Invariant B: status='cancelled' requires pipeline_stage='cancelled'.
144
+ --
145
+ -- We use triggers (not CHECK) because SQLite cannot add a CHECK constraint
146
+ -- to an existing column without a full table rebuild. Triggers are cheaper
147
+ -- to add and let us produce a clear error message on violation.
148
+ --
149
+ -- Drop-then-create makes the migration idempotent if replayed.
150
+ DROP TRIGGER IF EXISTS `trg_tasks_status_pipeline_insert`;
151
+ --> statement-breakpoint
152
+ DROP TRIGGER IF EXISTS `trg_tasks_status_pipeline_update`;
153
+ --> statement-breakpoint
154
+
155
+ CREATE TRIGGER `trg_tasks_status_pipeline_insert`
156
+ BEFORE INSERT ON `tasks`
157
+ FOR EACH ROW
158
+ WHEN (NEW.`status` = 'done' AND (NEW.`pipeline_stage` IS NULL OR NEW.`pipeline_stage` NOT IN ('contribution','cancelled')))
159
+ OR (NEW.`status` = 'cancelled' AND (NEW.`pipeline_stage` IS NULL OR NEW.`pipeline_stage` != 'cancelled'))
160
+ BEGIN
161
+ SELECT RAISE(ABORT, 'T877_INVARIANT_VIOLATION: status/pipeline_stage mismatch. status=done requires pipeline_stage IN (contribution,cancelled); status=cancelled requires pipeline_stage=cancelled.');
162
+ END;
163
+ --> statement-breakpoint
164
+
165
+ CREATE TRIGGER `trg_tasks_status_pipeline_update`
166
+ BEFORE UPDATE OF `status`, `pipeline_stage` ON `tasks`
167
+ FOR EACH ROW
168
+ WHEN (NEW.`status` = 'done' AND (NEW.`pipeline_stage` IS NULL OR NEW.`pipeline_stage` NOT IN ('contribution','cancelled')))
169
+ OR (NEW.`status` = 'cancelled' AND (NEW.`pipeline_stage` IS NULL OR NEW.`pipeline_stage` != 'cancelled'))
170
+ BEGIN
171
+ SELECT RAISE(ABORT, 'T877_INVARIANT_VIOLATION: status/pipeline_stage mismatch. status=done requires pipeline_stage IN (contribution,cancelled); status=cancelled requires pipeline_stage=cancelled.');
172
+ END;
173
+ --> statement-breakpoint
174
+
175
+ -- --------------------------------------------------------------------------
176
+ -- Part 4: Record migration in schema_meta for historical audit (optional)
177
+ -- --------------------------------------------------------------------------
178
+ -- Keep schema_meta keys from the old TS backfills populated so any code that
179
+ -- still checks `isTerminalPipelineStageBackfillDone()` / `isPipelineStageBackfillDone()`
180
+ -- continues to return true. This makes removal of the TS files safe in any
181
+ -- order and lets us retire them without breaking in-flight callers.
182
+ INSERT INTO `schema_meta` (`key`, `value`) VALUES
183
+ ('backfill:pipeline-stage-from-lifecycle', '{"ranAt":"migration:T877","task":"T877","replacedBy":"T877_migration"}'),
184
+ ('backfill:terminal-pipeline-stage', '{"ranAt":"migration:T877","task":"T877","replacedBy":"T877_migration"}')
185
+ ON CONFLICT(`key`) DO UPDATE SET `value` = excluded.`value`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/cleo",
3
- "version": "2026.4.80",
3
+ "version": "2026.4.83",
4
4
  "description": "CLEO CLI — the assembled product consuming @cleocode/core",
5
5
  "type": "module",
6
6
  "main": "./dist/cli/index.js",
@@ -29,13 +29,13 @@
29
29
  "tree-sitter-ruby": "^0.23.1",
30
30
  "tree-sitter-rust": "0.23.1",
31
31
  "tree-sitter-typescript": "^0.23.2",
32
- "@cleocode/caamp": "2026.4.80",
33
- "@cleocode/contracts": "2026.4.80",
34
- "@cleocode/core": "2026.4.80",
35
- "@cleocode/cant": "2026.4.80",
36
- "@cleocode/lafs": "2026.4.80",
37
- "@cleocode/runtime": "2026.4.80",
38
- "@cleocode/nexus": "2026.4.80"
32
+ "@cleocode/cant": "2026.4.83",
33
+ "@cleocode/contracts": "2026.4.83",
34
+ "@cleocode/lafs": "2026.4.83",
35
+ "@cleocode/core": "2026.4.83",
36
+ "@cleocode/caamp": "2026.4.83",
37
+ "@cleocode/nexus": "2026.4.83",
38
+ "@cleocode/runtime": "2026.4.83"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=24.0.0"