loomrail 0.1.0-alpha.1

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 (38) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +266 -0
  4. package/apps/cli/dist/index.js +11154 -0
  5. package/apps/cli/migrations/0001_initial.sql +99 -0
  6. package/apps/cli/migrations/0002_mock_workflow.sql +169 -0
  7. package/apps/cli/migrations/0003_budget_pause_recovery.sql +154 -0
  8. package/apps/cli/migrations/0004_pipeline_started_budget_backfill.sql +72 -0
  9. package/apps/cli/migrations/0005_acceptance_evidence.sql +129 -0
  10. package/apps/cli/migrations/0006_session_handoff.sql +166 -0
  11. package/apps/cli/migrations/0007_pack_share_backoff.sql +11 -0
  12. package/apps/cli/migrations/0008_stage_attempt_counter_backfill.sql +169 -0
  13. package/apps/cli/migrations/0009_session_window_occupancy.sql +44 -0
  14. package/apps/cli/migrations/0010_provider_session_pid.sql +5 -0
  15. package/apps/cli/migrations/0011_work_item_workspaces.sql +94 -0
  16. package/apps/cli/migrations/0012_registered_repository_projects.sql +54 -0
  17. package/apps/cli/migrations/0013_stage_attempt_result_tree.sql +166 -0
  18. package/apps/cli/migrations/0014_live_evidence_provider.sql +49 -0
  19. package/apps/web/dist/assets/index-DjwvCSSY.js +5 -0
  20. package/apps/web/dist/assets/index-DjwvCSSY.js.map +1 -0
  21. package/apps/web/dist/assets/index-Z88TqCIf.css +1 -0
  22. package/apps/web/dist/assets/radix-vendor-CRdHXIPR.js +42 -0
  23. package/apps/web/dist/assets/radix-vendor-CRdHXIPR.js.map +1 -0
  24. package/apps/web/dist/assets/react-vendor-BicOiEU0.js +10 -0
  25. package/apps/web/dist/assets/react-vendor-BicOiEU0.js.map +1 -0
  26. package/apps/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  27. package/apps/web/dist/assets/tanstack-vendor-ec08LVZ_.js +2 -0
  28. package/apps/web/dist/assets/tanstack-vendor-ec08LVZ_.js.map +1 -0
  29. package/apps/web/dist/assets/validation-vendor-CGrOSWd3.js +65 -0
  30. package/apps/web/dist/assets/validation-vendor-CGrOSWd3.js.map +1 -0
  31. package/apps/web/dist/favicon.svg +5 -0
  32. package/apps/web/dist/index.html +60 -0
  33. package/apps/web/dist/theme-bootstrap.js +46 -0
  34. package/fixtures/projects/api-service-b/README.md +3 -0
  35. package/fixtures/projects/api-service-b/loomrail-fixture.json +6 -0
  36. package/fixtures/projects/web-app-a/README.md +3 -0
  37. package/fixtures/projects/web-app-a/loomrail-fixture.json +6 -0
  38. package/package.json +46 -0
