@abelxiaoxing/cadence 1.8.4 → 2.0.0

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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +94 -21
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
@@ -1,1299 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import {
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- unlinkSync,
9
- writeFileSync,
10
- } from "node:fs";
11
- import path from "node:path";
12
- import {
13
- type ChangeContract,
14
- normalizeChangeContract,
15
- } from "./change-contract.ts";
16
- import { changeContractDiagnostics } from "./change-contract-schema.ts";
17
- import { isValidRelativePath } from "./contracts.ts";
18
- import {
19
- assessDeliveryTraceability,
20
- bindTaskVerifications,
21
- compileGateAReceipt,
22
- compileImplementPlan,
23
- compileReadyReceipt,
24
- type DeliveryArtifactBinding,
25
- DesignPlanValidationError,
26
- type GateApprovalProof,
27
- parseGateAReceipt,
28
- parseImplementPlan,
29
- parseReadyReceipt,
30
- } from "./delivery-compiler.ts";
31
- import type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
32
- import {
33
- type DesignArtifactOperationFacts,
34
- type DesignArtifactOperationOutcome,
35
- DesignJournal,
36
- type DesignStatusProjection,
37
- } from "./design-journal.ts";
38
- import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
39
- import { planAuthoringSummary } from "./plan-draft-summary.ts";
40
- import { canonicalJson } from "./run-state.ts";
41
- import { RunStore } from "./run-store.ts";
42
- import { observeSafePath } from "./safe-path.ts";
43
- import type { ResolvedStateRoot } from "./state-root.ts";
44
-
45
- const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
46
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
47
- const ARTIFACT_SEGMENT = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/iu;
48
- const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
49
- const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
50
- const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
51
-
52
- function validGateContract(value: unknown): boolean {
53
- if (typeof value === "string")
54
- return (
55
- value.trim().length > 0 &&
56
- Buffer.byteLength(value, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
57
- );
58
- try {
59
- normalizeChangeContract(value);
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- export interface DesignOpenSpecInspection {
67
- change: string;
68
- schema: string;
69
- planningComplete: boolean;
70
- strictValid: boolean;
71
- artifactPaths: string[];
72
- }
73
-
74
- export type DesignControlRequest =
75
- | {
76
- operation: "start";
77
- operationId: string;
78
- requirement: string;
79
- }
80
- | {
81
- operation: "start";
82
- operationId: string;
83
- change: string;
84
- }
85
- | {
86
- operation: "status";
87
- runId: string;
88
- }
89
- | {
90
- operation: "bind-change";
91
- runId: string;
92
- operationId: string;
93
- change: string;
94
- }
95
- | {
96
- operation: "record-decision";
97
- runId: string;
98
- operationId: string;
99
- decisionId: string;
100
- category: "behavior" | "technical";
101
- contract: string;
102
- refs: string[];
103
- }
104
- | {
105
- operation: "approve-gate";
106
- runId: string;
107
- operationId: string;
108
- gate: "gate-a";
109
- contract: string | ChangeContract;
110
- }
111
- | {
112
- operation: "approve-gate";
113
- runId: string;
114
- operationId: string;
115
- gate: "gate-b";
116
- }
117
- | {
118
- operation: "validate-plan-draft";
119
- runId: string;
120
- }
121
- | {
122
- operation: "compile-plan";
123
- runId: string;
124
- operationId: string;
125
- }
126
- | {
127
- operation: "finalize-delivery";
128
- runId: string;
129
- operationId: string;
130
- }
131
- | {
132
- operation: "write-artifact";
133
- runId: string;
134
- operationId: string;
135
- path: string;
136
- content: string;
137
- }
138
- | {
139
- operation: "delete-artifact";
140
- runId: string;
141
- operationId: string;
142
- path: string;
143
- };
144
-
145
- export interface DesignControllerOptions {
146
- consumerRoot: string;
147
- stateRoot: ResolvedStateRoot;
148
- now?: () => number;
149
- finalizationLeaseMs?: number;
150
- inspectOpenSpec: (
151
- consumerRoot: string,
152
- change: string,
153
- ) => Promise<DesignOpenSpecInspection>;
154
- }
155
-
156
- export class DesignFinalizationError extends Error {
157
- readonly diagnostics: readonly string[];
158
-
159
- readonly openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>;
160
-
161
- constructor(
162
- diagnostics: readonly string[],
163
- openSpecDiagnostic?: Readonly<OpenSpecDiagnostic>,
164
- ) {
165
- const normalized = [...new Set(diagnostics)].sort();
166
- super("design-finalization-invalid");
167
- this.name = "DesignFinalizationError";
168
- this.diagnostics = Object.freeze(normalized);
169
- this.openSpecDiagnostic = openSpecDiagnostic;
170
- }
171
- }
172
-
173
- function isRecord(value: unknown): value is Record<string, unknown> {
174
- return value !== null && typeof value === "object" && !Array.isArray(value);
175
- }
176
-
177
- function exactKeys(
178
- value: Record<string, unknown>,
179
- keys: readonly string[],
180
- ): boolean {
181
- const actual = Object.keys(value);
182
- return (
183
- actual.length === keys.length && actual.every((key) => keys.includes(key))
184
- );
185
- }
186
-
187
- function normalizedTransientText(value: string): string {
188
- return value.replace(/\r\n?/gu, "\n").trim();
189
- }
190
-
191
- function transientHash(domain: string, value: string): string {
192
- return createHash("sha256")
193
- .update(domain)
194
- .update("\0")
195
- .update(normalizedTransientText(value))
196
- .digest("hex");
197
- }
198
-
199
- function lifecycleOperationId(kind: string, runId: string): string {
200
- return `design-${kind}-${createHash("sha256")
201
- .update(runId)
202
- .digest("hex")
203
- .slice(0, 32)}`;
204
- }
205
-
206
- export class DesignControlValidationError extends DesignPlanValidationError {
207
- constructor(diagnostics: readonly DesignPlanDiagnostic[]) {
208
- super("invalid-design-control-request", diagnostics);
209
- }
210
- }
211
- export function validateDesignControlRequest(value: unknown):
212
- | { ok: true; value: DesignControlRequest }
213
- | {
214
- ok: false;
215
- code: "invalid-design-control-request";
216
- diagnostics: DesignPlanDiagnostic[];
217
- } {
218
- const parsed = parseDesignControlRequest(value);
219
- if (parsed.ok) return parsed;
220
- let diagnostics: DesignPlanDiagnostic[] = [];
221
- if (
222
- isRecord(value) &&
223
- value.operation === "approve-gate" &&
224
- value.gate === "gate-a" &&
225
- typeof value.contract !== "string"
226
- )
227
- diagnostics = changeContractDiagnostics(value.contract);
228
- if (!diagnostics.length) {
229
- const field = !isRecord(value)
230
- ? "request"
231
- : typeof value.operation !== "string"
232
- ? "operation"
233
- : value.operation !== "start" &&
234
- (typeof value.runId !== "string" || !IDENTIFIER.test(value.runId))
235
- ? "runId"
236
- : !["status", "validate-plan-draft"].includes(
237
- String(value.operation),
238
- ) &&
239
- (typeof value.operationId !== "string" ||
240
- !IDENTIFIER.test(value.operationId))
241
- ? "operationId"
242
- : value.operation === "approve-gate"
243
- ? value.gate === "gate-a"
244
- ? "contract"
245
- : "gate"
246
- : "request";
247
- diagnostics = [{ code: "design-control-field-invalid", field }];
248
- }
249
- return { ...parsed, diagnostics };
250
- }
251
-
252
- function parseDesignControlRequest(
253
- value: unknown,
254
- ):
255
- | { ok: true; value: DesignControlRequest }
256
- | { ok: false; code: "invalid-design-control-request" } {
257
- if (!isRecord(value))
258
- return { ok: false, code: "invalid-design-control-request" };
259
- if (value.operation === "start") {
260
- const operationValid =
261
- typeof value.operationId === "string" &&
262
- IDENTIFIER.test(value.operationId);
263
- if (!operationValid)
264
- return { ok: false, code: "invalid-design-control-request" };
265
- if (
266
- exactKeys(value, ["operation", "operationId", "requirement"]) &&
267
- typeof value.requirement === "string" &&
268
- value.requirement.trim().length > 0 &&
269
- Buffer.byteLength(value.requirement, "utf8") <=
270
- MAX_DESIGN_REQUIREMENT_BYTES
271
- ) {
272
- return {
273
- ok: true,
274
- value: structuredClone(value) as DesignControlRequest,
275
- };
276
- }
277
- return exactKeys(value, ["operation", "operationId", "change"]) &&
278
- typeof value.change === "string" &&
279
- CHANGE_NAME.test(value.change)
280
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
281
- : { ok: false, code: "invalid-design-control-request" };
282
- }
283
- if (
284
- value.operation === "status" ||
285
- value.operation === "validate-plan-draft"
286
- ) {
287
- return exactKeys(value, ["operation", "runId"]) &&
288
- typeof value.runId === "string" &&
289
- IDENTIFIER.test(value.runId)
290
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
291
- : { ok: false, code: "invalid-design-control-request" };
292
- }
293
- const commonValid =
294
- typeof value.runId === "string" &&
295
- IDENTIFIER.test(value.runId) &&
296
- typeof value.operationId === "string" &&
297
- IDENTIFIER.test(value.operationId);
298
- if (!commonValid)
299
- return { ok: false, code: "invalid-design-control-request" };
300
- if (value.operation === "bind-change") {
301
- return exactKeys(value, ["operation", "runId", "operationId", "change"]) &&
302
- typeof value.change === "string" &&
303
- CHANGE_NAME.test(value.change)
304
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
305
- : { ok: false, code: "invalid-design-control-request" };
306
- }
307
- if (value.operation === "write-artifact") {
308
- return exactKeys(value, [
309
- "operation",
310
- "runId",
311
- "operationId",
312
- "path",
313
- "content",
314
- ]) &&
315
- typeof value.path === "string" &&
316
- typeof value.content === "string"
317
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
318
- : { ok: false, code: "invalid-design-control-request" };
319
- }
320
- if (value.operation === "delete-artifact") {
321
- return exactKeys(value, ["operation", "runId", "operationId", "path"]) &&
322
- typeof value.path === "string"
323
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
324
- : { ok: false, code: "invalid-design-control-request" };
325
- }
326
- if (
327
- value.operation === "compile-plan" ||
328
- value.operation === "finalize-delivery"
329
- ) {
330
- return exactKeys(value, ["operation", "runId", "operationId"])
331
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
332
- : { ok: false, code: "invalid-design-control-request" };
333
- }
334
- if (value.operation === "approve-gate") {
335
- if (value.gate === "gate-b") {
336
- return exactKeys(value, ["operation", "runId", "operationId", "gate"])
337
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
338
- : { ok: false, code: "invalid-design-control-request" };
339
- }
340
- return value.gate === "gate-a" &&
341
- exactKeys(value, [
342
- "operation",
343
- "runId",
344
- "operationId",
345
- "gate",
346
- "contract",
347
- ]) &&
348
- validGateContract(value.contract)
349
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
350
- : { ok: false, code: "invalid-design-control-request" };
351
- }
352
- if (value.operation !== "record-decision") {
353
- return { ok: false, code: "invalid-design-control-request" };
354
- }
355
- return exactKeys(value, [
356
- "operation",
357
- "runId",
358
- "operationId",
359
- "decisionId",
360
- "category",
361
- "contract",
362
- "refs",
363
- ]) &&
364
- typeof value.decisionId === "string" &&
365
- IDENTIFIER.test(value.decisionId) &&
366
- (value.category === "behavior" || value.category === "technical") &&
367
- typeof value.contract === "string" &&
368
- value.contract.trim().length > 0 &&
369
- Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES &&
370
- Array.isArray(value.refs) &&
371
- value.refs.every((ref) => typeof ref === "string")
372
- ? { ok: true, value: structuredClone(value) as DesignControlRequest }
373
- : { ok: false, code: "invalid-design-control-request" };
374
- }
375
-
376
- function safeRelative(change: string, name: string): string {
377
- if (!CHANGE_NAME.test(change)) throw new Error("design-change-invalid");
378
- return `openspec/changes/${change}/${name}`;
379
- }
380
-
381
- function isAllowedDesignArtifactPath(relative: string): boolean {
382
- if (!isValidRelativePath(relative)) return false;
383
- if (
384
- [
385
- ".openspec.yaml",
386
- "proposal.md",
387
- "design.md",
388
- "tasks.md",
389
- "plan-draft.json",
390
- ].includes(relative)
391
- ) {
392
- return true;
393
- }
394
- const segments = relative.split("/");
395
- return (
396
- segments.length >= 3 &&
397
- segments[0] === "specs" &&
398
- segments.at(-1) === "spec.md" &&
399
- segments.slice(1, -1).every((segment) => ARTIFACT_SEGMENT.test(segment))
400
- );
401
- }
402
-
403
- function designArtifactRelative(change: string, relative: string): string {
404
- if (!isAllowedDesignArtifactPath(relative)) {
405
- throw new Error("design-artifact-path-invalid");
406
- }
407
- return safeRelative(change, relative);
408
- }
409
-
410
- function ensureSafeDirectory(root: string, relative: string): void {
411
- const segments = relative.split("/");
412
- for (let index = 0; index < segments.length; index += 1) {
413
- const current = segments.slice(0, index + 1).join("/");
414
- const observation = observeSafePath(root, current);
415
- if (observation.kind === "directory") continue;
416
- if (observation.kind !== "absent") {
417
- throw new Error("design-artifact-path-unsafe");
418
- }
419
- const parent = index === 0 ? "." : segments.slice(0, index).join("/");
420
- if (observeSafePath(root, parent).kind !== "directory") {
421
- throw new Error("design-artifact-path-unsafe");
422
- }
423
- try {
424
- mkdirSync(path.join(root, ...current.split("/")), { mode: 0o755 });
425
- } catch {
426
- if (observeSafePath(root, current).kind !== "directory") {
427
- throw new Error("design-artifact-path-unsafe");
428
- }
429
- }
430
- if (observeSafePath(root, current).kind !== "directory") {
431
- throw new Error("design-artifact-path-unsafe");
432
- }
433
- }
434
- }
435
-
436
- function boundedArtifactBytes(root: string, relative: string): Buffer {
437
- if (observeSafePath(root, relative).kind !== "file") {
438
- throw new Error("design-artifact-path-unsafe");
439
- }
440
- const absolute = path.join(root, ...relative.split("/"));
441
- const size = lstatSync(absolute).size;
442
- if (size > MAX_DESIGN_FILE_BYTES) {
443
- throw new Error("design-artifact-content-too-large");
444
- }
445
- return readFileSync(absolute);
446
- }
447
-
448
- function readSafeFile(
449
- root: string,
450
- relative: string,
451
- maximum = MAX_DESIGN_FILE_BYTES,
452
- ): Buffer {
453
- if (observeSafePath(root, relative).kind !== "file") {
454
- throw new Error("design-file-unavailable");
455
- }
456
- const absolute = path.join(root, ...relative.split("/"));
457
- const size = lstatSync(absolute).size;
458
- if (size < 1 || size > maximum) throw new Error("design-file-size-invalid");
459
- return readFileSync(absolute);
460
- }
461
-
462
- function removeSafeFile(root: string, relative: string): void {
463
- const observation = observeSafePath(root, relative);
464
- if (observation.kind === "absent") return;
465
- if (observation.kind !== "file") throw new Error("design-file-unsafe");
466
- unlinkSync(path.join(root, ...relative.split("/")));
467
- }
468
-
469
- function removeSafeFileWithHash(
470
- root: string,
471
- relative: string,
472
- expectedHash: string,
473
- ): void {
474
- const observation = observeSafePath(root, relative);
475
- if (observation.kind === "absent") return;
476
- if (observation.kind !== "file") throw new Error("design-file-unsafe");
477
- const bytes = readSafeFile(root, relative, 4 * 1024 * 1024);
478
- if (createHash("sha256").update(bytes).digest("hex") !== expectedHash) return;
479
- unlinkSync(path.join(root, ...relative.split("/")));
480
- }
481
-
482
- function atomicWrite(root: string, relative: string, bytes: Uint8Array): void {
483
- const parent = path.posix.dirname(relative);
484
- if (observeSafePath(root, parent).kind !== "directory") {
485
- throw new Error("design-file-parent-unsafe");
486
- }
487
- const targetObservation = observeSafePath(root, relative);
488
- if (
489
- targetObservation.kind !== "file" &&
490
- targetObservation.kind !== "absent"
491
- ) {
492
- throw new Error("design-file-unsafe");
493
- }
494
- const temporary = `${parent}/.abel-${path.posix.basename(relative)}-${randomUUID()}.tmp`;
495
- if (observeSafePath(root, temporary).kind !== "absent") {
496
- throw new Error("design-temporary-file-conflict");
497
- }
498
- const temporaryAbsolute = path.join(root, ...temporary.split("/"));
499
- try {
500
- writeFileSync(temporaryAbsolute, bytes, { flag: "wx", mode: 0o600 });
501
- renameSync(temporaryAbsolute, path.join(root, ...relative.split("/")));
502
- } catch (error) {
503
- try {
504
- if (observeSafePath(root, temporary).kind === "file") {
505
- unlinkSync(temporaryAbsolute);
506
- }
507
- } catch {
508
- // The original safe failure remains authoritative.
509
- }
510
- throw error;
511
- }
512
- }
513
-
514
- function proof(status: DesignStatusProjection, gate: "gateA" | "gateB") {
515
- const projection = status.gates[gate];
516
- if (!projection.current || !projection.proof) {
517
- throw new DesignFinalizationError([
518
- gate === "gateA" ? "design-gate-a-stale" : "design-gate-b-stale",
519
- ]);
520
- }
521
- return structuredClone(projection.proof) as GateApprovalProof;
522
- }
523
-
524
- export class DesignController {
525
- readonly consumerRoot: string;
526
- readonly #journal: DesignJournal;
527
- readonly #runs: RunStore;
528
- readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
529
- #closed = false;
530
- readonly #authority = new AsyncLocalStorage<() => void>();
531
-
532
- private constructor(options: DesignControllerOptions) {
533
- this.consumerRoot = path.resolve(options.consumerRoot);
534
- this.#runs = RunStore.open(options.stateRoot);
535
- try {
536
- this.#journal = DesignJournal.open(options.stateRoot, {
537
- ...(options.now ? { now: options.now } : {}),
538
- ...(options.finalizationLeaseMs !== undefined
539
- ? { finalizationLeaseMs: options.finalizationLeaseMs }
540
- : {}),
541
- });
542
- } catch (error) {
543
- try {
544
- this.#runs.close();
545
- } catch {
546
- // The Design journal initialization failure remains authoritative.
547
- }
548
- throw error;
549
- }
550
- this.#inspectOpenSpec = options.inspectOpenSpec;
551
- }
552
-
553
- static open(options: DesignControllerOptions): DesignController {
554
- return new DesignController(options);
555
- }
556
-
557
- executeWithAuthority(
558
- value: unknown,
559
- assertAuthority: () => void,
560
- ): Promise<Record<string, unknown>> {
561
- return this.#authority.run(assertAuthority, () => this.execute(value));
562
- }
563
-
564
- #assertFinalizationLease(
565
- lease: Parameters<DesignJournal["assertFinalizationLease"]>[0],
566
- ): void {
567
- this.#authority.getStore()?.();
568
- this.#journal.assertFinalizationLease(lease);
569
- }
570
-
571
- #assertOpen(): void {
572
- this.#authority.getStore()?.();
573
- if (this.#closed) throw new Error("design-controller-closed");
574
- }
575
-
576
- status(runId: string): DesignStatusProjection {
577
- this.#assertOpen();
578
- return this.#journal.status(runId);
579
- }
580
-
581
- assertDesignRun(runId: string): void {
582
- this.#assertOpen();
583
- this.#journal.assertDesignRun(runId);
584
- }
585
-
586
- recordEvidence(input: Parameters<DesignJournal["recordEvidence"]>[0]) {
587
- this.#assertOpen();
588
- return this.#journal.recordEvidence(input);
589
- }
590
-
591
- verifyGateProof(input: Parameters<DesignJournal["verifyGateProof"]>[0]) {
592
- this.#assertOpen();
593
- return this.#journal.verifyGateProof(input);
594
- }
595
-
596
- verifyFinalizedDelivery(
597
- input: Parameters<DesignJournal["verifyFinalizedDelivery"]>[0],
598
- ) {
599
- this.#assertOpen();
600
- return this.#journal.verifyFinalizedDelivery(input);
601
- }
602
-
603
- async execute(value: unknown): Promise<Record<string, unknown>> {
604
- this.#assertOpen();
605
- const validation = validateDesignControlRequest(value);
606
- if (!validation.ok)
607
- throw new DesignControlValidationError(validation.diagnostics);
608
- const request = validation.value;
609
- switch (request.operation) {
610
- case "start":
611
- return this.#start(request);
612
- case "status":
613
- return this.#statusOutcome(request.runId);
614
- case "bind-change":
615
- return this.#bindChange(request);
616
- case "record-decision":
617
- return this.#journal.recordDecision(request);
618
- case "approve-gate":
619
- return this.#journal.approveGate(request);
620
- case "validate-plan-draft":
621
- return this.#validatePlanDraft(request);
622
- case "compile-plan":
623
- return this.#compilePlan(request);
624
- case "finalize-delivery":
625
- return this.#finalizeDelivery(request);
626
- case "write-artifact":
627
- return this.#writeArtifact(request);
628
- case "delete-artifact":
629
- return this.#deleteArtifact(request);
630
- }
631
- }
632
-
633
- #statusOutcome(runId: string): Record<string, unknown> {
634
- const run = this.#runs.status(runId);
635
- if (run.stage !== "abel-design") throw new Error("design-run-invalid");
636
- const terminal = ["completed", "discarded", "rejected"].includes(run.state);
637
- const design = this.#journal.status(runId);
638
- const latestDecisionSequence = design.decisions.reduce(
639
- (latest, decision) => Math.max(latest, decision.sequence),
640
- 0,
641
- );
642
- const planReadyForGateB =
643
- design.gates.gateA.current &&
644
- design.plan !== null &&
645
- design.plan.sequence >
646
- Math.max(
647
- design.gates.gateA.approvedSequence ?? 0,
648
- latestDecisionSequence,
649
- );
650
- const canApproveGate =
651
- !design.gates.gateA.current ||
652
- (planReadyForGateB && !design.gates.gateB.current);
653
- const legalOperations = terminal
654
- ? ["status"]
655
- : [
656
- "status",
657
- "record-decision",
658
- ...(canApproveGate ? ["approve-gate"] : []),
659
- ...(design.gates.gateA.current && !design.change
660
- ? ["bind-change"]
661
- : []),
662
- ...(design.gates.gateA.current && design.change
663
- ? [
664
- "write-artifact",
665
- "delete-artifact",
666
- "validate-plan-draft",
667
- "compile-plan",
668
- ]
669
- : []),
670
- ...(design.change &&
671
- design.plan &&
672
- design.gates.gateA.current &&
673
- design.gates.gateB.current
674
- ? ["finalize-delivery"]
675
- : []),
676
- ];
677
- return {
678
- runId: run.runId,
679
- stage: run.stage,
680
- ...(run.change ? { change: run.change } : {}),
681
- state: run.state,
682
- completed: run.state === "completed",
683
- ...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
684
- legalOperations,
685
- nextStep: terminal
686
- ? null
687
- : !design.gates.gateA.current
688
- ? {
689
- operation: "approve-gate",
690
- instruction:
691
- "Read applicable instructions, the affected source and existing tests, and the verification manifest together. Resolve remaining substantive choices in Design; use existing same-task approval. Include the goal, acceptance, constraints, and scope for likely implementation repairs in the accepted structured ChangeContract. The parent owns later technical choices within that policy; do not defer user decisions to Implement.",
692
- }
693
- : !design.change
694
- ? {
695
- operation: "bind-change",
696
- instruction: "Bind the approved change name to this runId.",
697
- }
698
- : !design.plan || !design.gates.gateB.current
699
- ? {
700
- operation: "write-artifact",
701
- instruction:
702
- "Write proposal, design, specs, author task checkboxes with exact Scenario references, and plan-draft.json. Use the installed example named in the tool description; omit derived identities. Preflight and inspect summary.implementation for the accepted recovery policy and summary.parallelism for independent tasks and serialization causes. Resolve known execution prerequisites in Design. Compile to supply verification bindings and Gate B.",
703
- }
704
- : {
705
- operation: "finalize-delivery",
706
- instruction:
707
- "Finalize the prepared artifacts. If tasks or the draft changed since compilation, preflight and compile again first. No separate Gate B approval is needed.",
708
- },
709
- packetActions: terminal ? [] : ["finish"],
710
- design,
711
- };
712
- }
713
-
714
- #start(request: Extract<DesignControlRequest, { operation: "start" }>) {
715
- const fromRequirement = "requirement" in request;
716
- let run: ReturnType<RunStore["startRun"]>;
717
- try {
718
- run = this.#runs.startRun({
719
- stage: "abel-design",
720
- ...(fromRequirement
721
- ? {
722
- provisionalKey: transientHash(
723
- "abel-design-requirement",
724
- request.requirement,
725
- ),
726
- provisionalAliases: [
727
- transientHash(
728
- "abel-design-requirement-v1",
729
- request.requirement,
730
- ),
731
- ],
732
- }
733
- : { change: request.change }),
734
- operationId: request.operationId,
735
- });
736
- } catch (error) {
737
- if (error instanceof Error && error.message === "operation-id-conflict") {
738
- throw new Error("design-operation-conflict", { cause: error });
739
- }
740
- throw error;
741
- }
742
- if (run.state === "created") {
743
- this.#journal.inheritFinalizedAuthority(run.runId);
744
- this.#runs.transition({
745
- runId: run.runId,
746
- to: "paused",
747
- operationId: lifecycleOperationId(
748
- fromRequirement ? "await-gate-a" : "await-evidence",
749
- run.runId,
750
- ),
751
- code: fromRequirement
752
- ? "design-awaiting-gate-a"
753
- : "design-awaiting-evidence",
754
- });
755
- }
756
- return this.#statusOutcome(run.runId);
757
- }
758
-
759
- #bindChange(
760
- request: Extract<DesignControlRequest, { operation: "bind-change" }>,
761
- ) {
762
- const run = this.#runs.status(request.runId);
763
- if (run.change && run.change !== request.change) {
764
- throw new Error("design-operation-conflict");
765
- }
766
- if (!run.change) {
767
- const design = this.#journal.status(request.runId);
768
- if (!design.gates.gateA.current)
769
- throw new Error("design-gate-a-required");
770
- }
771
- const bound = this.#runs.bindChange({
772
- runId: request.runId,
773
- change: request.change,
774
- operationId: request.operationId,
775
- });
776
- if (bound.state === "paused") {
777
- this.#runs.transition({
778
- runId: request.runId,
779
- to: "paused",
780
- operationId: lifecycleOperationId("bound", request.runId),
781
- code: "design-awaiting-evidence",
782
- });
783
- }
784
- return this.#statusOutcome(request.runId);
785
- }
786
-
787
- #artifactRun(runId: string): { change: string; gateACurrent: boolean } {
788
- const status = this.#journal.status(runId);
789
- if (!status.change) throw new Error("design-change-required");
790
- return {
791
- change: status.change,
792
- gateACurrent: status.gates.gateA.current,
793
- };
794
- }
795
-
796
- #writeArtifact(
797
- request: Extract<DesignControlRequest, { operation: "write-artifact" }>,
798
- ): DesignArtifactOperationOutcome {
799
- const artifactRun = this.#artifactRun(request.runId);
800
- const change = artifactRun.change;
801
- const relative = designArtifactRelative(change, request.path);
802
- const bytes = Buffer.from(request.content, "utf8");
803
- if (bytes.toString("utf8") !== request.content) {
804
- throw new Error("design-artifact-content-invalid");
805
- }
806
- if (bytes.length > MAX_DESIGN_FILE_BYTES) {
807
- throw new Error("design-artifact-content-too-large");
808
- }
809
- const facts: DesignArtifactOperationFacts = {
810
- runId: request.runId,
811
- operationId: request.operationId,
812
- operation: request.operation,
813
- path: request.path,
814
- bytes: bytes.length,
815
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
816
- };
817
- const replay = this.#journal.artifactOperationState(facts);
818
- if (replay?.state === "committed") return replay.outcome;
819
- if (!replay && !artifactRun.gateACurrent) {
820
- throw new Error("design-gate-a-required");
821
- }
822
- const lease = this.#journal.acquireFinalizationLease({
823
- runId: request.runId,
824
- operationId: request.operationId,
825
- });
826
- try {
827
- const outcome: DesignArtifactOperationOutcome = {
828
- operation: request.operation,
829
- runId: request.runId,
830
- path: request.path,
831
- bytes: facts.bytes,
832
- rawSha256: facts.rawSha256,
833
- };
834
- ensureSafeDirectory(this.consumerRoot, path.posix.dirname(relative));
835
- const target = observeSafePath(this.consumerRoot, relative);
836
- if (target.kind !== "file" && target.kind !== "absent") {
837
- throw new Error("design-artifact-path-unsafe");
838
- }
839
- const prepared = this.#journal.prepareArtifactOperation(
840
- facts,
841
- lease,
842
- outcome,
843
- );
844
- if (prepared.state === "committed") return prepared.outcome;
845
- atomicWrite(this.consumerRoot, relative, bytes);
846
- return this.#journal.commitArtifactOperation(facts, lease);
847
- } finally {
848
- this.#journal.releaseFinalizationLease(lease);
849
- }
850
- }
851
-
852
- #deleteArtifact(
853
- request: Extract<DesignControlRequest, { operation: "delete-artifact" }>,
854
- ): DesignArtifactOperationOutcome {
855
- const artifactRun = this.#artifactRun(request.runId);
856
- const change = artifactRun.change;
857
- const relative = designArtifactRelative(change, request.path);
858
- const facts: DesignArtifactOperationFacts = {
859
- runId: request.runId,
860
- operationId: request.operationId,
861
- operation: request.operation,
862
- path: request.path,
863
- };
864
- const replay = this.#journal.artifactOperationState(facts);
865
- if (replay?.state === "committed") return replay.outcome;
866
- if (!replay && !artifactRun.gateACurrent) {
867
- throw new Error("design-gate-a-required");
868
- }
869
- const lease = this.#journal.acquireFinalizationLease({
870
- runId: request.runId,
871
- operationId: request.operationId,
872
- });
873
- try {
874
- let outcome = replay?.outcome;
875
- if (!outcome) {
876
- const observation = observeSafePath(this.consumerRoot, relative);
877
- if (observation.kind === "absent") {
878
- outcome = {
879
- operation: request.operation,
880
- runId: request.runId,
881
- path: request.path,
882
- deleted: false,
883
- };
884
- } else if (observation.kind === "file") {
885
- const bytes = boundedArtifactBytes(this.consumerRoot, relative);
886
- outcome = {
887
- operation: request.operation,
888
- runId: request.runId,
889
- path: request.path,
890
- deleted: true,
891
- bytes: bytes.length,
892
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
893
- };
894
- } else {
895
- throw new Error("design-artifact-path-unsafe");
896
- }
897
- }
898
- const prepared = this.#journal.prepareArtifactOperation(
899
- facts,
900
- lease,
901
- outcome,
902
- );
903
- if (prepared.state === "committed") return prepared.outcome;
904
- const intended = prepared.outcome;
905
- if (intended.operation !== "delete-artifact") {
906
- throw new Error("design-artifact-operation-outcome-invalid");
907
- }
908
- if (intended.deleted) {
909
- const observation = observeSafePath(this.consumerRoot, relative);
910
- if (observation.kind === "file") {
911
- const current = boundedArtifactBytes(this.consumerRoot, relative);
912
- if (
913
- current.length !== intended.bytes ||
914
- createHash("sha256").update(current).digest("hex") !==
915
- intended.rawSha256
916
- ) {
917
- throw new Error("design-artifact-currentness-invalid");
918
- }
919
- unlinkSync(path.join(this.consumerRoot, ...relative.split("/")));
920
- } else if (observation.kind !== "absent") {
921
- throw new Error("design-artifact-path-unsafe");
922
- }
923
- }
924
- return this.#journal.commitArtifactOperation(facts, lease);
925
- } finally {
926
- this.#journal.releaseFinalizationLease(lease);
927
- }
928
- }
929
-
930
- #readCompiledDraft(runId: string) {
931
- const status = this.#journal.status(runId);
932
- if (!status.change) throw new Error("design-change-required");
933
- if (!status.gates.gateA.current) throw new Error("design-gate-a-required");
934
- const draftPath = safeRelative(status.change, "plan-draft.json");
935
- const draftBytes = readSafeFile(this.consumerRoot, draftPath);
936
- let draft: unknown;
937
- try {
938
- draft = JSON.parse(draftBytes.toString("utf8"));
939
- } catch {
940
- throw new DesignPlanValidationError("design-plan-draft-invalid", [
941
- { code: "design-plan-draft-invalid", field: "plan-draft.json" },
942
- ]);
943
- }
944
- const authoredDraft = structuredClone(draft);
945
- if (status.changeContract) {
946
- if (!draft || typeof draft !== "object" || Array.isArray(draft))
947
- throw new Error("change-contract-invalid");
948
- const record = draft as Record<string, unknown>;
949
- if (
950
- record.changeContract !== undefined &&
951
- canonicalJson(normalizeChangeContract(record.changeContract)) !==
952
- canonicalJson(status.changeContract)
953
- )
954
- throw new Error("change-contract-mismatch");
955
- record.changeContract = status.changeContract;
956
- } else if (draft && typeof draft === "object" && "changeContract" in draft)
957
- throw new Error("change-contract-gate-a-required");
958
- const compiled = compileImplementPlan(draft, {
959
- bindExecutionInputs: true,
960
- consumerRoot: this.consumerRoot,
961
- });
962
- if (compiled.plan.changeId !== status.change) {
963
- throw new DesignPlanValidationError("design-plan-change-mismatch", [
964
- { code: "design-plan-change-mismatch", field: "changeId" },
965
- ]);
966
- }
967
- return { status, compiled, authoredDraft };
968
- }
969
-
970
- #validatePlanDraft(
971
- request: Extract<
972
- DesignControlRequest,
973
- { operation: "validate-plan-draft" }
974
- >,
975
- ) {
976
- const { compiled, authoredDraft } = this.#readCompiledDraft(request.runId);
977
- return {
978
- operation: request.operation,
979
- runId: request.runId,
980
- valid: true,
981
- checks: { ...compiled.checks, gateA: "current" },
982
- summary: planAuthoringSummary(compiled.plan, authoredDraft),
983
- plan: {
984
- taskCount: compiled.plan.tasks.length,
985
- outputCount: compiled.plan.outputs.length,
986
- rawSha256: compiled.rawSha256,
987
- canonicalHash: compiled.planHash,
988
- },
989
- };
990
- }
991
-
992
- #compilePlan(
993
- request: Extract<DesignControlRequest, { operation: "compile-plan" }>,
994
- ) {
995
- const replay = this.#journal.operationOutcome(
996
- request.runId,
997
- request.operationId,
998
- request.operation,
999
- );
1000
- if (replay) return replay;
1001
- const lease = this.#journal.acquireFinalizationLease({
1002
- runId: request.runId,
1003
- operationId: request.operationId,
1004
- });
1005
- try {
1006
- this.#journal.assertNoPendingArtifactOperations(request.runId);
1007
- const { status, compiled } = this.#readCompiledDraft(request.runId);
1008
- const change = status.change;
1009
- if (!change) throw new Error("design-change-required");
1010
- const readyPath = safeRelative(change, "ready.yaml");
1011
- const gateAPath = safeRelative(change, "gate-a.yaml");
1012
- const tasksPath = safeRelative(change, "tasks.md");
1013
- if (observeSafePath(this.consumerRoot, tasksPath).kind === "absent") {
1014
- throw new DesignPlanValidationError(
1015
- "design-traceability-input-unavailable",
1016
- [{ code: "design-traceability-input-unavailable", path: "tasks.md" }],
1017
- );
1018
- }
1019
- const priorTasks = boundedArtifactBytes(this.consumerRoot, tasksPath);
1020
- const tasksBytes = Buffer.from(
1021
- bindTaskVerifications(
1022
- new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
1023
- priorTasks,
1024
- ),
1025
- compiled.plan,
1026
- ),
1027
- "utf8",
1028
- );
1029
- if (tasksBytes.length > MAX_DESIGN_FILE_BYTES) {
1030
- throw new Error("design-artifact-content-too-large");
1031
- }
1032
- const tasksUnchanged = tasksBytes.equals(priorTasks);
1033
- const unchanged =
1034
- tasksUnchanged &&
1035
- status.gates.gateB.current &&
1036
- status.plan?.rawSha256 === compiled.rawSha256 &&
1037
- status.plan.canonicalHash === compiled.planHash;
1038
- if (!unchanged) {
1039
- removeSafeFile(this.consumerRoot, readyPath);
1040
- removeSafeFile(this.consumerRoot, gateAPath);
1041
- }
1042
- this.#assertFinalizationLease(lease);
1043
- if (
1044
- !boundedArtifactBytes(this.consumerRoot, tasksPath).equals(priorTasks)
1045
- ) {
1046
- throw new Error("design-artifact-currentness-mismatch");
1047
- }
1048
- // The compilation journal commits only after both files are installed.
1049
- // A crash before that point can replay this deterministic projection;
1050
- // finalization always rechecks the plan and task bytes together.
1051
- if (!tasksUnchanged)
1052
- atomicWrite(this.consumerRoot, tasksPath, tasksBytes);
1053
- atomicWrite(
1054
- this.consumerRoot,
1055
- safeRelative(change, "implement-plan.json"),
1056
- compiled.bytes,
1057
- );
1058
- return this.#journal.recordCompiledPlan({
1059
- runId: request.runId,
1060
- operationId: request.operationId,
1061
- bytes: compiled.bytes,
1062
- rawSha256: compiled.rawSha256,
1063
- canonicalHash: compiled.planHash,
1064
- lease,
1065
- certify: true,
1066
- });
1067
- } finally {
1068
- this.#journal.releaseFinalizationLease(lease);
1069
- }
1070
- }
1071
-
1072
- async #finalizeDelivery(
1073
- request: Extract<DesignControlRequest, { operation: "finalize-delivery" }>,
1074
- ) {
1075
- const replay = this.#journal.operationOutcome(
1076
- request.runId,
1077
- request.operationId,
1078
- request.operation,
1079
- );
1080
- if (replay) {
1081
- this.#completeFinalizedRun(request.runId, request.operationId);
1082
- return replay;
1083
- }
1084
- const committed = this.#journal.latestFinalizationOutcome(request.runId);
1085
- if (committed) {
1086
- this.#completeFinalizedRun(request.runId, committed.operationId);
1087
- return committed.outcome;
1088
- }
1089
- const status = this.#journal.status(request.runId);
1090
- if (!status.change) throw new Error("design-change-required");
1091
- const gateA = proof(status, "gateA");
1092
- const gateB = proof(status, "gateB");
1093
- const storedPlan = this.#journal.currentCompiledPlan(request.runId);
1094
- if (!storedPlan) throw new DesignFinalizationError(["design-plan-missing"]);
1095
- const change = status.change;
1096
- const readyPath = safeRelative(change, "ready.yaml");
1097
- const gateAPath = safeRelative(change, "gate-a.yaml");
1098
- const lease = this.#journal.acquireFinalizationLease({
1099
- runId: request.runId,
1100
- operationId: request.operationId,
1101
- });
1102
- let readyInstalled = false;
1103
- let readyHash: string | undefined;
1104
- let finalizationCommitted = false;
1105
- try {
1106
- this.#journal.assertNoPendingArtifactOperations(request.runId);
1107
- removeSafeFile(this.consumerRoot, readyPath);
1108
- const diagnostics = new Set<string>();
1109
- let inspection: DesignOpenSpecInspection | undefined;
1110
- let openSpecDiagnostic: Readonly<OpenSpecDiagnostic> | undefined;
1111
- try {
1112
- inspection = await this.#inspectOpenSpec(this.consumerRoot, change);
1113
- } catch (error) {
1114
- if (error instanceof OpenSpecCliError)
1115
- openSpecDiagnostic = error.diagnostic;
1116
- diagnostics.add("design-openspec-unavailable");
1117
- }
1118
- if (inspection) {
1119
- if (inspection.change !== change)
1120
- diagnostics.add("design-change-mismatch");
1121
- if (!inspection.strictValid)
1122
- diagnostics.add("design-openspec-strict-invalid");
1123
- if (!inspection.planningComplete)
1124
- diagnostics.add("design-openspec-incomplete");
1125
- if (!inspection.schema)
1126
- diagnostics.add("design-openspec-schema-invalid");
1127
- }
1128
- const installedPlanBytes = readSafeFile(
1129
- this.consumerRoot,
1130
- safeRelative(change, "implement-plan.json"),
1131
- );
1132
- if (
1133
- !Buffer.from(installedPlanBytes).equals(Buffer.from(storedPlan.bytes))
1134
- ) {
1135
- diagnostics.add("design-plan-binding-invalid");
1136
- }
1137
- let compiled: ReturnType<typeof compileImplementPlan> | undefined;
1138
- try {
1139
- const plan = parseImplementPlan(installedPlanBytes, {
1140
- allowLegacyOrder: true,
1141
- });
1142
- compiled = compileImplementPlan(plan, {
1143
- consumerRoot: this.consumerRoot,
1144
- });
1145
- if (
1146
- compiled.planHash !== storedPlan.projection.canonicalHash ||
1147
- compiled.rawSha256 !== storedPlan.projection.rawSha256
1148
- ) {
1149
- diagnostics.add("design-plan-binding-invalid");
1150
- }
1151
- } catch {
1152
- diagnostics.add("design-plan-invalid");
1153
- }
1154
- const artifacts: DeliveryArtifactBinding[] = [];
1155
- const artifactBytes = new Map<string, Buffer>();
1156
- for (const relative of inspection?.artifactPaths ?? []) {
1157
- if (
1158
- [
1159
- "gate-a.yaml",
1160
- "ready.yaml",
1161
- "implement-plan.json",
1162
- "plan-draft.json",
1163
- ].includes(relative)
1164
- ) {
1165
- diagnostics.add(`design-artifact-reserved:${relative}`);
1166
- continue;
1167
- }
1168
- try {
1169
- const bytes = readSafeFile(
1170
- this.consumerRoot,
1171
- safeRelative(change, relative),
1172
- );
1173
- artifactBytes.set(relative, bytes);
1174
- artifacts.push({
1175
- path: relative,
1176
- rawSha256: createHash("sha256").update(bytes).digest("hex"),
1177
- });
1178
- } catch {
1179
- diagnostics.add(`design-artifact-unavailable:${relative}`);
1180
- }
1181
- }
1182
- let traceability:
1183
- | ReturnType<typeof assessDeliveryTraceability>
1184
- | undefined;
1185
- if (compiled && inspection) {
1186
- const tasks = artifactBytes.get("tasks.md");
1187
- const specs = [...artifactBytes]
1188
- .filter(
1189
- ([relative]) =>
1190
- relative.startsWith("specs/") && relative.endsWith("/spec.md"),
1191
- )
1192
- .map(([relative, bytes]) => ({
1193
- path: relative,
1194
- text: bytes.toString("utf8"),
1195
- }));
1196
- if (!tasks || specs.length === 0) {
1197
- diagnostics.add("design-traceability-input-unavailable");
1198
- } else {
1199
- traceability = assessDeliveryTraceability({
1200
- tasksMarkdown: tasks.toString("utf8"),
1201
- specs,
1202
- plan: compiled.plan,
1203
- });
1204
- if (!traceability.ok) {
1205
- for (const diagnostic of traceability.diagnostics)
1206
- diagnostics.add(diagnostic);
1207
- }
1208
- }
1209
- }
1210
- if (
1211
- !inspection ||
1212
- !compiled ||
1213
- !traceability?.ok ||
1214
- diagnostics.size > 0
1215
- ) {
1216
- throw new DesignFinalizationError([...diagnostics], openSpecDiagnostic);
1217
- }
1218
- const gateAReceipt = compileGateAReceipt({
1219
- change,
1220
- schema: inspection.schema,
1221
- approval: gateA,
1222
- artifacts,
1223
- });
1224
- const deliveryRevision = this.#journal.nextDeliveryRevision(
1225
- request.runId,
1226
- );
1227
- const readyReceipt = compileReadyReceipt({
1228
- change,
1229
- schema: inspection.schema,
1230
- deliveryRevision,
1231
- gateA: { rawSha256: gateAReceipt.rawSha256 },
1232
- gateB,
1233
- artifacts,
1234
- compiledPlan: compiled,
1235
- traceability: traceability.value,
1236
- });
1237
- readyHash = readyReceipt.rawSha256;
1238
- this.#assertFinalizationLease(lease);
1239
- atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
1240
- atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
1241
- readyInstalled = true;
1242
- const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
1243
- const installedReady = readSafeFile(this.consumerRoot, readyPath);
1244
- parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
1245
- parseReadyReceipt(installedReady, { allowLegacyOrder: true });
1246
- if (
1247
- !Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
1248
- !Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))
1249
- ) {
1250
- throw new Error("design-installed-delivery-mismatch");
1251
- }
1252
- removeSafeFile(
1253
- this.consumerRoot,
1254
- safeRelative(change, "plan-draft.json"),
1255
- );
1256
- const outcome = this.#journal.recordFinalization({
1257
- runId: request.runId,
1258
- operationId: request.operationId,
1259
- lease,
1260
- deliveryRevision,
1261
- receiptHash: readyReceipt.rawSha256,
1262
- gateA,
1263
- gateB,
1264
- planCanonicalHash: compiled.planHash,
1265
- });
1266
- finalizationCommitted = true;
1267
- this.#completeFinalizedRun(request.runId, request.operationId);
1268
- return outcome;
1269
- } catch (error) {
1270
- if (readyInstalled && readyHash && !finalizationCommitted) {
1271
- try {
1272
- removeSafeFileWithHash(this.consumerRoot, readyPath, readyHash);
1273
- } catch {
1274
- // The original finalization failure remains authoritative.
1275
- }
1276
- }
1277
- throw error;
1278
- } finally {
1279
- this.#journal.releaseFinalizationLease(lease);
1280
- }
1281
- }
1282
-
1283
- #completeFinalizedRun(runId: string, operationId: string): void {
1284
- const currentRun = this.#runs.status(runId);
1285
- if (currentRun.state === "completed") return;
1286
- this.#runs.transition({
1287
- runId,
1288
- to: "completed",
1289
- operationId: `${operationId}:design-completed`,
1290
- });
1291
- }
1292
-
1293
- close(): void {
1294
- if (this.#closed) return;
1295
- this.#journal.close();
1296
- this.#runs.close();
1297
- this.#closed = true;
1298
- }
1299
- }