@abelxiaoxing/cadence 1.1.1 → 1.2.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 +8 -2
- package/agents/implementation-worker.md +17 -11
- package/config/routes.example.json +0 -2
- package/package.json +2 -2
- package/prompts/abel-design.md +20 -16
- package/prompts/abel-implement.md +4 -3
- package/skills/abel-workflow/SKILL.md +6 -3
- package/src/agent-registry.ts +0 -1
- package/src/candidate-patch.ts +250 -0
- package/src/child-session.ts +27 -24
- package/src/contracts.ts +1 -8
- package/src/control-contracts.ts +72 -48
- package/src/design-control.ts +224 -17
- package/src/design-journal.ts +69 -22
- package/src/index.ts +81 -23
- package/src/packet-runtime.ts +6 -6
- package/src/route-policy.ts +45 -17
- package/src/run-state.ts +4 -2
- package/src/run-store.ts +22 -6
- package/src/submit-tool.ts +149 -49
- package/src/worker-broker.ts +0 -1
- package/src/workflow-engine.ts +22 -171
- package/agents/contract-reviewer.md +0 -11
package/src/control-contracts.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const CONTROL_COMMANDS = [
|
|
|
11
11
|
"discard",
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
|
-
export const CONTROL_STAGES = ["abel-
|
|
14
|
+
export const CONTROL_STAGES = ["abel-implement"] as const;
|
|
15
15
|
|
|
16
16
|
export type ControlCommandName = (typeof CONTROL_COMMANDS)[number];
|
|
17
17
|
export type ControlStage = (typeof CONTROL_STAGES)[number];
|
|
@@ -23,52 +23,37 @@ const SHA256 = /^[a-f0-9]{64}$/u;
|
|
|
23
23
|
interface ControlBase {
|
|
24
24
|
command: ControlCommandName;
|
|
25
25
|
stage: ControlStage;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface NamedControlBase extends ControlBase {
|
|
29
26
|
change: string;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
export interface
|
|
33
|
-
command: "start";
|
|
34
|
-
operationId: string;
|
|
35
|
-
provisionalKey?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ProvisionalDesignStartControlCommand extends ControlBase {
|
|
29
|
+
export interface StartControlCommand extends ControlBase {
|
|
39
30
|
command: "start";
|
|
40
|
-
stage: "abel-design";
|
|
41
|
-
provisionalKey: string;
|
|
42
31
|
operationId: string;
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
export
|
|
46
|
-
| NamedStartControlCommand
|
|
47
|
-
| ProvisionalDesignStartControlCommand;
|
|
48
|
-
|
|
49
|
-
export interface StatusControlCommand extends NamedControlBase {
|
|
34
|
+
export interface StatusControlCommand extends ControlBase {
|
|
50
35
|
command: "status";
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
export interface ResumeControlCommand extends
|
|
38
|
+
export interface ResumeControlCommand extends ControlBase {
|
|
54
39
|
command: "resume";
|
|
55
40
|
operationId: string;
|
|
56
41
|
deliveryRevision?: number;
|
|
57
42
|
receiptHash?: string;
|
|
58
43
|
}
|
|
59
44
|
|
|
60
|
-
export interface RebindControlCommand extends
|
|
45
|
+
export interface RebindControlCommand extends ControlBase {
|
|
61
46
|
command: "rebind";
|
|
62
47
|
operationId: string;
|
|
63
48
|
routeId: string;
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
export interface CancelControlCommand extends
|
|
51
|
+
export interface CancelControlCommand extends ControlBase {
|
|
67
52
|
command: "cancel";
|
|
68
53
|
operationId: string;
|
|
69
54
|
}
|
|
70
55
|
|
|
71
|
-
export interface DiscardControlCommand extends
|
|
56
|
+
export interface DiscardControlCommand extends ControlBase {
|
|
72
57
|
command: "discard";
|
|
73
58
|
operationId: string;
|
|
74
59
|
}
|
|
@@ -89,6 +74,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
89
74
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
90
75
|
}
|
|
91
76
|
|
|
77
|
+
const CONTROL_TOOL_FIELDS = new Set([
|
|
78
|
+
"command",
|
|
79
|
+
"stage",
|
|
80
|
+
"change",
|
|
81
|
+
"operationId",
|
|
82
|
+
"deliveryRevision",
|
|
83
|
+
"receiptHash",
|
|
84
|
+
"routeId",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const COMMAND_FIELDS: Readonly<
|
|
88
|
+
Record<ControlCommandName, ReadonlySet<string>>
|
|
89
|
+
> = Object.freeze({
|
|
90
|
+
start: new Set(["command", "stage", "change", "operationId"]),
|
|
91
|
+
status: new Set(["command", "stage", "change"]),
|
|
92
|
+
resume: new Set([
|
|
93
|
+
"command",
|
|
94
|
+
"stage",
|
|
95
|
+
"change",
|
|
96
|
+
"operationId",
|
|
97
|
+
"deliveryRevision",
|
|
98
|
+
"receiptHash",
|
|
99
|
+
]),
|
|
100
|
+
rebind: new Set(["command", "stage", "change", "operationId", "routeId"]),
|
|
101
|
+
cancel: new Set(["command", "stage", "change", "operationId"]),
|
|
102
|
+
discard: new Set(["command", "stage", "change", "operationId"]),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Adapts Pi/provider strict-schema padding to the closed command union.
|
|
107
|
+
* Known fields belonging to a different command are projected away, while
|
|
108
|
+
* unknown fields and fields owned by the selected command remain available to
|
|
109
|
+
* the strict validator and therefore cannot bypass its exact-key/type checks.
|
|
110
|
+
*/
|
|
111
|
+
export function canonicalizeControlCommandToolInput(value: unknown): unknown {
|
|
112
|
+
if (
|
|
113
|
+
!isRecord(value) ||
|
|
114
|
+
!(CONTROL_COMMANDS as readonly unknown[]).includes(value.command)
|
|
115
|
+
) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const command = value.command as ControlCommandName;
|
|
119
|
+
const allowed = COMMAND_FIELDS[command];
|
|
120
|
+
const canonical = Object.create(null) as Record<string, unknown>;
|
|
121
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
122
|
+
if (CONTROL_TOOL_FIELDS.has(key) && !allowed.has(key)) continue;
|
|
123
|
+
if (
|
|
124
|
+
command === "resume" &&
|
|
125
|
+
(key === "deliveryRevision" || key === "receiptHash") &&
|
|
126
|
+
(entry === null || entry === undefined)
|
|
127
|
+
) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
canonical[key] = entry;
|
|
131
|
+
}
|
|
132
|
+
return canonical;
|
|
133
|
+
}
|
|
134
|
+
|
|
92
135
|
function hasExactKeys(
|
|
93
136
|
value: Record<string, unknown>,
|
|
94
137
|
required: readonly string[],
|
|
@@ -125,11 +168,10 @@ export function validateControlCommand(
|
|
|
125
168
|
return { ok: false, code: "invalid-control-command" };
|
|
126
169
|
}
|
|
127
170
|
|
|
128
|
-
const common = ["command", "stage"] as const;
|
|
129
|
-
const named = [...common, "change"] as const;
|
|
171
|
+
const common = ["command", "stage", "change"] as const;
|
|
130
172
|
switch (value.command) {
|
|
131
173
|
case "status":
|
|
132
|
-
if (!hasExactKeys(value,
|
|
174
|
+
if (!hasExactKeys(value, common) || !validChange(value.change)) break;
|
|
133
175
|
return {
|
|
134
176
|
ok: true,
|
|
135
177
|
value: structuredClone(value) as unknown as StatusControlCommand,
|
|
@@ -137,7 +179,7 @@ export function validateControlCommand(
|
|
|
137
179
|
case "cancel":
|
|
138
180
|
case "discard":
|
|
139
181
|
if (
|
|
140
|
-
hasExactKeys(value, [...
|
|
182
|
+
hasExactKeys(value, [...common, "operationId"]) &&
|
|
141
183
|
validChange(value.change) &&
|
|
142
184
|
validOperationId(value.operationId)
|
|
143
185
|
) {
|
|
@@ -149,37 +191,19 @@ export function validateControlCommand(
|
|
|
149
191
|
break;
|
|
150
192
|
case "start":
|
|
151
193
|
if (
|
|
152
|
-
hasExactKeys(value, [...
|
|
194
|
+
hasExactKeys(value, [...common, "operationId"]) &&
|
|
153
195
|
validChange(value.change) &&
|
|
154
|
-
validOperationId(value.operationId) &&
|
|
155
|
-
(value.provisionalKey === undefined ||
|
|
156
|
-
(value.stage === "abel-design" &&
|
|
157
|
-
typeof value.provisionalKey === "string" &&
|
|
158
|
-
SHA256.test(value.provisionalKey)))
|
|
159
|
-
) {
|
|
160
|
-
return {
|
|
161
|
-
ok: true,
|
|
162
|
-
value: structuredClone(value) as unknown as NamedStartControlCommand,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
if (
|
|
166
|
-
hasExactKeys(value, [...common, "provisionalKey", "operationId"]) &&
|
|
167
|
-
value.stage === "abel-design" &&
|
|
168
|
-
typeof value.provisionalKey === "string" &&
|
|
169
|
-
SHA256.test(value.provisionalKey) &&
|
|
170
196
|
validOperationId(value.operationId)
|
|
171
197
|
) {
|
|
172
198
|
return {
|
|
173
199
|
ok: true,
|
|
174
|
-
value: structuredClone(
|
|
175
|
-
value,
|
|
176
|
-
) as unknown as ProvisionalDesignStartControlCommand,
|
|
200
|
+
value: structuredClone(value) as unknown as StartControlCommand,
|
|
177
201
|
};
|
|
178
202
|
}
|
|
179
203
|
break;
|
|
180
204
|
case "rebind":
|
|
181
205
|
if (
|
|
182
|
-
hasExactKeys(value, [...
|
|
206
|
+
hasExactKeys(value, [...common, "operationId", "routeId"]) &&
|
|
183
207
|
validChange(value.change) &&
|
|
184
208
|
validOperationId(value.operationId) &&
|
|
185
209
|
typeof value.routeId === "string" &&
|
|
@@ -195,7 +219,7 @@ export function validateControlCommand(
|
|
|
195
219
|
if (
|
|
196
220
|
!hasExactKeys(
|
|
197
221
|
value,
|
|
198
|
-
[...
|
|
222
|
+
[...common, "operationId"],
|
|
199
223
|
["deliveryRevision", "receiptHash"],
|
|
200
224
|
) ||
|
|
201
225
|
!validChange(value.change) ||
|
package/src/design-control.ts
CHANGED
|
@@ -32,9 +32,10 @@ import type { ResolvedStateRoot } from "./state-root.ts";
|
|
|
32
32
|
|
|
33
33
|
const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
34
34
|
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
35
|
-
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
36
35
|
const ARTIFACT_SEGMENT = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/iu;
|
|
37
36
|
const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
|
|
37
|
+
const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
|
|
38
|
+
const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
|
|
38
39
|
|
|
39
40
|
export interface DesignOpenSpecInspection {
|
|
40
41
|
change: string;
|
|
@@ -45,21 +46,47 @@ export interface DesignOpenSpecInspection {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export type DesignControlRequest =
|
|
49
|
+
| {
|
|
50
|
+
operation: "start";
|
|
51
|
+
operationId: string;
|
|
52
|
+
requirement: string;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
operation: "start";
|
|
56
|
+
operationId: string;
|
|
57
|
+
change: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
operation: "status";
|
|
61
|
+
runId: string;
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
operation: "bind-change";
|
|
65
|
+
runId: string;
|
|
66
|
+
operationId: string;
|
|
67
|
+
change: string;
|
|
68
|
+
}
|
|
48
69
|
| {
|
|
49
70
|
operation: "record-decision";
|
|
50
71
|
runId: string;
|
|
51
72
|
operationId: string;
|
|
52
73
|
decisionId: string;
|
|
53
74
|
category: "behavior" | "technical";
|
|
54
|
-
|
|
75
|
+
contract: string;
|
|
55
76
|
refs: string[];
|
|
56
77
|
}
|
|
57
78
|
| {
|
|
58
79
|
operation: "approve-gate";
|
|
59
80
|
runId: string;
|
|
60
81
|
operationId: string;
|
|
61
|
-
gate: "gate-a"
|
|
62
|
-
|
|
82
|
+
gate: "gate-a";
|
|
83
|
+
contract: string;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
operation: "approve-gate";
|
|
87
|
+
runId: string;
|
|
88
|
+
operationId: string;
|
|
89
|
+
gate: "gate-b";
|
|
63
90
|
}
|
|
64
91
|
| {
|
|
65
92
|
operation: "compile-plan";
|
|
@@ -121,6 +148,25 @@ function exactKeys(
|
|
|
121
148
|
);
|
|
122
149
|
}
|
|
123
150
|
|
|
151
|
+
function normalizedTransientText(value: string): string {
|
|
152
|
+
return value.replace(/\r\n?/gu, "\n").trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function transientHash(domain: string, value: string): string {
|
|
156
|
+
return createHash("sha256")
|
|
157
|
+
.update(domain)
|
|
158
|
+
.update("\0")
|
|
159
|
+
.update(normalizedTransientText(value))
|
|
160
|
+
.digest("hex");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function lifecycleOperationId(kind: string, runId: string): string {
|
|
164
|
+
return `design-${kind}-${createHash("sha256")
|
|
165
|
+
.update(runId)
|
|
166
|
+
.digest("hex")
|
|
167
|
+
.slice(0, 32)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
124
170
|
export function validateDesignControlRequest(
|
|
125
171
|
value: unknown,
|
|
126
172
|
):
|
|
@@ -128,6 +174,37 @@ export function validateDesignControlRequest(
|
|
|
128
174
|
| { ok: false; code: "invalid-design-control-request" } {
|
|
129
175
|
if (!isRecord(value))
|
|
130
176
|
return { ok: false, code: "invalid-design-control-request" };
|
|
177
|
+
if (value.operation === "start") {
|
|
178
|
+
const operationValid =
|
|
179
|
+
typeof value.operationId === "string" &&
|
|
180
|
+
IDENTIFIER.test(value.operationId);
|
|
181
|
+
if (!operationValid)
|
|
182
|
+
return { ok: false, code: "invalid-design-control-request" };
|
|
183
|
+
if (
|
|
184
|
+
exactKeys(value, ["operation", "operationId", "requirement"]) &&
|
|
185
|
+
typeof value.requirement === "string" &&
|
|
186
|
+
value.requirement.trim().length > 0 &&
|
|
187
|
+
Buffer.byteLength(value.requirement, "utf8") <=
|
|
188
|
+
MAX_DESIGN_REQUIREMENT_BYTES
|
|
189
|
+
) {
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
value: structuredClone(value) as DesignControlRequest,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return exactKeys(value, ["operation", "operationId", "change"]) &&
|
|
196
|
+
typeof value.change === "string" &&
|
|
197
|
+
CHANGE_NAME.test(value.change)
|
|
198
|
+
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
199
|
+
: { ok: false, code: "invalid-design-control-request" };
|
|
200
|
+
}
|
|
201
|
+
if (value.operation === "status") {
|
|
202
|
+
return exactKeys(value, ["operation", "runId"]) &&
|
|
203
|
+
typeof value.runId === "string" &&
|
|
204
|
+
IDENTIFIER.test(value.runId)
|
|
205
|
+
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
206
|
+
: { ok: false, code: "invalid-design-control-request" };
|
|
207
|
+
}
|
|
131
208
|
const commonValid =
|
|
132
209
|
typeof value.runId === "string" &&
|
|
133
210
|
IDENTIFIER.test(value.runId) &&
|
|
@@ -135,6 +212,13 @@ export function validateDesignControlRequest(
|
|
|
135
212
|
IDENTIFIER.test(value.operationId);
|
|
136
213
|
if (!commonValid)
|
|
137
214
|
return { ok: false, code: "invalid-design-control-request" };
|
|
215
|
+
if (value.operation === "bind-change") {
|
|
216
|
+
return exactKeys(value, ["operation", "runId", "operationId", "change"]) &&
|
|
217
|
+
typeof value.change === "string" &&
|
|
218
|
+
CHANGE_NAME.test(value.change)
|
|
219
|
+
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
220
|
+
: { ok: false, code: "invalid-design-control-request" };
|
|
221
|
+
}
|
|
138
222
|
if (value.operation === "write-artifact") {
|
|
139
223
|
return exactKeys(value, [
|
|
140
224
|
"operation",
|
|
@@ -163,16 +247,22 @@ export function validateDesignControlRequest(
|
|
|
163
247
|
: { ok: false, code: "invalid-design-control-request" };
|
|
164
248
|
}
|
|
165
249
|
if (value.operation === "approve-gate") {
|
|
166
|
-
|
|
167
|
-
"operation",
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
250
|
+
if (value.gate === "gate-b") {
|
|
251
|
+
return exactKeys(value, ["operation", "runId", "operationId", "gate"])
|
|
252
|
+
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
253
|
+
: { ok: false, code: "invalid-design-control-request" };
|
|
254
|
+
}
|
|
255
|
+
return value.gate === "gate-a" &&
|
|
256
|
+
exactKeys(value, [
|
|
257
|
+
"operation",
|
|
258
|
+
"runId",
|
|
259
|
+
"operationId",
|
|
260
|
+
"gate",
|
|
261
|
+
"contract",
|
|
262
|
+
]) &&
|
|
263
|
+
typeof value.contract === "string" &&
|
|
264
|
+
value.contract.trim().length > 0 &&
|
|
265
|
+
Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
|
|
176
266
|
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
177
267
|
: { ok: false, code: "invalid-design-control-request" };
|
|
178
268
|
}
|
|
@@ -185,14 +275,15 @@ export function validateDesignControlRequest(
|
|
|
185
275
|
"operationId",
|
|
186
276
|
"decisionId",
|
|
187
277
|
"category",
|
|
188
|
-
"
|
|
278
|
+
"contract",
|
|
189
279
|
"refs",
|
|
190
280
|
]) &&
|
|
191
281
|
typeof value.decisionId === "string" &&
|
|
192
282
|
IDENTIFIER.test(value.decisionId) &&
|
|
193
283
|
(value.category === "behavior" || value.category === "technical") &&
|
|
194
|
-
typeof value.
|
|
195
|
-
|
|
284
|
+
typeof value.contract === "string" &&
|
|
285
|
+
value.contract.trim().length > 0 &&
|
|
286
|
+
Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES &&
|
|
196
287
|
Array.isArray(value.refs) &&
|
|
197
288
|
value.refs.every((ref) => typeof ref === "string")
|
|
198
289
|
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
@@ -407,6 +498,12 @@ export class DesignController {
|
|
|
407
498
|
if (!validation.ok) throw new Error(validation.code);
|
|
408
499
|
const request = validation.value;
|
|
409
500
|
switch (request.operation) {
|
|
501
|
+
case "start":
|
|
502
|
+
return this.#start(request);
|
|
503
|
+
case "status":
|
|
504
|
+
return this.#statusOutcome(request.runId);
|
|
505
|
+
case "bind-change":
|
|
506
|
+
return this.#bindChange(request);
|
|
410
507
|
case "record-decision":
|
|
411
508
|
return this.#journal.recordDecision(request);
|
|
412
509
|
case "approve-gate":
|
|
@@ -422,6 +519,116 @@ export class DesignController {
|
|
|
422
519
|
}
|
|
423
520
|
}
|
|
424
521
|
|
|
522
|
+
#statusOutcome(runId: string): Record<string, unknown> {
|
|
523
|
+
const run = this.#runs.status(runId);
|
|
524
|
+
if (run.stage !== "abel-design") throw new Error("design-run-invalid");
|
|
525
|
+
const terminal = ["completed", "discarded", "rejected"].includes(run.state);
|
|
526
|
+
const design = this.#journal.status(runId);
|
|
527
|
+
const latestDecisionSequence = design.decisions.reduce(
|
|
528
|
+
(latest, decision) => Math.max(latest, decision.sequence),
|
|
529
|
+
0,
|
|
530
|
+
);
|
|
531
|
+
const planReadyForGateB =
|
|
532
|
+
design.gates.gateA.current &&
|
|
533
|
+
design.plan !== null &&
|
|
534
|
+
design.plan.sequence >
|
|
535
|
+
Math.max(
|
|
536
|
+
design.gates.gateA.approvedSequence ?? 0,
|
|
537
|
+
latestDecisionSequence,
|
|
538
|
+
);
|
|
539
|
+
const canApproveGate =
|
|
540
|
+
!design.gates.gateA.current ||
|
|
541
|
+
(planReadyForGateB && !design.gates.gateB.current);
|
|
542
|
+
const legalActions = terminal
|
|
543
|
+
? ["status"]
|
|
544
|
+
: [
|
|
545
|
+
"status",
|
|
546
|
+
"record-decision",
|
|
547
|
+
...(canApproveGate ? ["approve-gate"] : []),
|
|
548
|
+
...(design.gates.gateA.current && !design.change
|
|
549
|
+
? ["bind-change"]
|
|
550
|
+
: []),
|
|
551
|
+
...(design.gates.gateA.current && design.change
|
|
552
|
+
? ["write-artifact", "delete-artifact", "compile-plan"]
|
|
553
|
+
: []),
|
|
554
|
+
...(design.change &&
|
|
555
|
+
design.plan &&
|
|
556
|
+
design.gates.gateA.current &&
|
|
557
|
+
design.gates.gateB.current
|
|
558
|
+
? ["finalize-delivery"]
|
|
559
|
+
: []),
|
|
560
|
+
"finish",
|
|
561
|
+
];
|
|
562
|
+
return {
|
|
563
|
+
runId: run.runId,
|
|
564
|
+
stage: run.stage,
|
|
565
|
+
...(run.change ? { change: run.change } : {}),
|
|
566
|
+
state: run.state,
|
|
567
|
+
completed: run.state === "completed",
|
|
568
|
+
...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
|
|
569
|
+
legalActions,
|
|
570
|
+
design,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#start(request: Extract<DesignControlRequest, { operation: "start" }>) {
|
|
575
|
+
const fromRequirement = "requirement" in request;
|
|
576
|
+
const run = this.#runs.startRun({
|
|
577
|
+
stage: "abel-design",
|
|
578
|
+
...(fromRequirement
|
|
579
|
+
? {
|
|
580
|
+
provisionalKey: transientHash(
|
|
581
|
+
"abel-design-requirement-v1",
|
|
582
|
+
request.requirement,
|
|
583
|
+
),
|
|
584
|
+
}
|
|
585
|
+
: { change: request.change }),
|
|
586
|
+
operationId: request.operationId,
|
|
587
|
+
});
|
|
588
|
+
if (run.state === "created") {
|
|
589
|
+
this.#runs.transition({
|
|
590
|
+
runId: run.runId,
|
|
591
|
+
to: "paused",
|
|
592
|
+
operationId: lifecycleOperationId(
|
|
593
|
+
fromRequirement ? "await-gate-a" : "await-evidence",
|
|
594
|
+
run.runId,
|
|
595
|
+
),
|
|
596
|
+
code: fromRequirement
|
|
597
|
+
? "design-awaiting-gate-a"
|
|
598
|
+
: "design-awaiting-evidence",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return this.#statusOutcome(run.runId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#bindChange(
|
|
605
|
+
request: Extract<DesignControlRequest, { operation: "bind-change" }>,
|
|
606
|
+
) {
|
|
607
|
+
const run = this.#runs.status(request.runId);
|
|
608
|
+
if (run.change && run.change !== request.change) {
|
|
609
|
+
throw new Error("design-operation-conflict");
|
|
610
|
+
}
|
|
611
|
+
if (!run.change) {
|
|
612
|
+
const design = this.#journal.status(request.runId);
|
|
613
|
+
if (!design.gates.gateA.current)
|
|
614
|
+
throw new Error("design-gate-a-required");
|
|
615
|
+
}
|
|
616
|
+
const bound = this.#runs.bindChange({
|
|
617
|
+
runId: request.runId,
|
|
618
|
+
change: request.change,
|
|
619
|
+
operationId: request.operationId,
|
|
620
|
+
});
|
|
621
|
+
if (bound.state === "paused") {
|
|
622
|
+
this.#runs.transition({
|
|
623
|
+
runId: request.runId,
|
|
624
|
+
to: "paused",
|
|
625
|
+
operationId: lifecycleOperationId("bound", request.runId),
|
|
626
|
+
code: "design-awaiting-evidence",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return this.#statusOutcome(request.runId);
|
|
630
|
+
}
|
|
631
|
+
|
|
425
632
|
#artifactRun(runId: string): { change: string; gateACurrent: boolean } {
|
|
426
633
|
const status = this.#journal.status(runId);
|
|
427
634
|
if (!status.change) throw new Error("design-change-required");
|