@abelxiaoxing/cadence 1.8.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -117
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/agents/design-explorer.md +14 -18
- package/agents/diagnosis-worker.md +14 -14
- package/agents/implementation-worker.md +16 -17
- package/config/workflow-task.example.json +7 -0
- package/docs/remote-access.md +60 -0
- package/docs/reuse-and-distribution.md +44 -0
- package/docs/service-management.md +108 -0
- package/licenses/bundled-web.txt +6245 -0
- package/licenses/pi-web-ui-MIT.txt +21 -0
- package/licenses/pi-web-ui-reuse.md +22 -0
- package/package.json +92 -20
- package/src/index.ts +21 -1802
- package/src/native-build.ts +6 -0
- package/src/native-client.ts +103 -0
- package/src/native-export.ts +58 -0
- package/src/native-history.ts +359 -0
- package/src/operator-cli.mjs +453 -1937
- package/src/plan-proposal.ts +87 -0
- package/src/sdk-worker.mjs +549 -0
- package/src/service/contracts/active.ts +82 -0
- package/src/service/contracts/budget.ts +148 -0
- package/src/service/contracts/build.ts +67 -0
- package/src/service/contracts/capture.ts +48 -0
- package/src/service/contracts/core.ts +33 -0
- package/src/service/contracts/current.ts +261 -0
- package/src/service/contracts/domain.ts +68 -0
- package/src/service/contracts/history.ts +2245 -0
- package/src/service/contracts/models.ts +139 -0
- package/src/service/coordination.ts +862 -0
- package/src/service/private-files.ts +204 -0
- package/src/service-cli.mjs +5592 -0
- package/src/service-client.ts +747 -0
- package/src/service-contract.ts +245 -0
- package/src/service-host.mjs +22382 -0
- package/src/service-http-client.ts +425 -0
- package/src/service-wire.ts +372 -0
- package/src/task-shell.ts +1045 -0
- package/src/task-store.ts +479 -0
- package/src/task-types.ts +184 -0
- package/src/verification-diagnostics.ts +29 -72
- package/src/worker-capture.mjs +1197 -0
- package/src/worker-public.mjs +1753 -0
- package/src/worker-redaction.mjs +608 -0
- package/src/worker-tools.mjs +202 -0
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/cadence.css +2 -0
- package/web/dist/assets/cadence.js +7 -0
- package/web/dist/assets/markdown.js +296 -0
- package/web/dist/assets/react.js +9 -0
- package/web/dist/assets/rolldown-runtime.js +1 -0
- package/web/dist/assets/timeline-client.js +41 -0
- package/web/dist/assets/vis-timeline.css +1274 -0
- package/web/dist/assets/vis-timeline.js +32 -0
- package/web/dist/index.html +18 -0
- package/web/dist/manifest.json +505 -0
- package/config/plan-draft.example.json +0 -125
- package/config/plan-draft.multiple-tasks.example.json +0 -375
- package/config/plan-draft.quick.example.json +0 -78
- package/prompts/abel-design.md +0 -382
- package/prompts/abel-diagnose.md +0 -71
- package/prompts/abel-implement.md +0 -284
- package/prompts/abel-init.md +0 -52
- package/src/activation.ts +0 -44
- package/src/activity-contracts.ts +0 -82
- package/src/agent-registry.ts +0 -53
- package/src/apply-transaction.ts +0 -1437
- package/src/artifact-store.ts +0 -213
- package/src/build-windows-job.mjs +0 -95
- package/src/candidate-artifact.ts +0 -143
- package/src/candidate-context.ts +0 -289
- package/src/candidate-patch.ts +0 -271
- package/src/candidate-workspace.ts +0 -80
- package/src/canonical.ts +0 -16
- package/src/change-contract-schema.ts +0 -191
- package/src/change-contract.ts +0 -324
- package/src/change-verification.ts +0 -1617
- package/src/contracts.ts +0 -2466
- package/src/control-contracts.ts +0 -382
- package/src/delivery-compiler.ts +0 -1805
- package/src/delivery-revision.ts +0 -42
- package/src/design-control.ts +0 -1299
- package/src/design-diagnostics.ts +0 -181
- package/src/design-journal.ts +0 -1773
- package/src/directory-sync.ts +0 -36
- package/src/durable-contracts.ts +0 -227
- package/src/durable-workflow.ts +0 -822
- package/src/evidence-draft.ts +0 -122
- package/src/execution-profile.ts +0 -64
- package/src/execution-retention.ts +0 -77
- package/src/file-snapshot.ts +0 -163
- package/src/implement-continuation.ts +0 -559
- package/src/implement-graph.ts +0 -690
- package/src/implement-plan.ts +0 -72
- package/src/install-config.d.mts +0 -6
- package/src/isolation-backend.ts +0 -353
- package/src/model-source.ts +0 -22
- package/src/openspec-cli.ts +0 -494
- package/src/operator-cli-entry.mjs +0 -24
- package/src/operator-tools.ts +0 -309
- package/src/package-candidate-process.ts +0 -206
- package/src/package-candidate.ts +0 -24
- package/src/package-delivery.ts +0 -486
- package/src/package-state.ts +0 -320
- package/src/package-verification.ts +0 -635
- package/src/package-workflow.ts +0 -305
- package/src/packet-runtime.ts +0 -459
- package/src/phase-execution.ts +0 -2163
- package/src/pi-adapter.ts +0 -19
- package/src/plan-draft-summary.ts +0 -527
- package/src/plan-draft.ts +0 -502
- package/src/remove-path.ts +0 -32
- package/src/run-state.ts +0 -420
- package/src/run-store.ts +0 -1179
- package/src/safe-path.ts +0 -77
- package/src/scoped-grep-worker.mjs +0 -23
- package/src/scoped-tools.ts +0 -775
- package/src/single-task-draft.ts +0 -142
- package/src/sqlite-schema.ts +0 -171
- package/src/state-root.ts +0 -215
- package/src/storage-schema.ts +0 -360
- package/src/subagent-activity.ts +0 -1417
- package/src/subagent-process.ts +0 -476
- package/src/task-ledger.ts +0 -1338
- package/src/transport-budget.ts +0 -50
- package/src/verification-capability.ts +0 -1191
- package/src/verification-environment-io.ts +0 -389
- package/src/verification-environment.ts +0 -162
- package/src/verification-identity.ts +0 -77
- package/src/verification-scan.ts +0 -64
- package/src/windows-job-backend.ts +0 -419
- package/src/windows-job.c +0 -149
- package/src/workflow-engine.ts +0 -28
- package/src/workflow-policy.ts +0 -1670
- package/src/workflow-recovery-policy.ts +0 -97
- package/src/workflow-scheduling.ts +0 -105
- package/src/workflow-state-machine.ts +0 -4341
- package/src/workflow-status.ts +0 -576
- package/src/workspace-io-entry.mjs +0 -76
- package/src/workspace-io-worker.mjs +0 -1312
- package/src/workspace-io.ts +0 -89
- package/src/workspace-store.ts +0 -634
package/src/design-control.ts
DELETED
|
@@ -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
|
-
}
|