@dsh-enhanced/plugin-control-plane 0.1.31 → 0.1.34

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 (199) hide show
  1. package/README.md +352 -4
  2. package/bin/dsh-local-release-adapter.js +80 -16
  3. package/bin/dsh-npm-registry-adapter.js +613 -0
  4. package/bin/dsh-source-adoption-authority.js +12 -0
  5. package/bin/dsh-source-approval-authority.js +16 -0
  6. package/bin/dsh-source-release-authority.js +12 -0
  7. package/bin/dsh-systemd-host-attestor.js +707 -0
  8. package/bin/dsh-task-observation-authority.js +15 -0
  9. package/lib/adapter-process.d.ts +23 -0
  10. package/lib/adapter-process.d.ts.map +1 -0
  11. package/lib/adapter-process.js +305 -0
  12. package/lib/adapter-process.js.map +1 -0
  13. package/lib/adoption-coordinator.d.ts +63 -0
  14. package/lib/adoption-coordinator.d.ts.map +1 -0
  15. package/lib/adoption-coordinator.js +241 -0
  16. package/lib/adoption-coordinator.js.map +1 -0
  17. package/lib/adoption-handoff.d.ts +16 -0
  18. package/lib/adoption-handoff.d.ts.map +1 -0
  19. package/lib/adoption-handoff.js +11 -0
  20. package/lib/adoption-handoff.js.map +1 -0
  21. package/lib/attestation.d.ts.map +1 -1
  22. package/lib/attestation.js +49 -1
  23. package/lib/attestation.js.map +1 -1
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +29 -2
  26. package/lib/catalog.js.map +1 -1
  27. package/lib/cli.d.ts +33 -9
  28. package/lib/cli.d.ts.map +1 -1
  29. package/lib/cli.js +624 -178
  30. package/lib/cli.js.map +1 -1
  31. package/lib/deployment-readiness.d.ts +38 -0
  32. package/lib/deployment-readiness.d.ts.map +1 -0
  33. package/lib/deployment-readiness.js +154 -0
  34. package/lib/deployment-readiness.js.map +1 -0
  35. package/lib/effect-blocked-replay.d.ts +81 -0
  36. package/lib/effect-blocked-replay.d.ts.map +1 -0
  37. package/lib/effect-blocked-replay.js +259 -0
  38. package/lib/effect-blocked-replay.js.map +1 -0
  39. package/lib/errors.d.ts +6 -0
  40. package/lib/errors.d.ts.map +1 -0
  41. package/lib/errors.js +9 -0
  42. package/lib/errors.js.map +1 -0
  43. package/lib/foreground-deployment-runtime.d.ts +20 -0
  44. package/lib/foreground-deployment-runtime.d.ts.map +1 -0
  45. package/lib/foreground-deployment-runtime.js +79 -0
  46. package/lib/foreground-deployment-runtime.js.map +1 -0
  47. package/lib/foreground-deployment.d.ts +17 -0
  48. package/lib/foreground-deployment.d.ts.map +1 -0
  49. package/lib/foreground-deployment.js +43 -0
  50. package/lib/foreground-deployment.js.map +1 -0
  51. package/lib/host-attestor.d.ts +1 -1
  52. package/lib/host-attestor.d.ts.map +1 -1
  53. package/lib/host-attestor.js +51 -57
  54. package/lib/host-attestor.js.map +1 -1
  55. package/lib/index.d.ts +25 -2
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +11 -2
  58. package/lib/index.js.map +1 -1
  59. package/lib/npm-publish.d.ts +25 -0
  60. package/lib/npm-publish.d.ts.map +1 -0
  61. package/lib/npm-publish.js +280 -0
  62. package/lib/npm-publish.js.map +1 -0
  63. package/lib/owner-task-gap-types.d.ts +21 -0
  64. package/lib/owner-task-gap-types.d.ts.map +1 -0
  65. package/lib/owner-task-gap-types.js +2 -0
  66. package/lib/owner-task-gap-types.js.map +1 -0
  67. package/lib/owner-task-gaps.d.ts +24 -0
  68. package/lib/owner-task-gaps.d.ts.map +1 -0
  69. package/lib/owner-task-gaps.js +74 -0
  70. package/lib/owner-task-gaps.js.map +1 -0
  71. package/lib/post-activation.d.ts +33 -0
  72. package/lib/post-activation.d.ts.map +1 -0
  73. package/lib/post-activation.js +219 -0
  74. package/lib/post-activation.js.map +1 -0
  75. package/lib/registry-fetch.d.ts +50 -0
  76. package/lib/registry-fetch.d.ts.map +1 -0
  77. package/lib/registry-fetch.js +267 -0
  78. package/lib/registry-fetch.js.map +1 -0
  79. package/lib/release.d.ts +4 -4
  80. package/lib/release.d.ts.map +1 -1
  81. package/lib/release.js +142 -56
  82. package/lib/release.js.map +1 -1
  83. package/lib/replay-endpoint-protocol.d.ts +65 -0
  84. package/lib/replay-endpoint-protocol.d.ts.map +1 -0
  85. package/lib/replay-endpoint-protocol.js +183 -0
  86. package/lib/replay-endpoint-protocol.js.map +1 -0
  87. package/lib/replay-endpoint.d.ts +7 -0
  88. package/lib/replay-endpoint.d.ts.map +1 -0
  89. package/lib/replay-endpoint.js +193 -0
  90. package/lib/replay-endpoint.js.map +1 -0
  91. package/lib/replay-grant.d.ts +62 -0
  92. package/lib/replay-grant.d.ts.map +1 -0
  93. package/lib/replay-grant.js +208 -0
  94. package/lib/replay-grant.js.map +1 -0
  95. package/lib/replay-journal.d.ts +25 -0
  96. package/lib/replay-journal.d.ts.map +1 -0
  97. package/lib/replay-journal.js +275 -0
  98. package/lib/replay-journal.js.map +1 -0
  99. package/lib/runtime-observer-protocol.d.ts +63 -0
  100. package/lib/runtime-observer-protocol.d.ts.map +1 -0
  101. package/lib/runtime-observer-protocol.js +280 -0
  102. package/lib/runtime-observer-protocol.js.map +1 -0
  103. package/lib/runtime-observer.d.ts +8 -0
  104. package/lib/runtime-observer.d.ts.map +1 -0
  105. package/lib/runtime-observer.js +182 -0
  106. package/lib/runtime-observer.js.map +1 -0
  107. package/lib/service.d.ts +145 -1
  108. package/lib/service.d.ts.map +1 -1
  109. package/lib/service.js +734 -6
  110. package/lib/service.js.map +1 -1
  111. package/lib/source-adoption-authority.d.ts +65 -0
  112. package/lib/source-adoption-authority.d.ts.map +1 -0
  113. package/lib/source-adoption-authority.js +271 -0
  114. package/lib/source-adoption-authority.js.map +1 -0
  115. package/lib/source-adoption-runner.d.ts +28 -0
  116. package/lib/source-adoption-runner.d.ts.map +1 -0
  117. package/lib/source-adoption-runner.js +197 -0
  118. package/lib/source-adoption-runner.js.map +1 -0
  119. package/lib/source-approval-authority.d.ts +50 -0
  120. package/lib/source-approval-authority.d.ts.map +1 -0
  121. package/lib/source-approval-authority.js +412 -0
  122. package/lib/source-approval-authority.js.map +1 -0
  123. package/lib/source-approval-client.d.ts +32 -0
  124. package/lib/source-approval-client.d.ts.map +1 -0
  125. package/lib/source-approval-client.js +175 -0
  126. package/lib/source-approval-client.js.map +1 -0
  127. package/lib/source-build.d.ts +60 -0
  128. package/lib/source-build.d.ts.map +1 -0
  129. package/lib/source-build.js +311 -0
  130. package/lib/source-build.js.map +1 -0
  131. package/lib/source-context.d.ts +24 -0
  132. package/lib/source-context.d.ts.map +1 -0
  133. package/lib/source-context.js +99 -0
  134. package/lib/source-context.js.map +1 -0
  135. package/lib/source-job-types.d.ts +87 -0
  136. package/lib/source-job-types.d.ts.map +1 -0
  137. package/lib/source-job-types.js +2 -0
  138. package/lib/source-job-types.js.map +1 -0
  139. package/lib/source-jobs.d.ts +113 -0
  140. package/lib/source-jobs.d.ts.map +1 -0
  141. package/lib/source-jobs.js +535 -0
  142. package/lib/source-jobs.js.map +1 -0
  143. package/lib/source-release-authority.d.ts +35 -0
  144. package/lib/source-release-authority.d.ts.map +1 -0
  145. package/lib/source-release-authority.js +271 -0
  146. package/lib/source-release-authority.js.map +1 -0
  147. package/lib/source-release-client.d.ts +10 -0
  148. package/lib/source-release-client.d.ts.map +1 -0
  149. package/lib/source-release-client.js +24 -0
  150. package/lib/source-release-client.js.map +1 -0
  151. package/lib/source-release-runner.d.ts +31 -0
  152. package/lib/source-release-runner.d.ts.map +1 -0
  153. package/lib/source-release-runner.js +125 -0
  154. package/lib/source-release-runner.js.map +1 -0
  155. package/lib/source-versioning.d.ts +24 -0
  156. package/lib/source-versioning.d.ts.map +1 -0
  157. package/lib/source-versioning.js +213 -0
  158. package/lib/source-versioning.js.map +1 -0
  159. package/lib/source-workspace.d.ts +117 -0
  160. package/lib/source-workspace.d.ts.map +1 -0
  161. package/lib/source-workspace.js +458 -0
  162. package/lib/source-workspace.js.map +1 -0
  163. package/lib/sqlite.d.ts +1 -1
  164. package/lib/sqlite.d.ts.map +1 -1
  165. package/lib/sqlite.js +509 -7
  166. package/lib/sqlite.js.map +1 -1
  167. package/lib/store.d.ts +229 -6
  168. package/lib/store.d.ts.map +1 -1
  169. package/lib/store.js +2431 -383
  170. package/lib/store.js.map +1 -1
  171. package/lib/task-observation-authority.d.ts +29 -0
  172. package/lib/task-observation-authority.d.ts.map +1 -0
  173. package/lib/task-observation-authority.js +370 -0
  174. package/lib/task-observation-authority.js.map +1 -0
  175. package/lib/task-observation-client.d.ts +4 -0
  176. package/lib/task-observation-client.d.ts.map +1 -0
  177. package/lib/task-observation-client.js +20 -0
  178. package/lib/task-observation-client.js.map +1 -0
  179. package/lib/task-observation-runtime.d.ts +59 -0
  180. package/lib/task-observation-runtime.d.ts.map +1 -0
  181. package/lib/task-observation-runtime.js +331 -0
  182. package/lib/task-observation-runtime.js.map +1 -0
  183. package/lib/task-observation-store.d.ts +17 -0
  184. package/lib/task-observation-store.d.ts.map +1 -0
  185. package/lib/task-observation-store.js +115 -0
  186. package/lib/task-observation-store.js.map +1 -0
  187. package/lib/task-observation-types.d.ts +67 -0
  188. package/lib/task-observation-types.d.ts.map +1 -0
  189. package/lib/task-observation-types.js +2 -0
  190. package/lib/task-observation-types.js.map +1 -0
  191. package/lib/trust.d.ts +3 -0
  192. package/lib/trust.d.ts.map +1 -1
  193. package/lib/trust.js +0 -0
  194. package/lib/trust.js.map +1 -1
  195. package/lib/types.d.ts +275 -8
  196. package/lib/types.d.ts.map +1 -1
  197. package/lib/version.d.ts +1 -1
  198. package/lib/version.js +1 -1
  199. package/package.json +40 -5