@@ -0,0 +1,166 @@
1
+ -- Spec docs/plans/15-e1-5-change-visibility-spec.ru.md D3, §4, §6 step 5 and §12.3.
2
+ --
3
+ -- The tree a stage's worktree held when the stage succeeded. Nothing in E1.5 reads it: spec §11
4
+ -- puts the per-stage breakdown among the milestone's non-goals, so this is precomputed for the
5
+ -- milestone that closes the second half of GD-002 (checkpoint commits, squash and the Conventional
6
+ -- Commit message), which requires GD-001. `stageAttemptSchema.resultTree` in @loomrail/contracts
7
+ -- carries the full statement: which milestone, what will read it, that the milestone has no ID in
8
+ -- the approved order yet, and the measured limit on what a reader arriving later can do with the
9
+ -- value (`git write-tree` leaves the tree UNREACHABLE, so `git gc` prunes it after
10
+ -- `gc.pruneExpire`; equality between two labels survives that, diffing from one does not).
11
+ --
12
+ -- Nullable with no default, because `null` is a fact rather than a gap: no tree was measured for
13
+ -- this stage. Every StageAttempt recorded before this migration stays null forever (spec §12.3),
14
+ -- and so does one whose stage never ran in a worktree or whose worktree was gone when the label was
15
+ -- taken. A stage that ran and changed NOTHING is not null -- it records the tree it started on --
16
+ -- so the two can never be confused.
17
+ --
18
+ -- The CHECK is the same shape assertion `treeShaSchema` makes at the contract boundary, restated
19
+ -- here because the database is written by more than the one code path and a garbled sha is worth
20
+ -- refusing at the moment of the write rather than at the moment of the read.
21
+
22
+ ALTER TABLE stage_attempts
23
+ ADD COLUMN result_tree TEXT
24
+ CHECK (result_tree IS NULL OR (length(result_tree) = 40 AND result_tree NOT GLOB '*[^0-9a-f]*'));
25
+
26
+ -- The rest of this migration is the history half, and it is the same repair 0008 performed for
27
+ -- `unproductive_sessions` and `pack_share_backoffs`: the column above gives every stored ROW a
28
+ -- correct value, but `stageAttemptSchema` is `.strict()` and the entity is embedded verbatim in the
29
+ -- payloads of PIPELINE_STARTED, STAGE_ATTEMPT_CHANGED, PIPELINE_PAUSED, PIPELINE_RESUMED,
30
+ -- PIPELINE_CANCELLED, BUDGET_OVERRIDE_APPROVED, RECOVERY_REPORT_CREATED, ACCEPTANCE_REQUESTED,
31
+ -- ACCEPTANCE_RESOLVED and PIPELINE_COMPLETED. Left alone, every such payload written before this
32
+ -- milestone fails `domainEventSchema.parse` inside `eventFromRow`, which makes the whole activity
33
+ -- timeline and the events endpoint unreadable -- and `commands.result_json` is replayed through
34
+ -- `stateCommandResultSchema`, so a receipt written before this milestone fails its replay the same
35
+ -- way. No test starting from an empty database can see either, which is exactly why 0008 exists.
36
+ --
37
+ -- Structure follows 0008 statement for statement, including the reasons behind its less obvious
38
+ -- choices, which all still hold: the paths cannot be spelled out literally (a StageAttempt appears
39
+ -- at the top level of a command result AND inside `$.events[i].data`, and the events array's length
40
+ -- varies by command type), so occurrences are found with json_tree and folded over with a recursive
41
+ -- CTE, one json_insert each; adding an object key never shifts an array index, so the fold order
42
+ -- does not matter; `$.stage IS NOT NULL` narrows the match to actual StageAttempts rather than
43
+ -- anything that happens to reuse the key name; and the occurrence list is materialised into an
44
+ -- indexed TEMP TABLE because a non-materialised CTE is re-evaluated per fold step, which made the
45
+ -- pass quadratic in history size (measured in 0008 at 89s for 2000 events plus 2000 receipts).
46
+ --
47
+ -- One difference from 0008 worth naming: the value inserted is JSON null rather than 0, and
48
+ -- `json_insert` with an SQL NULL writes JSON null (probed). The guard `json_type(...) IS NULL`
49
+ -- therefore still means "the key is absent" after a pass -- `json_type` answers the string 'null'
50
+ -- for a key present and null -- so the pass stays idempotent, and a re-run finds nothing to do.
51
+
52
+ DROP TRIGGER events_are_append_only_update;
53
+
54
+ CREATE TEMP TABLE legacy_event_result_tree (
55
+ sequence INTEGER NOT NULL,
56
+ path TEXT NOT NULL,
57
+ n INTEGER NOT NULL
58
+ );
59
+
60
+ INSERT INTO legacy_event_result_tree (sequence, path, n)
61
+ SELECT events.sequence, tree.fullkey,
62
+ ROW_NUMBER() OVER (PARTITION BY events.sequence ORDER BY tree.fullkey)
63
+ FROM events, json_tree(events.data_json) AS tree
64
+ WHERE tree.key IN ('stageAttempt', 'previousStageAttempt')
65
+ AND tree.type = 'object'
66
+ AND json_type(tree.value, '$.stage') IS NOT NULL
67
+ AND json_type(tree.value, '$.resultTree') IS NULL;
68
+
69
+ CREATE INDEX legacy_event_result_tree_fold_idx ON legacy_event_result_tree (sequence, n);
70
+
71
+ CREATE TEMP TABLE backfilled_event (
72
+ sequence INTEGER PRIMARY KEY,
73
+ data_json TEXT NOT NULL
74
+ );
75
+
76
+ INSERT INTO backfilled_event (sequence, data_json)
77
+ WITH RECURSIVE
78
+ backfilled(sequence, n, data_json) AS (
79
+ SELECT events.sequence, 0, events.data_json
80
+ FROM events
81
+ WHERE events.sequence IN (SELECT sequence FROM legacy_event_result_tree)
82
+ UNION ALL
83
+ SELECT backfilled.sequence, legacy_event_result_tree.n,
84
+ json_insert(backfilled.data_json, legacy_event_result_tree.path || '.resultTree', NULL)
85
+ FROM backfilled
86
+ JOIN legacy_event_result_tree
87
+ ON legacy_event_result_tree.sequence = backfilled.sequence
88
+ AND legacy_event_result_tree.n = backfilled.n + 1
89
+ )
90
+ SELECT backfilled.sequence, backfilled.data_json
91
+ FROM backfilled
92
+ JOIN (SELECT sequence, MAX(n) AS n FROM legacy_event_result_tree GROUP BY sequence) AS folded
93
+ ON folded.sequence = backfilled.sequence
94
+ AND folded.n = backfilled.n;
95
+
96
+ UPDATE events
97
+ SET data_json = (SELECT data_json FROM backfilled_event WHERE backfilled_event.sequence = events.sequence)
98
+ WHERE events.sequence IN (SELECT sequence FROM backfilled_event);
99
+
100
+ DROP TABLE backfilled_event;
101
+ DROP TABLE legacy_event_result_tree;
102
+
103
+ CREATE TRIGGER events_are_append_only_update
104
+ BEFORE UPDATE ON events
105
+ BEGIN
106
+ SELECT RAISE(ABORT, 'events are append-only');
107
+ END;
108
+
109
+ DROP TRIGGER commands_are_append_only_update;
110
+
111
+ CREATE TEMP TABLE legacy_command_result_tree (
112
+ command_id TEXT NOT NULL,
113
+ path TEXT NOT NULL,
114
+ n INTEGER NOT NULL
115
+ );
116
+
117
+ INSERT INTO legacy_command_result_tree (command_id, path, n)
118
+ SELECT commands.command_id, tree.fullkey,
119
+ ROW_NUMBER() OVER (PARTITION BY commands.command_id ORDER BY tree.fullkey)
120
+ FROM commands, json_tree(commands.result_json) AS tree
121
+ WHERE tree.key IN ('stageAttempt', 'previousStageAttempt')
122
+ AND tree.type = 'object'
123
+ AND json_type(tree.value, '$.stage') IS NOT NULL
124
+ AND json_type(tree.value, '$.resultTree') IS NULL;
125
+
126
+ CREATE INDEX legacy_command_result_tree_fold_idx ON legacy_command_result_tree (command_id, n);
127
+
128
+ CREATE TEMP TABLE backfilled_command (
129
+ command_id TEXT PRIMARY KEY,
130
+ result_json TEXT NOT NULL
131
+ );
132
+
133
+ INSERT INTO backfilled_command (command_id, result_json)
134
+ WITH RECURSIVE
135
+ backfilled(command_id, n, result_json) AS (
136
+ SELECT commands.command_id, 0, commands.result_json
137
+ FROM commands
138
+ WHERE commands.command_id IN (SELECT command_id FROM legacy_command_result_tree)
139
+ UNION ALL
140
+ SELECT backfilled.command_id, legacy_command_result_tree.n,
141
+ json_insert(backfilled.result_json, legacy_command_result_tree.path || '.resultTree', NULL)
142
+ FROM backfilled
143
+ JOIN legacy_command_result_tree
144
+ ON legacy_command_result_tree.command_id = backfilled.command_id
145
+ AND legacy_command_result_tree.n = backfilled.n + 1
146
+ )
147
+ SELECT backfilled.command_id, backfilled.result_json
148
+ FROM backfilled
149
+ JOIN (SELECT command_id, MAX(n) AS n FROM legacy_command_result_tree GROUP BY command_id) AS folded
150
+ ON folded.command_id = backfilled.command_id
151
+ AND folded.n = backfilled.n;
152
+
153
+ UPDATE commands
154
+ SET result_json = (
155
+ SELECT result_json FROM backfilled_command WHERE backfilled_command.command_id = commands.command_id
156
+ )
157
+ WHERE commands.command_id IN (SELECT command_id FROM backfilled_command);
158
+
159
+ DROP TABLE backfilled_command;
160
+ DROP TABLE legacy_command_result_tree;
161
+
162
+ CREATE TRIGGER commands_are_append_only_update
163
+ BEFORE UPDATE ON commands
164
+ BEGIN
165
+ SELECT RAISE(ABORT, 'commands are append-only');
166
+ END;
@@ -0,0 +1,49 @@
1
+ DROP TRIGGER evidence_artifacts_are_append_only_update;
2
+ DROP TRIGGER evidence_artifacts_are_append_only_delete;
3
+ DROP INDEX evidence_artifacts_run_created_idx;
4
+
5
+ ALTER TABLE evidence_artifacts RENAME TO evidence_artifacts_v13;
6
+
7
+ CREATE TABLE evidence_artifacts (
8
+ id TEXT PRIMARY KEY,
9
+ schema_version INTEGER NOT NULL CHECK (schema_version = 1),
10
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
11
+ work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE RESTRICT,
12
+ pipeline_run_id TEXT NOT NULL REFERENCES pipeline_runs(id) ON DELETE RESTRICT,
13
+ stage_attempt_id TEXT NOT NULL REFERENCES stage_attempts(id) ON DELETE RESTRICT,
14
+ stage TEXT NOT NULL CHECK (stage IN ('REVIEW', 'QA')),
15
+ kind TEXT NOT NULL CHECK (kind IN ('REVIEW_REPORT', 'QA_REPORT')),
16
+ status TEXT NOT NULL CHECK (status = 'PASSED'),
17
+ provider TEXT NOT NULL CHECK (provider IN ('MOCK', 'CODEX', 'CLAUDE_CODE')),
18
+ title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 200),
19
+ summary TEXT NOT NULL CHECK (length(summary) BETWEEN 1 AND 4000),
20
+ checks_json TEXT NOT NULL CHECK (json_valid(checks_json)),
21
+ created_at TEXT NOT NULL,
22
+ UNIQUE (pipeline_run_id, kind)
23
+ ) STRICT;
24
+
25
+ INSERT INTO evidence_artifacts (
26
+ id, schema_version, project_id, work_item_id, pipeline_run_id, stage_attempt_id,
27
+ stage, kind, status, provider, title, summary, checks_json, created_at
28
+ )
29
+ SELECT
30
+ id, schema_version, project_id, work_item_id, pipeline_run_id, stage_attempt_id,
31
+ stage, kind, status, provider, title, summary, checks_json, created_at
32
+ FROM evidence_artifacts_v13;
33
+
34
+ DROP TABLE evidence_artifacts_v13;
35
+
36
+ CREATE INDEX evidence_artifacts_run_created_idx
37
+ ON evidence_artifacts(pipeline_run_id, created_at, id);
38
+
39
+ CREATE TRIGGER evidence_artifacts_are_append_only_update
40
+ BEFORE UPDATE ON evidence_artifacts
41
+ BEGIN
42
+ SELECT RAISE(ABORT, 'evidence artifacts are append-only');
43
+ END;
44
+
45
+ CREATE TRIGGER evidence_artifacts_are_append_only_delete
46
+ BEFORE DELETE ON evidence_artifacts
47
+ BEGIN
48
+ SELECT RAISE(ABORT, 'evidence artifacts are append-only');
49
+ END;