@abelxiaoxing/cadence 1.3.0 → 1.4.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 +115 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +69 -11
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +380 -981
- package/src/isolation-backend.ts +84 -23
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
package/src/delivery-compiler.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
3
|
+
import {
|
|
4
|
+
assertPlanWithinChangeContract,
|
|
5
|
+
type ChangeContract,
|
|
6
|
+
normalizeChangeContract,
|
|
7
|
+
} from "./change-contract.ts";
|
|
2
8
|
import {
|
|
3
9
|
type AgentsImpact,
|
|
4
10
|
type ImplementGraphBoundary,
|
|
@@ -17,7 +23,10 @@ import {
|
|
|
17
23
|
hashCanonicalValue,
|
|
18
24
|
type ImplementGraphReadiness,
|
|
19
25
|
} from "./implement-graph.ts";
|
|
20
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
bindDraftVerificationInputs,
|
|
28
|
+
validateVerificationAdapterCapability,
|
|
29
|
+
} from "./verification-capability.ts";
|
|
21
30
|
|
|
22
31
|
export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
|
|
23
32
|
export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
|
|
@@ -117,7 +126,7 @@ export class DesignPlanValidationError extends Error {
|
|
|
117
126
|
diagnostics
|
|
118
127
|
.map((diagnostic) => structuredClone(diagnostic))
|
|
119
128
|
.sort((left, right) =>
|
|
120
|
-
canonicalJson(left)
|
|
129
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
121
130
|
)
|
|
122
131
|
.map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
|
|
123
132
|
).values(),
|
|
@@ -218,7 +227,9 @@ export function assessDeliveryTraceability(input: {
|
|
|
218
227
|
}
|
|
219
228
|
const verificationIds = input.plan.tasks
|
|
220
229
|
.flatMap((task) => [
|
|
221
|
-
task.
|
|
230
|
+
...(task.verificationMode && task.verificationMode !== "behavior"
|
|
231
|
+
? []
|
|
232
|
+
: [task.phases.red.verification.id]),
|
|
222
233
|
task.phases.green.verification.id,
|
|
223
234
|
...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
|
|
224
235
|
])
|
|
@@ -304,6 +315,7 @@ export interface PlanTracking {
|
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
export interface PlanDraft {
|
|
318
|
+
changeContract?: ChangeContract;
|
|
307
319
|
changeId: string;
|
|
308
320
|
tasks: PlanTaskDraft[];
|
|
309
321
|
outputs: ImplementGraphOutput[];
|
|
@@ -371,7 +383,9 @@ function normalizeArtifactBindings(
|
|
|
371
383
|
paths.add(binding.path);
|
|
372
384
|
return { path: binding.path, rawSha256: binding.rawSha256 };
|
|
373
385
|
});
|
|
374
|
-
return normalized.sort((left, right) =>
|
|
386
|
+
return normalized.sort((left, right) =>
|
|
387
|
+
compareCanonicalStrings(left.path, right.path),
|
|
388
|
+
);
|
|
375
389
|
}
|
|
376
390
|
|
|
377
391
|
function requireReceiptIdentity(
|
|
@@ -405,7 +419,41 @@ function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
|
|
|
405
419
|
return structuredClone(value);
|
|
406
420
|
}
|
|
407
421
|
|
|
408
|
-
|
|
422
|
+
export interface DeliveryReadOptions {
|
|
423
|
+
/** Only the proof-bound delivery loader may admit historical collection ordering. */
|
|
424
|
+
allowLegacyOrder?: boolean;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const legacyPlans = new WeakMap<object, string>();
|
|
428
|
+
|
|
429
|
+
function unorderedIdentity(value: unknown): string {
|
|
430
|
+
if (Array.isArray(value))
|
|
431
|
+
return `[${value.map(unorderedIdentity).sort().join(",")}]`;
|
|
432
|
+
if (isRecord(value))
|
|
433
|
+
return `{${Object.entries(value)
|
|
434
|
+
.sort(([a], [b]) => compareCanonicalStrings(a, b))
|
|
435
|
+
.map(
|
|
436
|
+
([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
|
|
437
|
+
)
|
|
438
|
+
.join(",")}}`;
|
|
439
|
+
return JSON.stringify(value) ?? "null";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function sameNormalized(
|
|
443
|
+
left: unknown,
|
|
444
|
+
right: unknown,
|
|
445
|
+
options: DeliveryReadOptions,
|
|
446
|
+
): boolean {
|
|
447
|
+
return options.allowLegacyOrder
|
|
448
|
+
? unorderedIdentity(left) === unorderedIdentity(right)
|
|
449
|
+
: canonicalJson(left) === canonicalJson(right);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseCanonicalReceipt<T>(
|
|
453
|
+
bytes: Uint8Array,
|
|
454
|
+
code: string,
|
|
455
|
+
options: DeliveryReadOptions = {},
|
|
456
|
+
): T {
|
|
409
457
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
410
458
|
let parsed: unknown;
|
|
411
459
|
try {
|
|
@@ -413,7 +461,11 @@ function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
|
|
|
413
461
|
} catch {
|
|
414
462
|
throw new Error(code);
|
|
415
463
|
}
|
|
416
|
-
if (
|
|
464
|
+
if (
|
|
465
|
+
`${canonicalJson(parsed)}\n` !== text &&
|
|
466
|
+
(!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
|
|
467
|
+
)
|
|
468
|
+
throw new Error(code);
|
|
417
469
|
return parsed as T;
|
|
418
470
|
}
|
|
419
471
|
|
|
@@ -438,10 +490,14 @@ export function compileGateAReceipt(input: {
|
|
|
438
490
|
};
|
|
439
491
|
}
|
|
440
492
|
|
|
441
|
-
export function parseGateAReceipt(
|
|
493
|
+
export function parseGateAReceipt(
|
|
494
|
+
bytes: Uint8Array,
|
|
495
|
+
options: DeliveryReadOptions = {},
|
|
496
|
+
): GateAReceipt {
|
|
442
497
|
const receipt = parseCanonicalReceipt<GateAReceipt>(
|
|
443
498
|
bytes,
|
|
444
499
|
"gate-a-receipt-invalid",
|
|
500
|
+
options,
|
|
445
501
|
);
|
|
446
502
|
if (
|
|
447
503
|
!isRecord(receipt) ||
|
|
@@ -455,8 +511,8 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
|
|
|
455
511
|
const approval = normalizeGateProof(receipt.approval);
|
|
456
512
|
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
457
513
|
if (
|
|
458
|
-
|
|
459
|
-
|
|
514
|
+
!sameNormalized(approval, receipt.approval, options) ||
|
|
515
|
+
!sameNormalized(artifacts, receipt.artifacts, options)
|
|
460
516
|
) {
|
|
461
517
|
throw new Error("gate-a-receipt-invalid");
|
|
462
518
|
}
|
|
@@ -537,10 +593,14 @@ export function compileReadyReceipt(input: {
|
|
|
537
593
|
};
|
|
538
594
|
}
|
|
539
595
|
|
|
540
|
-
export function parseReadyReceipt(
|
|
596
|
+
export function parseReadyReceipt(
|
|
597
|
+
bytes: Uint8Array,
|
|
598
|
+
options: DeliveryReadOptions = {},
|
|
599
|
+
): ReadyReceipt {
|
|
541
600
|
const receipt = parseCanonicalReceipt<ReadyReceipt>(
|
|
542
601
|
bytes,
|
|
543
602
|
"delivery-receipt-invalid",
|
|
603
|
+
options,
|
|
544
604
|
);
|
|
545
605
|
if (
|
|
546
606
|
!isRecord(receipt) ||
|
|
@@ -592,9 +652,9 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
|
|
|
592
652
|
const traceability = normalizeTraceability(receipt.traceability);
|
|
593
653
|
if (
|
|
594
654
|
gateB.contractHash !== receipt.plan.canonicalHash ||
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
655
|
+
!sameNormalized(gateB, receipt.approvals.gateB, options) ||
|
|
656
|
+
!sameNormalized(artifacts, receipt.artifacts, options) ||
|
|
657
|
+
!sameNormalized(traceability, receipt.traceability, options)
|
|
598
658
|
) {
|
|
599
659
|
throw new Error("delivery-receipt-invalid");
|
|
600
660
|
}
|
|
@@ -602,12 +662,14 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
|
|
|
602
662
|
}
|
|
603
663
|
|
|
604
664
|
function sortStrings(values: readonly string[]): string[] {
|
|
605
|
-
return [...values].sort((left, right) =>
|
|
665
|
+
return [...values].sort((left, right) =>
|
|
666
|
+
compareCanonicalStrings(left, right),
|
|
667
|
+
);
|
|
606
668
|
}
|
|
607
669
|
|
|
608
670
|
function sortCanonical<T>(values: readonly T[]): T[] {
|
|
609
671
|
return [...values].sort((left, right) =>
|
|
610
|
-
canonicalJson(left)
|
|
672
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
611
673
|
);
|
|
612
674
|
}
|
|
613
675
|
|
|
@@ -628,7 +690,7 @@ function normalizeVerification(
|
|
|
628
690
|
return cloned;
|
|
629
691
|
}
|
|
630
692
|
|
|
631
|
-
function normalizePhase(value: unknown): PlanPhaseDraft {
|
|
693
|
+
function normalizePhase(value: unknown, readOnly = false): PlanPhaseDraft {
|
|
632
694
|
if (
|
|
633
695
|
!isRecord(value) ||
|
|
634
696
|
!exactKeys(value, [
|
|
@@ -651,7 +713,7 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
|
|
|
651
713
|
}
|
|
652
714
|
const write = sortStrings(value.write as string[]);
|
|
653
715
|
const deletions = sortStrings(value.delete as string[]);
|
|
654
|
-
if (write.length + deletions.length === 0) {
|
|
716
|
+
if (!readOnly && write.length + deletions.length === 0) {
|
|
655
717
|
throw new Error("delivery-plan-empty-phase-boundary");
|
|
656
718
|
}
|
|
657
719
|
if (write.some((candidate) => deletions.includes(candidate))) {
|
|
@@ -724,7 +786,12 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
724
786
|
candidate: unknown,
|
|
725
787
|
) => {
|
|
726
788
|
try {
|
|
727
|
-
return normalizePhase(
|
|
789
|
+
return normalizePhase(
|
|
790
|
+
candidate,
|
|
791
|
+
phase === "red" &&
|
|
792
|
+
value.verificationMode !== undefined &&
|
|
793
|
+
value.verificationMode !== "behavior",
|
|
794
|
+
);
|
|
728
795
|
} catch (error) {
|
|
729
796
|
throw asPlanValidationError(error, {
|
|
730
797
|
...context,
|
|
@@ -758,8 +825,8 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
758
825
|
searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
|
|
759
826
|
relatedTests: [...cloned.impactClosure.relatedTests].sort(
|
|
760
827
|
(left, right) =>
|
|
761
|
-
left.path
|
|
762
|
-
left.disposition
|
|
828
|
+
compareCanonicalStrings(left.path, right.path) ||
|
|
829
|
+
compareCanonicalStrings(left.disposition, right.disposition),
|
|
763
830
|
),
|
|
764
831
|
affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
|
|
765
832
|
},
|
|
@@ -936,7 +1003,7 @@ function normalizeAgentsCheckpointOperations(
|
|
|
936
1003
|
: normalizeAgentsManagedBlock(operation.managedBlock),
|
|
937
1004
|
};
|
|
938
1005
|
})
|
|
939
|
-
.sort((left, right) => left.target
|
|
1006
|
+
.sort((left, right) => compareCanonicalStrings(left.target, right.target));
|
|
940
1007
|
}
|
|
941
1008
|
|
|
942
1009
|
function normalizeVerificationPlan(value: unknown): PlanVerification {
|
|
@@ -1059,6 +1126,7 @@ function normalizeTracking(value: unknown): PlanTracking {
|
|
|
1059
1126
|
}
|
|
1060
1127
|
|
|
1061
1128
|
function normalizeDraft(value: unknown): ImplementPlan {
|
|
1129
|
+
value = structuredClone(value);
|
|
1062
1130
|
if (
|
|
1063
1131
|
!isRecord(value) ||
|
|
1064
1132
|
!exactKeys(value, [
|
|
@@ -1067,6 +1135,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1067
1135
|
"outputs",
|
|
1068
1136
|
"verification",
|
|
1069
1137
|
"tracking",
|
|
1138
|
+
...(value.changeContract === undefined ? [] : ["changeContract"]),
|
|
1070
1139
|
]) ||
|
|
1071
1140
|
typeof value.changeId !== "string" ||
|
|
1072
1141
|
!CHANGE_NAME.test(value.changeId) ||
|
|
@@ -1075,9 +1144,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1075
1144
|
) {
|
|
1076
1145
|
throw new Error("delivery-plan-invalid");
|
|
1077
1146
|
}
|
|
1147
|
+
const source = value;
|
|
1148
|
+
const taskDrafts = value.tasks.map((task: unknown) => {
|
|
1149
|
+
if (
|
|
1150
|
+
!isRecord(task) ||
|
|
1151
|
+
!["mechanical", "refactor"].includes(String(task.verificationMode))
|
|
1152
|
+
)
|
|
1153
|
+
return task;
|
|
1154
|
+
if (!isRecord(task.phases) || !isRecord(task.phases.green))
|
|
1155
|
+
throw new Error("delivery-plan-invalid-task-phases");
|
|
1156
|
+
if (!source.changeContract)
|
|
1157
|
+
throw new Error("change-contract-verification-mode-required");
|
|
1158
|
+
const green = task.phases.green;
|
|
1159
|
+
return {
|
|
1160
|
+
...task,
|
|
1161
|
+
phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
|
|
1162
|
+
};
|
|
1163
|
+
});
|
|
1078
1164
|
const draftGraph = validateImplementGraphBoundary({
|
|
1079
1165
|
changeId: value.changeId,
|
|
1080
|
-
tasks:
|
|
1166
|
+
tasks: taskDrafts.map((candidate) => {
|
|
1081
1167
|
if (!isRecord(candidate)) return candidate;
|
|
1082
1168
|
const {
|
|
1083
1169
|
affectedVerification: _affectedVerification,
|
|
@@ -1095,12 +1181,17 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1095
1181
|
);
|
|
1096
1182
|
}
|
|
1097
1183
|
const plan: ImplementPlan = {
|
|
1184
|
+
...(value.changeContract === undefined
|
|
1185
|
+
? {}
|
|
1186
|
+
: { changeContract: normalizeChangeContract(value.changeContract) }),
|
|
1098
1187
|
changeId: value.changeId,
|
|
1099
|
-
tasks:
|
|
1188
|
+
tasks: taskDrafts
|
|
1100
1189
|
.map(normalizeTask)
|
|
1101
|
-
.sort((left, right) =>
|
|
1190
|
+
.sort((left, right) =>
|
|
1191
|
+
compareCanonicalStrings(left.taskId, right.taskId),
|
|
1192
|
+
),
|
|
1102
1193
|
outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
|
|
1103
|
-
(left, right) => left.id
|
|
1194
|
+
(left, right) => compareCanonicalStrings(left.id, right.id),
|
|
1104
1195
|
),
|
|
1105
1196
|
verification: normalizeVerificationPlan(value.verification),
|
|
1106
1197
|
tracking: normalizeTracking(value.tracking),
|
|
@@ -1141,6 +1232,11 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1141
1232
|
) {
|
|
1142
1233
|
throw new Error("delivery-agents-checkpoint-mismatch");
|
|
1143
1234
|
}
|
|
1235
|
+
if (plan.changeContract)
|
|
1236
|
+
assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
|
|
1237
|
+
plan.verification.change.fullSuite,
|
|
1238
|
+
plan.verification.change.postApply,
|
|
1239
|
+
]);
|
|
1144
1240
|
const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
|
|
1145
1241
|
if (!graphValidation.ok) {
|
|
1146
1242
|
throw new DesignPlanValidationError(
|
|
@@ -1158,7 +1254,12 @@ function renderTasks(plan: ImplementPlan): string {
|
|
|
1158
1254
|
lines.push(
|
|
1159
1255
|
` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
|
|
1160
1256
|
);
|
|
1161
|
-
|
|
1257
|
+
if (!task.verificationMode || task.verificationMode === "behavior")
|
|
1258
|
+
lines.push(` - Red verification: ${task.phases.red.verification.id}`);
|
|
1259
|
+
else
|
|
1260
|
+
lines.push(
|
|
1261
|
+
` - Evidence mode: ${task.verificationMode}; baseline and postconditions required`,
|
|
1262
|
+
);
|
|
1162
1263
|
lines.push(` - Green verification: ${task.phases.green.verification.id}`);
|
|
1163
1264
|
}
|
|
1164
1265
|
lines.push("");
|
|
@@ -1167,10 +1268,16 @@ function renderTasks(plan: ImplementPlan): string {
|
|
|
1167
1268
|
|
|
1168
1269
|
export function compileImplementPlan(
|
|
1169
1270
|
draft: unknown,
|
|
1170
|
-
options: { consumerRoot: string },
|
|
1271
|
+
options: { consumerRoot: string; bindExecutionInputs?: boolean },
|
|
1171
1272
|
): CompiledDelivery {
|
|
1172
1273
|
try {
|
|
1173
|
-
|
|
1274
|
+
if (options.bindExecutionInputs)
|
|
1275
|
+
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1276
|
+
const normalized = normalizeDraft(draft);
|
|
1277
|
+
const plan =
|
|
1278
|
+
isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
|
|
1279
|
+
? (draft as unknown as ImplementPlan)
|
|
1280
|
+
: normalized;
|
|
1174
1281
|
const graph = graphFromPlan(plan);
|
|
1175
1282
|
const readiness = assessImplementGraphReadiness(
|
|
1176
1283
|
options.consumerRoot,
|
|
@@ -1217,7 +1324,10 @@ export function compileImplementPlan(
|
|
|
1217
1324
|
}
|
|
1218
1325
|
}
|
|
1219
1326
|
|
|
1220
|
-
export function parseImplementPlan(
|
|
1327
|
+
export function parseImplementPlan(
|
|
1328
|
+
bytes: Uint8Array,
|
|
1329
|
+
options: DeliveryReadOptions = {},
|
|
1330
|
+
): ImplementPlan {
|
|
1221
1331
|
const text = new TextDecoder().decode(bytes);
|
|
1222
1332
|
let parsed: unknown;
|
|
1223
1333
|
try {
|
|
@@ -1226,8 +1336,15 @@ export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
|
|
|
1226
1336
|
throw new Error("delivery-plan-json-invalid");
|
|
1227
1337
|
}
|
|
1228
1338
|
const normalized = normalizeDraft(parsed);
|
|
1229
|
-
if (`${canonicalJson(normalized)}\n`
|
|
1339
|
+
if (`${canonicalJson(normalized)}\n` === text) return normalized;
|
|
1340
|
+
if (
|
|
1341
|
+
!options.allowLegacyOrder ||
|
|
1342
|
+
`${canonicalJson(parsed)}\n` !== text ||
|
|
1343
|
+
!sameNormalized(normalized, parsed, options)
|
|
1344
|
+
) {
|
|
1230
1345
|
throw new Error("delivery-plan-not-canonical");
|
|
1231
1346
|
}
|
|
1232
|
-
|
|
1347
|
+
// Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
|
|
1348
|
+
legacyPlans.set(parsed as object, JSON.stringify(parsed));
|
|
1349
|
+
return parsed as ImplementPlan;
|
|
1233
1350
|
}
|
package/src/design-control.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
lstatSync,
|
|
@@ -8,6 +9,10 @@ import {
|
|
|
8
9
|
writeFileSync,
|
|
9
10
|
} from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
type ChangeContract,
|
|
14
|
+
normalizeChangeContract,
|
|
15
|
+
} from "./change-contract.ts";
|
|
11
16
|
import { isValidRelativePath } from "./contracts.ts";
|
|
12
17
|
import {
|
|
13
18
|
assessDeliveryTraceability,
|
|
@@ -28,6 +33,7 @@ import {
|
|
|
28
33
|
type DesignStatusProjection,
|
|
29
34
|
} from "./design-journal.ts";
|
|
30
35
|
import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
|
|
36
|
+
import { canonicalJson } from "./run-state.ts";
|
|
31
37
|
import { RunStore } from "./run-store.ts";
|
|
32
38
|
import { observeSafePath } from "./safe-path.ts";
|
|
33
39
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
@@ -39,6 +45,20 @@ const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
|
|
|
39
45
|
const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
|
|
40
46
|
const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
|
|
41
47
|
|
|
48
|
+
function validGateContract(value: unknown): boolean {
|
|
49
|
+
if (typeof value === "string")
|
|
50
|
+
return (
|
|
51
|
+
value.trim().length > 0 &&
|
|
52
|
+
Buffer.byteLength(value, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
|
|
53
|
+
);
|
|
54
|
+
try {
|
|
55
|
+
normalizeChangeContract(value);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export interface DesignOpenSpecInspection {
|
|
43
63
|
change: string;
|
|
44
64
|
schema: string;
|
|
@@ -82,7 +102,7 @@ export type DesignControlRequest =
|
|
|
82
102
|
runId: string;
|
|
83
103
|
operationId: string;
|
|
84
104
|
gate: "gate-a";
|
|
85
|
-
contract: string;
|
|
105
|
+
contract: string | ChangeContract;
|
|
86
106
|
}
|
|
87
107
|
| {
|
|
88
108
|
operation: "approve-gate";
|
|
@@ -275,9 +295,7 @@ export function validateDesignControlRequest(
|
|
|
275
295
|
"gate",
|
|
276
296
|
"contract",
|
|
277
297
|
]) &&
|
|
278
|
-
|
|
279
|
-
value.contract.trim().length > 0 &&
|
|
280
|
-
Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
|
|
298
|
+
validGateContract(value.contract)
|
|
281
299
|
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
282
300
|
: { ok: false, code: "invalid-design-control-request" };
|
|
283
301
|
}
|
|
@@ -459,6 +477,7 @@ export class DesignController {
|
|
|
459
477
|
readonly #runs: RunStore;
|
|
460
478
|
readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
|
|
461
479
|
#closed = false;
|
|
480
|
+
readonly #authority = new AsyncLocalStorage<() => void>();
|
|
462
481
|
|
|
463
482
|
private constructor(options: DesignControllerOptions) {
|
|
464
483
|
this.consumerRoot = path.resolve(options.consumerRoot);
|
|
@@ -485,7 +504,22 @@ export class DesignController {
|
|
|
485
504
|
return new DesignController(options);
|
|
486
505
|
}
|
|
487
506
|
|
|
507
|
+
executeWithAuthority(
|
|
508
|
+
value: unknown,
|
|
509
|
+
assertAuthority: () => void,
|
|
510
|
+
): Promise<Record<string, unknown>> {
|
|
511
|
+
return this.#authority.run(assertAuthority, () => this.execute(value));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#assertFinalizationLease(
|
|
515
|
+
lease: Parameters<DesignJournal["assertFinalizationLease"]>[0],
|
|
516
|
+
): void {
|
|
517
|
+
this.#authority.getStore()?.();
|
|
518
|
+
this.#journal.assertFinalizationLease(lease);
|
|
519
|
+
}
|
|
520
|
+
|
|
488
521
|
#assertOpen(): void {
|
|
522
|
+
this.#authority.getStore()?.();
|
|
489
523
|
if (this.#closed) throw new Error("design-controller-closed");
|
|
490
524
|
}
|
|
491
525
|
|
|
@@ -631,6 +665,7 @@ export class DesignController {
|
|
|
631
665
|
throw error;
|
|
632
666
|
}
|
|
633
667
|
if (run.state === "created") {
|
|
668
|
+
this.#journal.inheritFinalizedAuthority(run.runId);
|
|
634
669
|
this.#runs.transition({
|
|
635
670
|
runId: run.runId,
|
|
636
671
|
to: "paused",
|
|
@@ -831,7 +866,21 @@ export class DesignController {
|
|
|
831
866
|
{ code: "design-plan-draft-invalid", field: "plan-draft.json" },
|
|
832
867
|
]);
|
|
833
868
|
}
|
|
869
|
+
if (status.changeContract) {
|
|
870
|
+
if (!draft || typeof draft !== "object" || Array.isArray(draft))
|
|
871
|
+
throw new Error("change-contract-invalid");
|
|
872
|
+
const record = draft as Record<string, unknown>;
|
|
873
|
+
if (
|
|
874
|
+
record.changeContract !== undefined &&
|
|
875
|
+
canonicalJson(normalizeChangeContract(record.changeContract)) !==
|
|
876
|
+
canonicalJson(status.changeContract)
|
|
877
|
+
)
|
|
878
|
+
throw new Error("change-contract-mismatch");
|
|
879
|
+
record.changeContract = status.changeContract;
|
|
880
|
+
} else if (draft && typeof draft === "object" && "changeContract" in draft)
|
|
881
|
+
throw new Error("change-contract-gate-a-required");
|
|
834
882
|
const compiled = compileImplementPlan(draft, {
|
|
883
|
+
bindExecutionInputs: true,
|
|
835
884
|
consumerRoot: this.consumerRoot,
|
|
836
885
|
});
|
|
837
886
|
if (compiled.plan.changeId !== status.change) {
|
|
@@ -882,9 +931,15 @@ export class DesignController {
|
|
|
882
931
|
if (!change) throw new Error("design-change-required");
|
|
883
932
|
const readyPath = safeRelative(change, "ready.yaml");
|
|
884
933
|
const gateAPath = safeRelative(change, "gate-a.yaml");
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
934
|
+
const unchanged =
|
|
935
|
+
status.gates.gateB.current &&
|
|
936
|
+
status.plan?.rawSha256 === compiled.rawSha256 &&
|
|
937
|
+
status.plan.canonicalHash === compiled.planHash;
|
|
938
|
+
if (!unchanged) {
|
|
939
|
+
removeSafeFile(this.consumerRoot, readyPath);
|
|
940
|
+
removeSafeFile(this.consumerRoot, gateAPath);
|
|
941
|
+
}
|
|
942
|
+
this.#assertFinalizationLease(lease);
|
|
888
943
|
atomicWrite(
|
|
889
944
|
this.consumerRoot,
|
|
890
945
|
safeRelative(change, "implement-plan.json"),
|
|
@@ -897,6 +952,7 @@ export class DesignController {
|
|
|
897
952
|
rawSha256: compiled.rawSha256,
|
|
898
953
|
canonicalHash: compiled.planHash,
|
|
899
954
|
lease,
|
|
955
|
+
certify: true,
|
|
900
956
|
});
|
|
901
957
|
} finally {
|
|
902
958
|
this.#journal.releaseFinalizationLease(lease);
|
|
@@ -970,7 +1026,9 @@ export class DesignController {
|
|
|
970
1026
|
}
|
|
971
1027
|
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
972
1028
|
try {
|
|
973
|
-
const plan = parseImplementPlan(installedPlanBytes
|
|
1029
|
+
const plan = parseImplementPlan(installedPlanBytes, {
|
|
1030
|
+
allowLegacyOrder: true,
|
|
1031
|
+
});
|
|
974
1032
|
compiled = compileImplementPlan(plan, {
|
|
975
1033
|
consumerRoot: this.consumerRoot,
|
|
976
1034
|
});
|
|
@@ -1067,14 +1125,14 @@ export class DesignController {
|
|
|
1067
1125
|
traceability: traceability.value,
|
|
1068
1126
|
});
|
|
1069
1127
|
readyHash = readyReceipt.rawSha256;
|
|
1070
|
-
this.#
|
|
1128
|
+
this.#assertFinalizationLease(lease);
|
|
1071
1129
|
atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
|
|
1072
1130
|
atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
|
|
1073
1131
|
readyInstalled = true;
|
|
1074
1132
|
const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
|
|
1075
1133
|
const installedReady = readSafeFile(this.consumerRoot, readyPath);
|
|
1076
|
-
parseGateAReceipt(installedGateA);
|
|
1077
|
-
parseReadyReceipt(installedReady);
|
|
1134
|
+
parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
|
|
1135
|
+
parseReadyReceipt(installedReady, { allowLegacyOrder: true });
|
|
1078
1136
|
if (
|
|
1079
1137
|
!Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
|
|
1080
1138
|
!Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))
|