package/lib/sqlite.js CHANGED
@@ -2,7 +2,41 @@ import { createHash } from 'node:crypto';
2
2
  import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync } from 'node:fs';
3
3
  import { dirname, isAbsolute } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
- export const controlPlaneSchemaVersion = 11;
5
+ export const controlPlaneSchemaVersion = 23;
6
+ const adoptionHandoffsSchema = `CREATE TABLE IF NOT EXISTS adoption_handoffs (
7
+ plan_id TEXT PRIMARY KEY REFERENCES activation_plans(id) ON DELETE RESTRICT, plan_digest TEXT NOT NULL CHECK(length(plan_digest)=64), coordinator_id TEXT NOT NULL,
8
+ created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER, CHECK(expires_at > created_at)
9
+ ) STRICT, WITHOUT ROWID;`;
10
+ const hostAttestationDispatchesSchema = `CREATE TABLE IF NOT EXISTS host_attestation_dispatches (
11
+ operation_id TEXT PRIMARY KEY REFERENCES host_attestation_operations(operation_id) ON DELETE RESTRICT,
12
+ status TEXT NOT NULL CHECK(status IN ('claimed','completed')),
13
+ claimed_at INTEGER NOT NULL, completed_at INTEGER,
14
+ CHECK((status = 'claimed' AND completed_at IS NULL) OR (status = 'completed' AND completed_at IS NOT NULL AND completed_at >= claimed_at))
15
+ ) STRICT, WITHOUT ROWID;`;
16
+ const taskObservationsSchema = `CREATE TABLE IF NOT EXISTS task_observation_batches (
17
+ id TEXT PRIMARY KEY, lane TEXT NOT NULL, plan_id TEXT NOT NULL REFERENCES activation_plans(id) ON DELETE RESTRICT,
18
+ batch_json TEXT NOT NULL CHECK(json_valid(batch_json) AND json_type(batch_json) = 'object'),
19
+ batch_digest TEXT NOT NULL CHECK(length(batch_digest) = 64),
20
+ state TEXT NOT NULL CHECK(state IN ('pending','signed','applied','stale')),
21
+ receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
22
+ receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64),
23
+ created_at INTEGER NOT NULL
24
+ ) STRICT, WITHOUT ROWID;
25
+ CREATE INDEX IF NOT EXISTS task_observation_batches_lane ON task_observation_batches(lane, created_at DESC);
26
+ CREATE UNIQUE INDEX IF NOT EXISTS task_observation_batches_open_lane ON task_observation_batches(lane) WHERE state IN ('pending','signed');`;
27
+ const foregroundDeploymentsSchema = `CREATE TABLE IF NOT EXISTS foreground_deployments (
28
+ inbox_id TEXT PRIMARY KEY,
29
+ plan_id TEXT NOT NULL REFERENCES activation_plans(id) ON DELETE RESTRICT,
30
+ record_json TEXT NOT NULL CHECK(json_valid(record_json) AND json_type(record_json) = 'object'),
31
+ record_digest TEXT NOT NULL CHECK(length(record_digest) = 64)
32
+ ) STRICT, WITHOUT ROWID;`;
33
+ const sourceAdoptionsSchema = `CREATE TABLE IF NOT EXISTS source_adoptions (
34
+ source_plan_id TEXT PRIMARY KEY REFERENCES source_plans(id) ON DELETE RESTRICT,
35
+ activation_plan_id TEXT NOT NULL UNIQUE REFERENCES activation_plans(id) ON DELETE RESTRICT,
36
+ binding_json TEXT NOT NULL CHECK(json_valid(binding_json) AND json_type(binding_json) = 'object'),
37
+ binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
38
+ created_at INTEGER NOT NULL
39
+ ) STRICT, WITHOUT ROWID;`;
6
40
  export function controlPlaneOperationReceiptDigest(idempotencyKey, operation, inputDigest, resultJson, createdAt) {
7
41
  return createHash('sha256').update(JSON.stringify([idempotencyKey, operation, inputDigest, resultJson, createdAt])).digest('hex');
8
42
  }
