@abelxiaoxing/cadence 1.1.1 → 1.2.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.
- 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 +14 -48
- package/src/design-control.ts +224 -17
- package/src/design-journal.ts +69 -22
- package/src/index.ts +67 -20
- 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
|
|
29
|
+
export interface StartControlCommand extends ControlBase {
|
|
33
30
|
command: "start";
|
|
34
31
|
operationId: string;
|
|
35
|
-
provisionalKey?: string;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
export interface
|
|
39
|
-
command: "start";
|
|
40
|
-
stage: "abel-design";
|
|
41
|
-
provisionalKey: string;
|
|
42
|
-
operationId: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type StartControlCommand =
|
|
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
|
}
|
|
@@ -125,11 +110,10 @@ export function validateControlCommand(
|
|
|
125
110
|
return { ok: false, code: "invalid-control-command" };
|
|
126
111
|
}
|
|
127
112
|
|
|
128
|
-
const common = ["command", "stage"] as const;
|
|
129
|
-
const named = [...common, "change"] as const;
|
|
113
|
+
const common = ["command", "stage", "change"] as const;
|
|
130
114
|
switch (value.command) {
|
|
131
115
|
case "status":
|
|
132
|
-
if (!hasExactKeys(value,
|
|
116
|
+
if (!hasExactKeys(value, common) || !validChange(value.change)) break;
|
|
133
117
|
return {
|
|
134
118
|
ok: true,
|
|
135
119
|
value: structuredClone(value) as unknown as StatusControlCommand,
|
|
@@ -137,7 +121,7 @@ export function validateControlCommand(
|
|
|
137
121
|
case "cancel":
|
|
138
122
|
case "discard":
|
|
139
123
|
if (
|
|
140
|
-
hasExactKeys(value, [...
|
|
124
|
+
hasExactKeys(value, [...common, "operationId"]) &&
|
|
141
125
|
validChange(value.change) &&
|
|
142
126
|
validOperationId(value.operationId)
|
|
143
127
|
) {
|
|
@@ -149,37 +133,19 @@ export function validateControlCommand(
|
|
|
149
133
|
break;
|
|
150
134
|
case "start":
|
|
151
135
|
if (
|
|
152
|
-
hasExactKeys(value, [...
|
|
136
|
+
hasExactKeys(value, [...common, "operationId"]) &&
|
|
153
137
|
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
138
|
validOperationId(value.operationId)
|
|
171
139
|
) {
|
|
172
140
|
return {
|
|
173
141
|
ok: true,
|
|
174
|
-
value: structuredClone(
|
|
175
|
-
value,
|
|
176
|
-
) as unknown as ProvisionalDesignStartControlCommand,
|
|
142
|
+
value: structuredClone(value) as unknown as StartControlCommand,
|
|
177
143
|
};
|
|
178
144
|
}
|
|
179
145
|
break;
|
|
180
146
|
case "rebind":
|
|
181
147
|
if (
|
|
182
|
-
hasExactKeys(value, [...
|
|
148
|
+
hasExactKeys(value, [...common, "operationId", "routeId"]) &&
|
|
183
149
|
validChange(value.change) &&
|
|
184
150
|
validOperationId(value.operationId) &&
|
|
185
151
|
typeof value.routeId === "string" &&
|
|
@@ -195,7 +161,7 @@ export function validateControlCommand(
|
|
|
195
161
|
if (
|
|
196
162
|
!hasExactKeys(
|
|
197
163
|
value,
|
|
198
|
-
[...
|
|
164
|
+
[...common, "operationId"],
|
|
199
165
|
["deliveryRevision", "receiptHash"],
|
|
200
166
|
) ||
|
|
201
167
|
!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");
|
package/src/design-journal.ts
CHANGED
|
@@ -11,6 +11,7 @@ const SHA256 = /^[a-f0-9]{64}$/u;
|
|
|
11
11
|
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
12
12
|
const ZERO_HASH = "0".repeat(64);
|
|
13
13
|
const MAX_PLAN_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const MAX_CONTRACT_BYTES = 256 * 1024;
|
|
14
15
|
|
|
15
16
|
const SCHEMA = `
|
|
16
17
|
CREATE TABLE IF NOT EXISTS design_facts (
|
|
@@ -144,6 +145,7 @@ export interface DesignGateProjection {
|
|
|
144
145
|
| "behavior-decision-changed"
|
|
145
146
|
| "technical-decision-changed"
|
|
146
147
|
| "plan-recompiled"
|
|
148
|
+
| "gate-a-reapproved"
|
|
147
149
|
| "gate-a-stale"
|
|
148
150
|
| "plan-missing";
|
|
149
151
|
}
|
|
@@ -223,6 +225,21 @@ function sha256(value: Uint8Array | string): string {
|
|
|
223
225
|
return createHash("sha256").update(value).digest("hex");
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
function contractHash(domain: string, value: string): string {
|
|
229
|
+
if (
|
|
230
|
+
typeof value !== "string" ||
|
|
231
|
+
value.trim().length === 0 ||
|
|
232
|
+
Buffer.byteLength(value, "utf8") > MAX_CONTRACT_BYTES
|
|
233
|
+
) {
|
|
234
|
+
throw new Error("design-transient-input-invalid");
|
|
235
|
+
}
|
|
236
|
+
return createHash("sha256")
|
|
237
|
+
.update(domain)
|
|
238
|
+
.update("\0")
|
|
239
|
+
.update(value.replace(/\r\n?/gu, "\n").trim())
|
|
240
|
+
.digest("hex");
|
|
241
|
+
}
|
|
242
|
+
|
|
226
243
|
function requireIdentifier(value: string, label: string): void {
|
|
227
244
|
if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
|
|
228
245
|
}
|
|
@@ -877,11 +894,14 @@ export class DesignJournal {
|
|
|
877
894
|
operationId: string;
|
|
878
895
|
decisionId: string;
|
|
879
896
|
category: DecisionCategory;
|
|
880
|
-
|
|
897
|
+
contract: string;
|
|
881
898
|
refs: string[];
|
|
882
899
|
}): Record<string, unknown> {
|
|
883
900
|
requireIdentifier(input.decisionId, "design-decision-id");
|
|
884
|
-
|
|
901
|
+
const decisionContractHash = contractHash(
|
|
902
|
+
"abel-design-decision-v1",
|
|
903
|
+
input.contract,
|
|
904
|
+
);
|
|
885
905
|
if (input.category !== "behavior" && input.category !== "technical") {
|
|
886
906
|
throw new Error("design-decision-category-invalid");
|
|
887
907
|
}
|
|
@@ -890,7 +910,7 @@ export class DesignJournal {
|
|
|
890
910
|
operation: "record-decision",
|
|
891
911
|
decisionId: input.decisionId,
|
|
892
912
|
category: input.category,
|
|
893
|
-
contractHash:
|
|
913
|
+
contractHash: decisionContractHash,
|
|
894
914
|
refs,
|
|
895
915
|
};
|
|
896
916
|
return this.#transaction(() => {
|
|
@@ -909,7 +929,7 @@ export class DesignJournal {
|
|
|
909
929
|
let decision: DesignDecisionProjection;
|
|
910
930
|
if (
|
|
911
931
|
prior &&
|
|
912
|
-
prior.contractHash ===
|
|
932
|
+
prior.contractHash === decisionContractHash &&
|
|
913
933
|
canonicalJson(prior.refs) === canonicalJson(refs)
|
|
914
934
|
) {
|
|
915
935
|
decision = prior;
|
|
@@ -919,7 +939,7 @@ export class DesignJournal {
|
|
|
919
939
|
decisionId: input.decisionId,
|
|
920
940
|
category: input.category,
|
|
921
941
|
revision,
|
|
922
|
-
contractHash:
|
|
942
|
+
contractHash: decisionContractHash,
|
|
923
943
|
refs,
|
|
924
944
|
};
|
|
925
945
|
const fact = this.#appendFact(
|
|
@@ -980,12 +1000,20 @@ export class DesignJournal {
|
|
|
980
1000
|
return undefined;
|
|
981
1001
|
}
|
|
982
1002
|
|
|
983
|
-
approveGate(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1003
|
+
approveGate(
|
|
1004
|
+
input:
|
|
1005
|
+
| {
|
|
1006
|
+
runId: string;
|
|
1007
|
+
operationId: string;
|
|
1008
|
+
gate: "gate-a";
|
|
1009
|
+
contract: string;
|
|
1010
|
+
}
|
|
1011
|
+
| {
|
|
1012
|
+
runId: string;
|
|
1013
|
+
operationId: string;
|
|
1014
|
+
gate: "gate-b";
|
|
1015
|
+
},
|
|
1016
|
+
): {
|
|
989
1017
|
operation: "approve-gate";
|
|
990
1018
|
runId: string;
|
|
991
1019
|
gate: DesignGate;
|
|
@@ -994,14 +1022,17 @@ export class DesignJournal {
|
|
|
994
1022
|
if (input.gate !== "gate-a" && input.gate !== "gate-b") {
|
|
995
1023
|
throw new Error("design-gate-invalid");
|
|
996
1024
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
contractHash: input.contractHash,
|
|
1002
|
-
};
|
|
1025
|
+
const gateAContractHash =
|
|
1026
|
+
input.gate === "gate-a"
|
|
1027
|
+
? contractHash("abel-design-gate-a-v1", input.contract)
|
|
1028
|
+
: undefined;
|
|
1003
1029
|
return this.#transaction(() => {
|
|
1004
1030
|
this.#run(input.runId, true);
|
|
1031
|
+
const request = {
|
|
1032
|
+
operation: "approve-gate",
|
|
1033
|
+
gate: input.gate,
|
|
1034
|
+
...(gateAContractHash ? { contractHash: gateAContractHash } : {}),
|
|
1035
|
+
};
|
|
1005
1036
|
const replay = this.#operationReplay(
|
|
1006
1037
|
input.runId,
|
|
1007
1038
|
input.operationId,
|
|
@@ -1012,15 +1043,26 @@ export class DesignJournal {
|
|
|
1012
1043
|
return structuredClone(replay) as ReturnType<
|
|
1013
1044
|
DesignJournal["approveGate"]
|
|
1014
1045
|
>;
|
|
1046
|
+
let approvedContractHash = gateAContractHash;
|
|
1015
1047
|
if (input.gate === "gate-b") {
|
|
1016
1048
|
const current = this.#gateStatus(input.runId);
|
|
1017
1049
|
if (!current.gateA.current) throw new Error("design-gate-a-required");
|
|
1018
1050
|
const plan = this.#currentPlanProjection(input.runId);
|
|
1019
1051
|
if (!plan) throw new Error("design-plan-required");
|
|
1020
|
-
|
|
1021
|
-
|
|
1052
|
+
const latestDecisionSequence = this.#facts(input.runId)
|
|
1053
|
+
.filter((fact) => fact.kind === "decision")
|
|
1054
|
+
.at(-1)?.sequence;
|
|
1055
|
+
if (
|
|
1056
|
+
plan.sequence <= (current.gateA.approvedSequence ?? 0) ||
|
|
1057
|
+
(latestDecisionSequence !== undefined &&
|
|
1058
|
+
plan.sequence <= latestDecisionSequence)
|
|
1059
|
+
) {
|
|
1060
|
+
throw new Error("design-plan-stale");
|
|
1022
1061
|
}
|
|
1062
|
+
approvedContractHash = plan.canonicalHash;
|
|
1023
1063
|
}
|
|
1064
|
+
if (!approvedContractHash)
|
|
1065
|
+
throw new Error("design-transient-input-invalid");
|
|
1024
1066
|
const existing = this.#latestApproval(input.runId, input.gate);
|
|
1025
1067
|
const currentGate = this.#gateStatus(input.runId)[
|
|
1026
1068
|
input.gate === "gate-a" ? "gateA" : "gateB"
|
|
@@ -1029,7 +1071,7 @@ export class DesignJournal {
|
|
|
1029
1071
|
if (
|
|
1030
1072
|
existing &&
|
|
1031
1073
|
currentGate.current &&
|
|
1032
|
-
existing.proof.contractHash ===
|
|
1074
|
+
existing.proof.contractHash === approvedContractHash
|
|
1033
1075
|
) {
|
|
1034
1076
|
proof = existing.proof;
|
|
1035
1077
|
} else {
|
|
@@ -1037,7 +1079,7 @@ export class DesignJournal {
|
|
|
1037
1079
|
const payload = {
|
|
1038
1080
|
gate: input.gate,
|
|
1039
1081
|
revision,
|
|
1040
|
-
contractHash:
|
|
1082
|
+
contractHash: approvedContractHash,
|
|
1041
1083
|
};
|
|
1042
1084
|
const fact = this.#appendFact(
|
|
1043
1085
|
input.runId,
|
|
@@ -1047,7 +1089,7 @@ export class DesignJournal {
|
|
|
1047
1089
|
);
|
|
1048
1090
|
proof = {
|
|
1049
1091
|
revision,
|
|
1050
|
-
contractHash:
|
|
1092
|
+
contractHash: approvedContractHash,
|
|
1051
1093
|
recordHash: fact.record_hash,
|
|
1052
1094
|
};
|
|
1053
1095
|
}
|
|
@@ -1506,6 +1548,11 @@ export class DesignJournal {
|
|
|
1506
1548
|
latestTechnical > gateBApproval.sequence
|
|
1507
1549
|
) {
|
|
1508
1550
|
staleReason = "technical-decision-changed";
|
|
1551
|
+
} else if (
|
|
1552
|
+
gateAApproval !== undefined &&
|
|
1553
|
+
gateAApproval.sequence > gateBApproval.sequence
|
|
1554
|
+
) {
|
|
1555
|
+
staleReason = "gate-a-reapproved";
|
|
1509
1556
|
} else if (plan && plan.sequence > gateBApproval.sequence) {
|
|
1510
1557
|
staleReason = "plan-recompiled";
|
|
1511
1558
|
} else if (!plan) {
|