@cleocode/cleo 2026.4.82 → 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.
|
|
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/cant": "2026.4.
|
|
33
|
-
"@cleocode/contracts": "2026.4.
|
|
34
|
-
"@cleocode/
|
|
35
|
-
"@cleocode/
|
|
36
|
-
"@cleocode/caamp": "2026.4.
|
|
37
|
-
"@cleocode/nexus": "2026.4.
|
|
38
|
-
"@cleocode/runtime": "2026.4.
|
|
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"
|