@@ -64,6 +98,13 @@ function createCurrent(database) {
64
98
  updated_at INTEGER NOT NULL
65
99
  ) STRICT;
66
100
 
101
+ CREATE TABLE owner_task_failure_gaps (
102
+ gap_id TEXT PRIMARY KEY REFERENCES capability_gaps(id) ON DELETE RESTRICT,
103
+ reference_json TEXT NOT NULL CHECK(json_valid(reference_json) AND json_type(reference_json) = 'object'),
104
+ reference_digest TEXT NOT NULL CHECK(length(reference_digest) = 64)
105
+ ) STRICT, WITHOUT ROWID;
106
+
107
+
67
108
  CREATE TABLE activation_plans (
68
109
  id TEXT PRIMARY KEY,
69
110
  plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
@@ -96,6 +137,9 @@ function createCurrent(database) {
96
137
  activation_fence INTEGER NOT NULL DEFAULT 0 CHECK(activation_fence >= 0),
97
138
  activation_lease_until INTEGER,
98
139
  activation_target_existed INTEGER CHECK(activation_target_existed IN (0, 1)),
140
+ activation_target_baseline_json TEXT CHECK(activation_target_baseline_json IS NULL OR (json_valid(activation_target_baseline_json) AND json_type(activation_target_baseline_json) = 'array')),
141
+ host_recovery_required INTEGER NOT NULL DEFAULT 0 CHECK(host_recovery_required IN (0, 1)),
142
+ rollback_profile_restored INTEGER NOT NULL DEFAULT 0 CHECK(rollback_profile_restored IN (0, 1)),
99
143
  failure_code TEXT,
100
144
  updated_at INTEGER NOT NULL,
101
145
  FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
@@ -119,8 +163,9 @@ function createCurrent(database) {
119
163
  plugin_name TEXT NOT NULL,
120
164
  generator_digest TEXT NOT NULL CHECK(length(generator_digest) = 64),
121
165
  scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'array'),
166
+ mode TEXT NOT NULL DEFAULT 'create' CHECK(mode IN ('create', 'modify')),
122
167
  status TEXT NOT NULL CHECK(status IN (
123
- 'pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed',
168
+ 'expired', 'pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed',
124
169
  'awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign', 'awaiting-publish',
125
170
  'awaiting-registry-verify', 'awaiting-catalog-admission', 'release-complete', 'release-failed', 'publish-ambiguous'
126
171
  )),
@@ -131,6 +176,8 @@ function createCurrent(database) {
131
176
  checked_tree_digest TEXT CHECK(checked_tree_digest IS NULL OR (length(checked_tree_digest) = 64 AND checked_tree_digest NOT GLOB '*[^a-f0-9]*')),
132
177
  checked_patch_digest TEXT CHECK(checked_patch_digest IS NULL OR (length(checked_patch_digest) = 64 AND checked_patch_digest NOT GLOB '*[^a-f0-9]*')),
133
178
  checked_at INTEGER,
179
+ prepared_evidence_json TEXT CHECK(prepared_evidence_json IS NULL OR
180
+ (json_valid(prepared_evidence_json) AND json_type(prepared_evidence_json) = 'object')),
134
181
  release_authorization_json TEXT CHECK(release_authorization_json IS NULL OR (json_valid(release_authorization_json) AND json_type(release_authorization_json) = 'object')),
135
182
  release_authorization_digest TEXT CHECK(release_authorization_digest IS NULL OR length(release_authorization_digest) = 64),
136
183
  release_id TEXT,
@@ -142,11 +189,51 @@ function createCurrent(database) {
142
189
  updated_at INTEGER NOT NULL,
143
190
  CHECK((checked_tree_digest IS NULL AND checked_patch_digest IS NULL AND checked_at IS NULL) OR
144
191
  (checked_tree_digest IS NOT NULL AND checked_patch_digest IS NOT NULL AND checked_at IS NOT NULL)),
192
+ CHECK(mode = 'create' OR
193
+ (checked_tree_digest IS NOT NULL AND checked_patch_digest IS NOT NULL AND checked_at IS NOT NULL)),
194
+ CHECK((mode = 'create' AND prepared_evidence_json IS NULL) OR
195
+ (mode = 'modify' AND prepared_evidence_json IS NOT NULL)),
145
196
  CHECK((release_authorization_json IS NULL AND release_authorization_digest IS NULL) OR
146
197
  (release_authorization_json IS NOT NULL AND release_authorization_digest IS NOT NULL)),
147
198
  FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
148
199
  ) STRICT;
149
200
 
201
+ CREATE TABLE IF NOT EXISTS source_job_authorities (
202
+ authority_id TEXT PRIMARY KEY,
203
+ authority_digest TEXT NOT NULL CHECK(length(authority_digest) = 64),
204
+ expires_at INTEGER NOT NULL,
205
+ max_submissions INTEGER NOT NULL CHECK(max_submissions >= 1),
206
+ submissions INTEGER NOT NULL CHECK(submissions >= 0 AND submissions <= max_submissions)
207
+ ) STRICT, WITHOUT ROWID;
208
+
209
+ CREATE TABLE IF NOT EXISTS source_jobs (
210
+ id TEXT PRIMARY KEY,
211
+ automation_id TEXT NOT NULL UNIQUE,
212
+ authority_id TEXT NOT NULL,
213
+ idempotency_key TEXT NOT NULL,
214
+ intent_json TEXT NOT NULL CHECK(json_valid(intent_json) AND json_type(intent_json) = 'object'),
215
+ intent_digest TEXT NOT NULL CHECK(length(intent_digest) = 64),
216
+ status TEXT NOT NULL CHECK(status IN ('queued', 'running', 'prepared', 'failed', 'unknown')),
217
+ revision INTEGER NOT NULL CHECK(revision >= 1),
218
+ created_at INTEGER NOT NULL,
219
+ expires_at INTEGER NOT NULL,
220
+ definition_hash TEXT,
221
+ occurrence_id TEXT,
222
+ plan_id TEXT UNIQUE,
223
+ failure_code TEXT,
224
+ updated_at INTEGER NOT NULL,
225
+ UNIQUE(authority_id, idempotency_key),
226
+ FOREIGN KEY(authority_id) REFERENCES source_job_authorities(authority_id) ON DELETE RESTRICT,
227
+ FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT,
228
+ CHECK((status = 'queued' AND occurrence_id IS NULL AND plan_id IS NULL AND failure_code IS NULL)
229
+ OR (status = 'running' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NULL AND failure_code IS NULL)
230
+ OR (status = 'prepared' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NOT NULL AND failure_code IS NULL)
231
+ OR (status = 'failed' AND plan_id IS NULL AND failure_code IS NOT NULL)
232
+ OR (status = 'unknown' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NULL AND failure_code IS NOT NULL))
233
+ ) STRICT;
234
+ CREATE UNIQUE INDEX IF NOT EXISTS source_jobs_single_active ON source_jobs((1)) WHERE status IN ('queued', 'running', 'unknown');
235
+ CREATE INDEX IF NOT EXISTS source_jobs_created ON source_jobs(created_at, id);
236
+
150
237
  CREATE TABLE gap_plan_claims (
151
238
  gap_id TEXT PRIMARY KEY,
152
239
  plan_id TEXT NOT NULL UNIQUE,
@@ -155,9 +242,9 @@ function createCurrent(database) {
155
242
  FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
156
243
  ) STRICT, WITHOUT ROWID;
157
244
 
158
- CREATE TABLE host_attestations (
245
+ CREATE TABLE IF NOT EXISTS host_attestations (
159
246
  plan_id TEXT NOT NULL,
160
- phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
247
+ phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health', 'rollback')),
161
248
  receipt_id TEXT NOT NULL UNIQUE,
162
249
  receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
163
250
  receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'),
@@ -167,9 +254,9 @@ function createCurrent(database) {
167
254
  FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
168
255
  ) STRICT, WITHOUT ROWID;
169
256
 
170
- CREATE TABLE host_attestation_operations (
257
+ CREATE TABLE IF NOT EXISTS host_attestation_operations (
171
258
  plan_id TEXT NOT NULL,
172
- phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health')),
259
+ phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health', 'rollback')),
173
260
  operation_id TEXT NOT NULL UNIQUE,
174
261
  binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64),
175
262
  request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
@@ -210,6 +297,26 @@ function createCurrent(database) {
210
297
  FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT
211
298
  ) STRICT, WITHOUT ROWID;
212
299
 
300
+ -- A release adapter can mutate an external registry. Its durable dispatch
301
+ -- claim is deliberately separate from the signed receipt: after a crash
302
+ -- between dispatch and receipt persistence, the outcome is unknown and
303
+ -- must be reconciled rather than executed again.
304
+ CREATE TABLE source_release_dispatches (
305
+ operation_id TEXT PRIMARY KEY REFERENCES source_release_operations(operation_id) ON DELETE RESTRICT,
306
+ status TEXT NOT NULL CHECK(status IN ('claimed', 'completed')),
307
+ claimed_at INTEGER NOT NULL,
308
+ completed_at INTEGER,
309
+ CHECK((status = 'claimed' AND completed_at IS NULL) OR
310
+ (status = 'completed' AND completed_at IS NOT NULL AND completed_at >= claimed_at))
311
+ ) STRICT, WITHOUT ROWID;
312
+
313
+ CREATE TABLE host_attestation_dispatches (
314
+ operation_id TEXT PRIMARY KEY REFERENCES host_attestation_operations(operation_id) ON DELETE RESTRICT,
315
+ status TEXT NOT NULL CHECK(status IN ('claimed', 'completed')),
316
+ claimed_at INTEGER NOT NULL, completed_at INTEGER,
317
+ CHECK((status = 'claimed' AND completed_at IS NULL) OR (status = 'completed' AND completed_at IS NOT NULL AND completed_at >= claimed_at))
318
+ ) STRICT, WITHOUT ROWID;
319
+
213
320
  CREATE TABLE operation_receipts (
214
321
  idempotency_key TEXT PRIMARY KEY,
215
322
  operation TEXT NOT NULL,
@@ -252,7 +359,75 @@ function createCurrent(database) {
252
359
  ) STRICT, WITHOUT ROWID;
253
360
  CREATE INDEX source_publish_reconciliations_release ON source_publish_reconciliations(plan_id, release_fence, created_at);
254
361
 
255
- PRAGMA user_version = 11;
362
+ -- Post-activation quality watch for a promoted plugin cohort. A watch is an
363
+ -- independent lifecycle from the activation state machine: the activated
364
+ -- state has no exit and its backup/stage are physically removed, so
365
+ -- post-promotion quality is tracked here against the exact immutable
366
+ -- package@version+integrity rather than by trying to re-open an activation.
367
+ CREATE TABLE activation_watch (
368
+ plan_id TEXT PRIMARY KEY,
369
+ package_name TEXT NOT NULL,
370
+ package_version TEXT NOT NULL,
371
+ package_integrity TEXT NOT NULL,
372
+ activation_id TEXT NOT NULL,
373
+ fence INTEGER NOT NULL CHECK(fence >= 1),
374
+ state TEXT NOT NULL CHECK(state IN ('watching', 'closed-regressed', 'closed-retracted')),
375
+ revision INTEGER NOT NULL CHECK(revision >= 1),
376
+ last_host_generation INTEGER NOT NULL DEFAULT 0 CHECK(last_host_generation >= 0),
377
+ healthy_observations INTEGER NOT NULL DEFAULT 0 CHECK(healthy_observations >= 0),
378
+ close_disposition TEXT CHECK(close_disposition IS NULL OR close_disposition IN ('regressed', 'retracted')),
379
+ close_at INTEGER,
380
+ close_evidence_id TEXT,
381
+ close_signature_digest TEXT CHECK(close_signature_digest IS NULL OR length(close_signature_digest) = 64),
382
+ started_at INTEGER NOT NULL,
383
+ updated_at INTEGER NOT NULL,
384
+ CHECK(
385
+ (state = 'watching' AND close_disposition IS NULL AND close_at IS NULL AND close_evidence_id IS NULL AND close_signature_digest IS NULL) OR
386
+ (state IN ('closed-regressed', 'closed-retracted') AND close_disposition IS NOT NULL AND close_at IS NOT NULL
387
+ AND close_evidence_id IS NOT NULL AND close_signature_digest IS NOT NULL)
388
+ ),
389
+ FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
390
+ ) STRICT, WITHOUT ROWID;
391
+
392
+ -- Append-only signed observations. Positive healthy rows are kept as
393
+ -- evidence but never close a watch; only a regressed Host observation or an
394
+ -- owner retraction transitions the watch to a terminal state.
395
+ CREATE TABLE activation_watch_evidence (
396
+ observation_id TEXT PRIMARY KEY,
397
+ plan_id TEXT NOT NULL,
398
+ disposition TEXT NOT NULL CHECK(disposition IN ('regressed', 'healthy', 'retracted')),
399
+ receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
400
+ signature_digest TEXT NOT NULL CHECK(length(signature_digest) = 64),
401
+ receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'),
402
+ host_generation INTEGER NOT NULL DEFAULT 0 CHECK(host_generation >= 0),
403
+ failures INTEGER NOT NULL DEFAULT 0 CHECK(failures >= 0),
404
+ checks INTEGER NOT NULL DEFAULT 0 CHECK(checks >= 0),
405
+ created_at INTEGER NOT NULL,
406
+ FOREIGN KEY(plan_id) REFERENCES activation_watch(plan_id) ON DELETE RESTRICT
407
+ ) STRICT, WITHOUT ROWID;
408
+ CREATE INDEX activation_watch_evidence_plan ON activation_watch_evidence(plan_id, created_at);
409
+
410
+ -- Core-file pins captured immediately before terminal promotion. They are
411
+ -- deliberately outside the immutable activation dossier: they describe the
412
+ -- Host-visible deployment and give a post-promotion rollback an ABA-safe
413
+ -- ordering without rewriting the approved plan.
414
+ CREATE TABLE activation_deployment_checkpoints (
415
+ plan_id TEXT PRIMARY KEY REFERENCES activation_plans(id) ON DELETE RESTRICT,
416
+ baseline_json TEXT NOT NULL CHECK(json_valid(baseline_json) AND json_type(baseline_json) = 'array'),
417
+ exposure_order INTEGER NOT NULL UNIQUE CHECK(exposure_order >= 1),
418
+ successful_order INTEGER UNIQUE CHECK(successful_order IS NULL OR successful_order = exposure_order),
419
+ recorded_at INTEGER NOT NULL,
420
+ succeeded_at INTEGER
421
+ ) STRICT, WITHOUT ROWID;
422
+ CREATE INDEX activation_deployment_checkpoints_success ON activation_deployment_checkpoints(successful_order)
423
+ WHERE successful_order IS NOT NULL;
424
+ CREATE TABLE activation_deployment_sequence (
425
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
426
+ next_exposure_order INTEGER NOT NULL CHECK(next_exposure_order >= 1)
427
+ ) STRICT, WITHOUT ROWID;
428
+ INSERT INTO activation_deployment_sequence (singleton, next_exposure_order) VALUES (1, 1);
429
+
430
+ PRAGMA user_version = 18;
256
431
  `);
257
432
  }
258
433
  function migrateV1(database) {
@@ -666,6 +841,290 @@ function migrateV10ToV11(database) {
666
841
  throw error;
667
842
  }
668
843
  }
844
+ function migrateV11ToV12(database) {
845
+ // IF NOT EXISTS makes the migration idempotent when a database was created with
846
+ // the current schema and its user_version was rolled back to exercise the full
847
+ // migration chain (the v6/v8 fixtures do this). A real v11 database can never
848
+ // contain these table names, so the guard cannot mask a structurally different
849
+ // legacy table in production.
850
+ database.exec('BEGIN IMMEDIATE');
851
+ try {
852
+ database.exec(`
853
+ CREATE TABLE IF NOT EXISTS activation_watch (
854
+ plan_id TEXT PRIMARY KEY,
855
+ package_name TEXT NOT NULL,
856
+ package_version TEXT NOT NULL,
857
+ package_integrity TEXT NOT NULL,
858
+ activation_id TEXT NOT NULL,
859
+ fence INTEGER NOT NULL CHECK(fence >= 1),
860
+ state TEXT NOT NULL CHECK(state IN ('watching', 'closed-regressed', 'closed-retracted')),
861
+ revision INTEGER NOT NULL CHECK(revision >= 1),
862
+ last_host_generation INTEGER NOT NULL DEFAULT 0 CHECK(last_host_generation >= 0),
863
+ healthy_observations INTEGER NOT NULL DEFAULT 0 CHECK(healthy_observations >= 0),
864
+ close_disposition TEXT CHECK(close_disposition IS NULL OR close_disposition IN ('regressed', 'retracted')),
865
+ close_at INTEGER,
866
+ close_evidence_id TEXT,
867
+ close_signature_digest TEXT CHECK(close_signature_digest IS NULL OR length(close_signature_digest) = 64),
868
+ started_at INTEGER NOT NULL,
869
+ updated_at INTEGER NOT NULL,
870
+ CHECK(
871
+ (state = 'watching' AND close_disposition IS NULL AND close_at IS NULL AND close_evidence_id IS NULL AND close_signature_digest IS NULL) OR
872
+ (state IN ('closed-regressed', 'closed-retracted') AND close_disposition IS NOT NULL AND close_at IS NOT NULL
873
+ AND close_evidence_id IS NOT NULL AND close_signature_digest IS NOT NULL)
874
+ ),
875
+ FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
876
+ ) STRICT, WITHOUT ROWID;
877
+
878
+ CREATE TABLE IF NOT EXISTS activation_watch_evidence (
879
+ observation_id TEXT PRIMARY KEY,
880
+ plan_id TEXT NOT NULL,
881
+ disposition TEXT NOT NULL CHECK(disposition IN ('regressed', 'healthy', 'retracted')),
882
+ receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
883
+ signature_digest TEXT NOT NULL CHECK(length(signature_digest) = 64),
884
+ receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'),
885
+ host_generation INTEGER NOT NULL DEFAULT 0 CHECK(host_generation >= 0),
886
+ failures INTEGER NOT NULL DEFAULT 0 CHECK(failures >= 0),
887
+ checks INTEGER NOT NULL DEFAULT 0 CHECK(checks >= 0),
888
+ created_at INTEGER NOT NULL,
889
+ FOREIGN KEY(plan_id) REFERENCES activation_watch(plan_id) ON DELETE RESTRICT
890
+ ) STRICT, WITHOUT ROWID;
891
+ CREATE INDEX IF NOT EXISTS activation_watch_evidence_plan ON activation_watch_evidence(plan_id, created_at);
892
+ `);
893
+ // Backfill watches for plans already promoted before the watch lifecycle existed.
894
+ // The exact target is reconstructed from the immutable candidate snapshot pinned
895
+ // in the plan row (candidate_json), never from a mutable current catalog.
896
+ const activated = database.prepare(`SELECT id, candidate_json, activation_id, activation_fence, updated_at
897
+ FROM activation_plans WHERE status = 'activated'`).all();
898
+ const insert = database.prepare(`INSERT INTO activation_watch (plan_id, package_name, package_version, package_integrity,
899
+ activation_id, fence, state, revision, last_host_generation, healthy_observations, started_at, updated_at)
900
+ VALUES (?, ?, ?, ?, ?, ?, 'watching', 1, 0, 0, ?, ?)`);
901
+ for (const row of activated) {
902
+ const candidate = JSON.parse(row.candidate_json);
903
+ if (typeof candidate.package !== 'string' || typeof candidate.version !== 'string' || typeof candidate.integrity !== 'string'
904
+ || typeof row.activation_id !== 'string' || row.activation_fence < 1) {
905
+ throw new ControlPlaneDatabaseError('unsafe-file', 'legacy activated plan has an invalid immutable candidate binding');
906
+ }
907
+ insert.run(row.id, candidate.package, candidate.version, candidate.integrity, row.activation_id, row.activation_fence, row.updated_at, row.updated_at);
908
+ }
909
+ database.exec('PRAGMA user_version = 12; COMMIT');
910
+ }
911
+ catch (error) {
912
+ database.exec('ROLLBACK');
913
+ throw error;
914
+ }
915
+ }
916
+ function migrateV12ToV13(database) {
917
+ // Add the 'modify' source-plan mode: a growth executor may prepare a patch for
918
+ // an *existing* plugin in an isolated worktree and attach its frozen-build
919
+ // evidence while the plan is still pending owner approval. SQLite cannot widen
920
+ // a CHECK in place, so rebuild source_plans and copy every column verbatim;
921
+ // every pre-existing row is backfilled to mode='create' with no prepared
922
+ // evidence. Existing rows are all create-mode and retain their immutable
923
+ // digest. Build the replacement under a temporary name before dropping the
924
+ // parent, then rename it into place: renaming the old parent would rewrite
925
+ // dependent foreign-key declarations to the temporary name.
926
+ database.exec(`
927
+ PRAGMA foreign_keys = OFF;
928
+ BEGIN IMMEDIATE;
929
+ CREATE TABLE source_plans_v13 (
930
+ id TEXT PRIMARY KEY,
931
+ plan_digest TEXT NOT NULL UNIQUE CHECK(length(plan_digest) = 64),
932
+ gap_id TEXT NOT NULL,
933
+ gap_snapshot_json TEXT NOT NULL CHECK(json_valid(gap_snapshot_json) AND json_type(gap_snapshot_json) = 'object'),
934
+ repository TEXT NOT NULL,
935
+ worktree TEXT NOT NULL,
936
+ base_commit TEXT NOT NULL CHECK(length(base_commit) = 40),
937
+ plugin_name TEXT NOT NULL,
938
+ generator_digest TEXT NOT NULL CHECK(length(generator_digest) = 64),
939
+ scope_json TEXT NOT NULL CHECK(json_valid(scope_json) AND json_type(scope_json) = 'array'),
940
+ mode TEXT NOT NULL DEFAULT 'create' CHECK(mode IN ('create', 'modify')),
941
+ status TEXT NOT NULL CHECK(status IN (
942
+ 'expired', 'pending-approval', 'approved', 'running-local-checks', 'ready-for-human-review', 'local-checks-failed',
943
+ 'awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign', 'awaiting-publish',
944
+ 'awaiting-registry-verify', 'awaiting-catalog-admission', 'release-complete', 'release-failed', 'publish-ambiguous'
945
+ )),
946
+ revision INTEGER NOT NULL CHECK(revision >= 1),
947
+ created_at INTEGER NOT NULL,
948
+ expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
949
+ approval_json TEXT CHECK(approval_json IS NULL OR json_valid(approval_json)),
950
+ checked_tree_digest TEXT CHECK(checked_tree_digest IS NULL OR (length(checked_tree_digest) = 64 AND checked_tree_digest NOT GLOB '*[^a-f0-9]*')),
951
+ checked_patch_digest TEXT CHECK(checked_patch_digest IS NULL OR (length(checked_patch_digest) = 64 AND checked_patch_digest NOT GLOB '*[^a-f0-9]*')),
952
+ checked_at INTEGER,
953
+ prepared_evidence_json TEXT CHECK(prepared_evidence_json IS NULL OR
954
+ (json_valid(prepared_evidence_json) AND json_type(prepared_evidence_json) = 'object')),
955
+ release_authorization_json TEXT CHECK(release_authorization_json IS NULL OR (json_valid(release_authorization_json) AND json_type(release_authorization_json) = 'object')),
956
+ release_authorization_digest TEXT CHECK(release_authorization_digest IS NULL OR length(release_authorization_digest) = 64),
957
+ release_id TEXT,
958
+ release_fence INTEGER NOT NULL DEFAULT 0 CHECK(release_fence >= 0),
959
+ release_failure_phase TEXT CHECK(release_failure_phase IS NULL OR release_failure_phase IN (
960
+ 'pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'
961
+ )),
962
+ release_failure_code TEXT,
963
+ updated_at INTEGER NOT NULL,
964
+ CHECK((checked_tree_digest IS NULL AND checked_patch_digest IS NULL AND checked_at IS NULL) OR
965
+ (checked_tree_digest IS NOT NULL AND checked_patch_digest IS NOT NULL AND checked_at IS NOT NULL)),
966
+ CHECK(mode = 'create' OR
967
+ (checked_tree_digest IS NOT NULL AND checked_patch_digest IS NOT NULL AND checked_at IS NOT NULL)),
968
+ CHECK((mode = 'create' AND prepared_evidence_json IS NULL) OR
969
+ (mode = 'modify' AND prepared_evidence_json IS NOT NULL)),
970
+ CHECK((release_authorization_json IS NULL AND release_authorization_digest IS NULL) OR
971
+ (release_authorization_json IS NOT NULL AND release_authorization_digest IS NOT NULL)),
972
+ FOREIGN KEY(gap_id) REFERENCES capability_gaps(id) ON DELETE RESTRICT
973
+ ) STRICT;
974
+ INSERT INTO source_plans_v13 (id, plan_digest, gap_id, gap_snapshot_json, repository, worktree, base_commit,
975
+ plugin_name, generator_digest, scope_json, mode, status, revision, created_at, expires_at, approval_json,
976
+ checked_tree_digest, checked_patch_digest, checked_at, prepared_evidence_json,
977
+ release_authorization_json, release_authorization_digest, release_id, release_fence,
978
+ release_failure_phase, release_failure_code, updated_at)
979
+ SELECT id, plan_digest, gap_id, gap_snapshot_json, repository, worktree, base_commit, plugin_name,
980
+ generator_digest, scope_json, 'create', status, revision, created_at, expires_at, approval_json,
981
+ checked_tree_digest, checked_patch_digest, checked_at, NULL,
982
+ release_authorization_json, release_authorization_digest, release_id, release_fence,
983
+ release_failure_phase, release_failure_code, updated_at FROM source_plans;
984
+ DROP TABLE source_plans;
985
+ ALTER TABLE source_plans_v13 RENAME TO source_plans;
986
+ PRAGMA user_version = 13;
987
+ COMMIT;
988
+ PRAGMA foreign_keys = ON;
989
+ `);
990
+ }
991
+ function migrateV13ToV14(database) {
992
+ database.exec(`
993
+ BEGIN IMMEDIATE;
994
+ CREATE TABLE IF NOT EXISTS source_job_authorities (
995
+ authority_id TEXT PRIMARY KEY,
996
+ authority_digest TEXT NOT NULL CHECK(length(authority_digest) = 64),
997
+ expires_at INTEGER NOT NULL,
998
+ max_submissions INTEGER NOT NULL CHECK(max_submissions >= 1),
999
+ submissions INTEGER NOT NULL CHECK(submissions >= 0 AND submissions <= max_submissions)
1000
+ ) STRICT, WITHOUT ROWID;
1001
+ CREATE TABLE IF NOT EXISTS source_jobs (
1002
+ id TEXT PRIMARY KEY,
1003
+ automation_id TEXT NOT NULL UNIQUE,
1004
+ authority_id TEXT NOT NULL,
1005
+ idempotency_key TEXT NOT NULL,
1006
+ intent_json TEXT NOT NULL CHECK(json_valid(intent_json) AND json_type(intent_json) = 'object'),
1007
+ intent_digest TEXT NOT NULL CHECK(length(intent_digest) = 64),
1008
+ status TEXT NOT NULL CHECK(status IN ('queued', 'running', 'prepared', 'failed', 'unknown')),
1009
+ revision INTEGER NOT NULL CHECK(revision >= 1),
1010
+ created_at INTEGER NOT NULL,
1011
+ expires_at INTEGER NOT NULL,
1012
+ definition_hash TEXT,
1013
+ occurrence_id TEXT,
1014
+ plan_id TEXT UNIQUE,
1015
+ failure_code TEXT,
1016
+ updated_at INTEGER NOT NULL,
1017
+ UNIQUE(authority_id, idempotency_key),
1018
+ FOREIGN KEY(authority_id) REFERENCES source_job_authorities(authority_id) ON DELETE RESTRICT,
1019
+ FOREIGN KEY(plan_id) REFERENCES source_plans(id) ON DELETE RESTRICT,
1020
+ CHECK((status = 'queued' AND occurrence_id IS NULL AND plan_id IS NULL AND failure_code IS NULL)
1021
+ OR (status = 'running' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NULL AND failure_code IS NULL)
1022
+ OR (status = 'prepared' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NOT NULL AND failure_code IS NULL)
1023
+ OR (status = 'failed' AND plan_id IS NULL AND failure_code IS NOT NULL)
1024
+ OR (status = 'unknown' AND definition_hash IS NOT NULL AND occurrence_id IS NOT NULL AND plan_id IS NULL AND failure_code IS NOT NULL))
1025
+ ) STRICT;
1026
+ CREATE UNIQUE INDEX IF NOT EXISTS source_jobs_single_active ON source_jobs((1)) WHERE status IN ('queued', 'running', 'unknown');
1027
+ CREATE INDEX IF NOT EXISTS source_jobs_created ON source_jobs(created_at, id);
1028
+ PRAGMA user_version = 14;
1029
+ COMMIT;
1030
+ `);
1031
+ }
1032
+ function migrateV14ToV15(database) {
1033
+ const columns = new Set(database.prepare('PRAGMA table_info(activation_plans)').all().map(row => row.name));
1034
+ const hasRollbackPhase = database.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'host_attestations'").get()?.sql?.includes("'rollback'") === true;
1035
+ database.exec(`
1036
+ BEGIN IMMEDIATE;
1037
+ ${columns.has('activation_target_baseline_json') ? '' : "ALTER TABLE activation_plans ADD COLUMN activation_target_baseline_json TEXT CHECK(activation_target_baseline_json IS NULL OR (json_valid(activation_target_baseline_json) AND json_type(activation_target_baseline_json) = 'array'));"}
1038
+ ${columns.has('host_recovery_required') ? '' : 'ALTER TABLE activation_plans ADD COLUMN host_recovery_required INTEGER NOT NULL DEFAULT 0 CHECK(host_recovery_required IN (0, 1));'}
1039
+ ${columns.has('rollback_profile_restored') ? '' : 'ALTER TABLE activation_plans ADD COLUMN rollback_profile_restored INTEGER NOT NULL DEFAULT 0 CHECK(rollback_profile_restored IN (0, 1));'}
1040
+ -- Older active Host states have no trustworthy core-file baseline. Keep them
1041
+ -- pending recovery, never manufacture a baseline from mutable files.
1042
+ UPDATE activation_plans SET host_recovery_required = 1
1043
+ WHERE status IN ('awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending');
1044
+ ${hasRollbackPhase ? '' : 'ALTER TABLE host_attestations RENAME TO host_attestations_v14;'}
1045
+ CREATE TABLE IF NOT EXISTS host_attestations (
1046
+ plan_id TEXT NOT NULL, phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health', 'rollback')),
1047
+ receipt_id TEXT NOT NULL UNIQUE, receipt_digest TEXT NOT NULL CHECK(length(receipt_digest) = 64),
1048
+ receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND json_type(receipt_json) = 'object'), host_generation INTEGER NOT NULL CHECK(host_generation >= 1),
1049
+ created_at INTEGER NOT NULL, PRIMARY KEY(plan_id, phase), FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
1050
+ ) STRICT, WITHOUT ROWID;
1051
+ ${hasRollbackPhase ? '' : 'INSERT INTO host_attestations SELECT * FROM host_attestations_v14; DROP TABLE host_attestations_v14;'}
1052
+ ${hasRollbackPhase ? '' : 'ALTER TABLE host_attestation_operations RENAME TO host_attestation_operations_v14;'}
1053
+ CREATE TABLE IF NOT EXISTS host_attestation_operations (
1054
+ plan_id TEXT NOT NULL, phase TEXT NOT NULL CHECK(phase IN ('reload', 'readiness', 'effect-blocked-replay', 'shadow', 'canary', 'soak', 'health', 'rollback')),
1055
+ operation_id TEXT NOT NULL UNIQUE, binding_digest TEXT NOT NULL CHECK(length(binding_digest) = 64), request_digest TEXT NOT NULL CHECK(length(request_digest) = 64),
1056
+ request_json TEXT NOT NULL CHECK(json_valid(request_json) AND json_type(request_json) = 'object'), status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'applied')),
1057
+ receipt_digest TEXT CHECK(receipt_digest IS NULL OR length(receipt_digest) = 64), receipt_json TEXT CHECK(receipt_json IS NULL OR (json_valid(receipt_json) AND json_type(receipt_json) = 'object')),
1058
+ created_at INTEGER NOT NULL, completed_at INTEGER, applied_at INTEGER, PRIMARY KEY(plan_id, phase), FOREIGN KEY(plan_id) REFERENCES activation_plans(id) ON DELETE RESTRICT
1059
+ ) STRICT, WITHOUT ROWID;
1060
+ ${hasRollbackPhase ? '' : 'INSERT INTO host_attestation_operations SELECT * FROM host_attestation_operations_v14; DROP TABLE host_attestation_operations_v14;'}
1061
+ PRAGMA user_version = 15;
1062
+ COMMIT;
1063
+ `);
1064
+ }
1065
+ function migrateV15ToV16(database) {
1066
+ database.exec(`
1067
+ BEGIN IMMEDIATE;
1068
+ CREATE TABLE IF NOT EXISTS owner_task_failure_gaps (
1069
+ gap_id TEXT PRIMARY KEY REFERENCES capability_gaps(id) ON DELETE RESTRICT,
1070
+ reference_json TEXT NOT NULL CHECK(json_valid(reference_json) AND json_type(reference_json) = 'object'),
1071
+ reference_digest TEXT NOT NULL CHECK(length(reference_digest) = 64)
1072
+ ) STRICT, WITHOUT ROWID;
1073
+ PRAGMA user_version = 16;
1074
+ COMMIT;
1075
+ `);
1076
+ }
1077
+ function migrateV16ToV17(database) {
1078
+ database.exec(`
1079
+ BEGIN IMMEDIATE;
1080
+ CREATE TABLE IF NOT EXISTS activation_deployment_checkpoints (
1081
+ plan_id TEXT PRIMARY KEY REFERENCES activation_plans(id) ON DELETE RESTRICT,
1082
+ baseline_json TEXT NOT NULL CHECK(json_valid(baseline_json) AND json_type(baseline_json) = 'array'),
1083
+ exposure_order INTEGER NOT NULL UNIQUE CHECK(exposure_order >= 1),
1084
+ successful_order INTEGER UNIQUE CHECK(successful_order IS NULL OR successful_order = exposure_order),
1085
+ recorded_at INTEGER NOT NULL,
1086
+ succeeded_at INTEGER
1087
+ ) STRICT, WITHOUT ROWID;
1088
+ CREATE INDEX IF NOT EXISTS activation_deployment_checkpoints_success ON activation_deployment_checkpoints(successful_order)
1089
+ WHERE successful_order IS NOT NULL;
1090
+ CREATE TABLE IF NOT EXISTS activation_deployment_sequence (
1091
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
1092
+ next_exposure_order INTEGER NOT NULL CHECK(next_exposure_order >= 1)
1093
+ ) STRICT, WITHOUT ROWID;
1094
+ INSERT OR IGNORE INTO activation_deployment_sequence (singleton, next_exposure_order) VALUES (1, 1);
1095
+ -- Historical promotions did not retain a verified installed core-file pin.
1096
+ -- Do not invent one from mutable profile contents: they remain observable,
1097
+ -- but cannot be physically rolled back by this new path.
1098
+ PRAGMA user_version = 17;
1099
+ COMMIT;
1100
+ `);
1101
+ }
1102
+ function migrateV17ToV18(database) {
1103
+ database.exec(`
1104
+ PRAGMA foreign_keys = OFF;
1105
+ BEGIN IMMEDIATE;
1106
+ CREATE TABLE IF NOT EXISTS source_release_dispatches (
1107
+ operation_id TEXT PRIMARY KEY REFERENCES source_release_operations(operation_id) ON DELETE RESTRICT,
1108
+ status TEXT NOT NULL CHECK(status IN ('claimed', 'completed')),
1109
+ claimed_at INTEGER NOT NULL,
1110
+ completed_at INTEGER,
1111
+ CHECK((status = 'claimed' AND completed_at IS NULL) OR
1112
+ (status = 'completed' AND completed_at IS NOT NULL AND completed_at >= claimed_at))
1113
+ ) STRICT, WITHOUT ROWID;
1114
+ -- A v17 pending operation may already have reached an external adapter
1115
+ -- before this binary could persist its signed receipt. Preserve that
1116
+ -- uncertainty; do not infer that dispatch is safe to repeat.
1117
+ INSERT OR IGNORE INTO source_release_dispatches (operation_id, status, claimed_at, completed_at)
1118
+ SELECT operation_id,
1119
+ CASE WHEN status = 'pending' THEN 'claimed' ELSE 'completed' END,
1120
+ created_at,
1121
+ CASE WHEN status = 'pending' THEN NULL ELSE completed_at END
1122
+ FROM source_release_operations;
1123
+ PRAGMA user_version = 18;
1124
+ COMMIT;
1125
+ PRAGMA foreign_keys = ON;
1126
+ `);
1127
+ }
669
1128
  export function openControlPlaneDatabase(path) {
670
1129
  prepare(path);
671
1130
  const database = new DatabaseSync(path);
@@ -697,7 +1156,50 @@ export function openControlPlaneDatabase(path) {
697
1156
  migrateV9ToV10(database);
698
1157
  if (version <= 10)
699
1158
  migrateV10ToV11(database);
1159
+ if (version <= 11)
1160
+ migrateV11ToV12(database);
1161
+ if (version <= 12)
1162
+ migrateV12ToV13(database);
1163
+ if (version <= 13)
1164
+ migrateV13ToV14(database);
1165
+ if (version <= 14)
1166
+ migrateV14ToV15(database);
1167
+ if (version <= 15)
1168
+ migrateV15ToV16(database);
1169
+ if (version <= 16)
1170
+ migrateV16ToV17(database);
1171
+ if (version <= 17)
1172
+ migrateV17ToV18(database);
1173
+ }
1174
+ if (Number(database.prepare('PRAGMA user_version').get()?.user_version) < 19) {
1175
+ database.exec(`BEGIN IMMEDIATE; ${sourceAdoptionsSchema} PRAGMA user_version = 19; COMMIT;`);
1176
+ }
1177
+ else
1178
+ database.exec(sourceAdoptionsSchema);
1179
+ if (Number(database.prepare('PRAGMA user_version').get()?.user_version) < 20) {
1180
+ database.exec(`BEGIN IMMEDIATE; ${foregroundDeploymentsSchema} PRAGMA user_version = 20; COMMIT;`);
1181
+ }
1182
+ else
1183
+ database.exec(foregroundDeploymentsSchema);
1184
+ if (Number(database.prepare('PRAGMA user_version').get()?.user_version) < 21) {
1185
+ database.exec(`BEGIN IMMEDIATE; ${taskObservationsSchema} PRAGMA user_version = 21; COMMIT;`);
1186
+ }
1187
+ else
1188
+ database.exec(taskObservationsSchema);
1189
+ if (Number(database.prepare('PRAGMA user_version').get()?.user_version) < 22) {
1190
+ database.exec(`BEGIN IMMEDIATE; ${hostAttestationDispatchesSchema}
1191
+ INSERT OR IGNORE INTO host_attestation_dispatches (operation_id,status,claimed_at,completed_at)
1192
+ SELECT operation_id, CASE WHEN status = 'pending' THEN 'claimed' ELSE 'completed' END, created_at,
1193
+ CASE WHEN status = 'pending' THEN NULL ELSE completed_at END FROM host_attestation_operations;
1194
+ PRAGMA user_version = 22; COMMIT;`);
1195
+ }
1196
+ else
1197
+ database.exec(hostAttestationDispatchesSchema);
1198
+ if (Number(database.prepare('PRAGMA user_version').get()?.user_version) < 23) {
1199
+ database.exec(`BEGIN IMMEDIATE; ${adoptionHandoffsSchema} PRAGMA user_version = 23; COMMIT;`);
700
1200
  }
1201
+ else
1202
+ database.exec(adoptionHandoffsSchema);
701
1203
  database.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;');
702
1204
  return database;
703
1205
  }