@abelxiaoxing/cadence 1.3.0 → 1.4.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 +172 -13
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -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 +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- 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 +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -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/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
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,9 +9,15 @@ 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";
|
|
16
|
+
import { changeContractDiagnostics } from "./change-contract-schema.ts";
|
|
11
17
|
import { isValidRelativePath } from "./contracts.ts";
|
|
12
18
|
import {
|
|
13
19
|
assessDeliveryTraceability,
|
|
20
|
+
bindTaskVerifications,
|
|
14
21
|
compileGateAReceipt,
|
|
15
22
|
compileImplementPlan,
|
|
16
23
|
compileReadyReceipt,
|
|
@@ -21,6 +28,7 @@ import {
|
|
|
21
28
|
parseImplementPlan,
|
|
22
29
|
parseReadyReceipt,
|
|
23
30
|
} from "./delivery-compiler.ts";
|
|
31
|
+
import type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
24
32
|
import {
|
|
25
33
|
type DesignArtifactOperationFacts,
|
|
26
34
|
type DesignArtifactOperationOutcome,
|
|
@@ -28,6 +36,8 @@ import {
|
|
|
28
36
|
type DesignStatusProjection,
|
|
29
37
|
} from "./design-journal.ts";
|
|
30
38
|
import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
|
|
39
|
+
import { planAuthoringSummary } from "./plan-draft-summary.ts";
|
|
40
|
+
import { canonicalJson } from "./run-state.ts";
|
|
31
41
|
import { RunStore } from "./run-store.ts";
|
|
32
42
|
import { observeSafePath } from "./safe-path.ts";
|
|
33
43
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
@@ -39,6 +49,20 @@ const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
|
|
|
39
49
|
const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
|
|
40
50
|
const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
|
|
41
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
|
+
|
|
42
66
|
export interface DesignOpenSpecInspection {
|
|
43
67
|
change: string;
|
|
44
68
|
schema: string;
|
|
@@ -82,7 +106,7 @@ export type DesignControlRequest =
|
|
|
82
106
|
runId: string;
|
|
83
107
|
operationId: string;
|
|
84
108
|
gate: "gate-a";
|
|
85
|
-
contract: string;
|
|
109
|
+
contract: string | ChangeContract;
|
|
86
110
|
}
|
|
87
111
|
| {
|
|
88
112
|
operation: "approve-gate";
|
|
@@ -179,7 +203,53 @@ function lifecycleOperationId(kind: string, runId: string): string {
|
|
|
179
203
|
.slice(0, 32)}`;
|
|
180
204
|
}
|
|
181
205
|
|
|
182
|
-
export
|
|
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(
|
|
183
253
|
value: unknown,
|
|
184
254
|
):
|
|
185
255
|
| { ok: true; value: DesignControlRequest }
|
|
@@ -275,9 +345,7 @@ export function validateDesignControlRequest(
|
|
|
275
345
|
"gate",
|
|
276
346
|
"contract",
|
|
277
347
|
]) &&
|
|
278
|
-
|
|
279
|
-
value.contract.trim().length > 0 &&
|
|
280
|
-
Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
|
|
348
|
+
validGateContract(value.contract)
|
|
281
349
|
? { ok: true, value: structuredClone(value) as DesignControlRequest }
|
|
282
350
|
: { ok: false, code: "invalid-design-control-request" };
|
|
283
351
|
}
|
|
@@ -459,6 +527,7 @@ export class DesignController {
|
|
|
459
527
|
readonly #runs: RunStore;
|
|
460
528
|
readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
|
|
461
529
|
#closed = false;
|
|
530
|
+
readonly #authority = new AsyncLocalStorage<() => void>();
|
|
462
531
|
|
|
463
532
|
private constructor(options: DesignControllerOptions) {
|
|
464
533
|
this.consumerRoot = path.resolve(options.consumerRoot);
|
|
@@ -485,7 +554,22 @@ export class DesignController {
|
|
|
485
554
|
return new DesignController(options);
|
|
486
555
|
}
|
|
487
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
|
+
|
|
488
571
|
#assertOpen(): void {
|
|
572
|
+
this.#authority.getStore()?.();
|
|
489
573
|
if (this.#closed) throw new Error("design-controller-closed");
|
|
490
574
|
}
|
|
491
575
|
|
|
@@ -519,7 +603,8 @@ export class DesignController {
|
|
|
519
603
|
async execute(value: unknown): Promise<Record<string, unknown>> {
|
|
520
604
|
this.#assertOpen();
|
|
521
605
|
const validation = validateDesignControlRequest(value);
|
|
522
|
-
if (!validation.ok)
|
|
606
|
+
if (!validation.ok)
|
|
607
|
+
throw new DesignControlValidationError(validation.diagnostics);
|
|
523
608
|
const request = validation.value;
|
|
524
609
|
switch (request.operation) {
|
|
525
610
|
case "start":
|
|
@@ -597,6 +682,30 @@ export class DesignController {
|
|
|
597
682
|
completed: run.state === "completed",
|
|
598
683
|
...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
|
|
599
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 only remaining substantive choices; use existing same-task approval. Submit the accepted structured ChangeContract, not a prose placeholder.",
|
|
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 then compile; compilation supplies 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
|
+
},
|
|
600
709
|
packetActions: terminal ? [] : ["finish"],
|
|
601
710
|
design,
|
|
602
711
|
};
|
|
@@ -631,6 +740,7 @@ export class DesignController {
|
|
|
631
740
|
throw error;
|
|
632
741
|
}
|
|
633
742
|
if (run.state === "created") {
|
|
743
|
+
this.#journal.inheritFinalizedAuthority(run.runId);
|
|
634
744
|
this.#runs.transition({
|
|
635
745
|
runId: run.runId,
|
|
636
746
|
to: "paused",
|
|
@@ -831,7 +941,22 @@ export class DesignController {
|
|
|
831
941
|
{ code: "design-plan-draft-invalid", field: "plan-draft.json" },
|
|
832
942
|
]);
|
|
833
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");
|
|
834
958
|
const compiled = compileImplementPlan(draft, {
|
|
959
|
+
bindExecutionInputs: true,
|
|
835
960
|
consumerRoot: this.consumerRoot,
|
|
836
961
|
});
|
|
837
962
|
if (compiled.plan.changeId !== status.change) {
|
|
@@ -839,7 +964,7 @@ export class DesignController {
|
|
|
839
964
|
{ code: "design-plan-change-mismatch", field: "changeId" },
|
|
840
965
|
]);
|
|
841
966
|
}
|
|
842
|
-
return { status, compiled };
|
|
967
|
+
return { status, compiled, authoredDraft };
|
|
843
968
|
}
|
|
844
969
|
|
|
845
970
|
#validatePlanDraft(
|
|
@@ -848,11 +973,12 @@ export class DesignController {
|
|
|
848
973
|
{ operation: "validate-plan-draft" }
|
|
849
974
|
>,
|
|
850
975
|
) {
|
|
851
|
-
const { compiled } = this.#readCompiledDraft(request.runId);
|
|
976
|
+
const { compiled, authoredDraft } = this.#readCompiledDraft(request.runId);
|
|
852
977
|
return {
|
|
853
978
|
operation: request.operation,
|
|
854
979
|
runId: request.runId,
|
|
855
980
|
valid: true,
|
|
981
|
+
summary: planAuthoringSummary(compiled.plan, authoredDraft),
|
|
856
982
|
plan: {
|
|
857
983
|
taskCount: compiled.plan.tasks.length,
|
|
858
984
|
outputCount: compiled.plan.outputs.length,
|
|
@@ -882,9 +1008,47 @@ export class DesignController {
|
|
|
882
1008
|
if (!change) throw new Error("design-change-required");
|
|
883
1009
|
const readyPath = safeRelative(change, "ready.yaml");
|
|
884
1010
|
const gateAPath = safeRelative(change, "gate-a.yaml");
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1011
|
+
const tasksPath = safeRelative(change, "tasks.md");
|
|
1012
|
+
if (observeSafePath(this.consumerRoot, tasksPath).kind === "absent") {
|
|
1013
|
+
throw new DesignPlanValidationError(
|
|
1014
|
+
"design-traceability-input-unavailable",
|
|
1015
|
+
[{ code: "design-traceability-input-unavailable", path: "tasks.md" }],
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const priorTasks = boundedArtifactBytes(this.consumerRoot, tasksPath);
|
|
1019
|
+
const tasksBytes = Buffer.from(
|
|
1020
|
+
bindTaskVerifications(
|
|
1021
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1022
|
+
priorTasks,
|
|
1023
|
+
),
|
|
1024
|
+
compiled.plan,
|
|
1025
|
+
),
|
|
1026
|
+
"utf8",
|
|
1027
|
+
);
|
|
1028
|
+
if (tasksBytes.length > MAX_DESIGN_FILE_BYTES) {
|
|
1029
|
+
throw new Error("design-artifact-content-too-large");
|
|
1030
|
+
}
|
|
1031
|
+
const tasksUnchanged = tasksBytes.equals(priorTasks);
|
|
1032
|
+
const unchanged =
|
|
1033
|
+
tasksUnchanged &&
|
|
1034
|
+
status.gates.gateB.current &&
|
|
1035
|
+
status.plan?.rawSha256 === compiled.rawSha256 &&
|
|
1036
|
+
status.plan.canonicalHash === compiled.planHash;
|
|
1037
|
+
if (!unchanged) {
|
|
1038
|
+
removeSafeFile(this.consumerRoot, readyPath);
|
|
1039
|
+
removeSafeFile(this.consumerRoot, gateAPath);
|
|
1040
|
+
}
|
|
1041
|
+
this.#assertFinalizationLease(lease);
|
|
1042
|
+
if (
|
|
1043
|
+
!boundedArtifactBytes(this.consumerRoot, tasksPath).equals(priorTasks)
|
|
1044
|
+
) {
|
|
1045
|
+
throw new Error("design-artifact-currentness-mismatch");
|
|
1046
|
+
}
|
|
1047
|
+
// The compilation journal commits only after both files are installed.
|
|
1048
|
+
// A crash before that point can replay this deterministic projection;
|
|
1049
|
+
// finalization always rechecks the plan and task bytes together.
|
|
1050
|
+
if (!tasksUnchanged)
|
|
1051
|
+
atomicWrite(this.consumerRoot, tasksPath, tasksBytes);
|
|
888
1052
|
atomicWrite(
|
|
889
1053
|
this.consumerRoot,
|
|
890
1054
|
safeRelative(change, "implement-plan.json"),
|
|
@@ -897,6 +1061,7 @@ export class DesignController {
|
|
|
897
1061
|
rawSha256: compiled.rawSha256,
|
|
898
1062
|
canonicalHash: compiled.planHash,
|
|
899
1063
|
lease,
|
|
1064
|
+
certify: true,
|
|
900
1065
|
});
|
|
901
1066
|
} finally {
|
|
902
1067
|
this.#journal.releaseFinalizationLease(lease);
|
|
@@ -970,7 +1135,9 @@ export class DesignController {
|
|
|
970
1135
|
}
|
|
971
1136
|
let compiled: ReturnType<typeof compileImplementPlan> | undefined;
|
|
972
1137
|
try {
|
|
973
|
-
const plan = parseImplementPlan(installedPlanBytes
|
|
1138
|
+
const plan = parseImplementPlan(installedPlanBytes, {
|
|
1139
|
+
allowLegacyOrder: true,
|
|
1140
|
+
});
|
|
974
1141
|
compiled = compileImplementPlan(plan, {
|
|
975
1142
|
consumerRoot: this.consumerRoot,
|
|
976
1143
|
});
|
|
@@ -1067,14 +1234,14 @@ export class DesignController {
|
|
|
1067
1234
|
traceability: traceability.value,
|
|
1068
1235
|
});
|
|
1069
1236
|
readyHash = readyReceipt.rawSha256;
|
|
1070
|
-
this.#
|
|
1237
|
+
this.#assertFinalizationLease(lease);
|
|
1071
1238
|
atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
|
|
1072
1239
|
atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
|
|
1073
1240
|
readyInstalled = true;
|
|
1074
1241
|
const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
|
|
1075
1242
|
const installedReady = readSafeFile(this.consumerRoot, readyPath);
|
|
1076
|
-
parseGateAReceipt(installedGateA);
|
|
1077
|
-
parseReadyReceipt(installedReady);
|
|
1243
|
+
parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
|
|
1244
|
+
parseReadyReceipt(installedReady, { allowLegacyOrder: true });
|
|
1078
1245
|
if (
|
|
1079
1246
|
!Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
|
|
1080
1247
|
!Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { canonicalJson, compareCanonicalStrings } from "./canonical.ts";
|
|
2
|
+
import { isValidRelativePath } from "./contracts.ts";
|
|
3
|
+
|
|
4
|
+
export type SafeDesignDiagnostic = Record<string, string | boolean | string[]>;
|
|
5
|
+
const CODE = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
6
|
+
const IDENTITIES = new Set([
|
|
7
|
+
"code",
|
|
8
|
+
"taskId",
|
|
9
|
+
"phase",
|
|
10
|
+
"field",
|
|
11
|
+
"category",
|
|
12
|
+
"owner",
|
|
13
|
+
"verificationId",
|
|
14
|
+
"outputId",
|
|
15
|
+
"dependencyTaskId",
|
|
16
|
+
"producerTaskId",
|
|
17
|
+
"producerPhase",
|
|
18
|
+
"command",
|
|
19
|
+
"reason",
|
|
20
|
+
"systemCode",
|
|
21
|
+
"exitCode",
|
|
22
|
+
]);
|
|
23
|
+
const HINTS = new Map<string, string>(
|
|
24
|
+
Object.entries({
|
|
25
|
+
"change-contract-acceptance-missing":
|
|
26
|
+
"Read the current approved ChangeContract from Design status and include each accepted verification obligation in the plan's phase, affected, full-suite or post-apply verification. Command, arguments, inputs and classification must match; copying only its ID is insufficient. Keep accepted IDs, statements and scope unchanged.",
|
|
27
|
+
"script-command-mismatch":
|
|
28
|
+
"command must equal the literal package.json scripts[script] value, not an invocation such as npm run test. In PlanDraft, omit command to bind it from the manifest. Explicit commands, including approved Gate acceptance verifiers, remain exact and must be corrected rather than silently replaced.",
|
|
29
|
+
"design-traceability-input-unavailable":
|
|
30
|
+
"Write tasks.md with author task checkboxes and exact owned Scenario references, plus the referenced spec artifacts, before compilation and finalization.",
|
|
31
|
+
"traceability-managed-region-invalid":
|
|
32
|
+
"Repair the paired ABEL:VERIFICATION-BINDINGS markers in tasks.md without removing author evidence, then compile again. Code replaces only one complete managed region.",
|
|
33
|
+
"traceability-managed-region-stale":
|
|
34
|
+
"Compile the current draft again to regenerate phase verification bindings; do not copy verifier identities by hand.",
|
|
35
|
+
"traceability-task-unmapped":
|
|
36
|
+
"Put each planned task ID in backticks on its author checkbox outside the generated bindings region.",
|
|
37
|
+
"traceability-verification-unmapped":
|
|
38
|
+
"Compile after writing tasks.md so code installs phase verification bindings while preserving the author evidence.",
|
|
39
|
+
"traceability-reference-empty":
|
|
40
|
+
"Add exact backticked specs/<capability>/spec.md#Requirement title/Scenario title references under the owning author tasks. Code does not infer Scenario ownership.",
|
|
41
|
+
"change-contract-field-invalid":
|
|
42
|
+
"Correct the indicated ChangeContract field using the tool schema. Keep accepted scope and evidence unchanged.",
|
|
43
|
+
"design-control-field-invalid":
|
|
44
|
+
"Use the exact operation envelope and returned runId; inspect the indicated field in the tool schema.",
|
|
45
|
+
"verification-reference-unavailable":
|
|
46
|
+
"Use a declared verificationDefinitions name or an explicit verification contract.",
|
|
47
|
+
"verification-reference-invalid":
|
|
48
|
+
"A verification reference contains only use and optional Red expectedFailure. Put executable fields in its definition.",
|
|
49
|
+
"verification-definition-invalid":
|
|
50
|
+
"Define one existing atomic verifier without id, classification, executionBindings or references.",
|
|
51
|
+
"current-task-outside-write-set":
|
|
52
|
+
"Omit disposition to derive the test owner from task writes. Keep path and evidence; do not widen writes to satisfy this label.",
|
|
53
|
+
"related-test-owner-ambiguous":
|
|
54
|
+
"Several other tasks edit this test. Choose the intended regression-task owner explicitly, or resolve overlapping task ownership.",
|
|
55
|
+
"verification-input-binding-mismatch":
|
|
56
|
+
"Omit verificationInputs to derive exact bindings from the verification contract and declared outputs. Keep supporting source files in read, not in verificationInputs.",
|
|
57
|
+
"workspace-input-has-producer":
|
|
58
|
+
"Omit verificationInputs or bind this path to its declared outputId instead of workspace.",
|
|
59
|
+
"multiple-output-producers":
|
|
60
|
+
"Declare one producer per output path before deriving input bindings. Do not choose a producer by list order.",
|
|
61
|
+
"producer-not-dependency":
|
|
62
|
+
"The producing task must be an explicit dependency of this task. Input inference does not add dependency authority.",
|
|
63
|
+
"producer-phase-after-consumer":
|
|
64
|
+
"This verification needs an output from a later phase. Correct the producer or phase order; input inference cannot make that output available earlier.",
|
|
65
|
+
"verification-input-not-declared":
|
|
66
|
+
"The verification contract requires a path outside this phase's read/write declarations. Correct the contract or explicitly declare the approved input path.",
|
|
67
|
+
"workspace-input-unavailable":
|
|
68
|
+
"The required workspace input is missing or not a safe regular file. Correct its path or declare its planned producer.",
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
function safePath(value: unknown): value is string {
|
|
72
|
+
return isValidRelativePath(value) && !/[\p{Cc}\p{Cf}]/u.test(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One bounded projection for tool feedback and TUI; guidance is code-owned. */
|
|
76
|
+
export function projectDesignDiagnostic(
|
|
77
|
+
value: unknown,
|
|
78
|
+
): SafeDesignDiagnostic | undefined {
|
|
79
|
+
if (typeof value === "string") value = { code: value.split(":", 1)[0] };
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
81
|
+
return undefined;
|
|
82
|
+
const record = value as Record<string, unknown>;
|
|
83
|
+
if (typeof record.code !== "string" || !CODE.test(record.code))
|
|
84
|
+
return undefined;
|
|
85
|
+
const result: SafeDesignDiagnostic = {};
|
|
86
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
87
|
+
if (IDENTITIES.has(key) && typeof entry === "string" && CODE.test(entry))
|
|
88
|
+
result[key] = entry;
|
|
89
|
+
else if (
|
|
90
|
+
(key === "retryable" || key === "pathsTruncated") &&
|
|
91
|
+
typeof entry === "boolean"
|
|
92
|
+
)
|
|
93
|
+
result[key] =
|
|
94
|
+
key === "pathsTruncated"
|
|
95
|
+
? result.pathsTruncated === true || entry
|
|
96
|
+
: entry;
|
|
97
|
+
else if (key === "path" && safePath(entry)) result.path = entry;
|
|
98
|
+
else if (
|
|
99
|
+
(key === "expectedPaths" || key === "actualPaths") &&
|
|
100
|
+
Array.isArray(entry)
|
|
101
|
+
) {
|
|
102
|
+
const paths = [...new Set(entry.filter(safePath))].sort(
|
|
103
|
+
compareCanonicalStrings,
|
|
104
|
+
);
|
|
105
|
+
result[key] = paths.slice(0, 32);
|
|
106
|
+
if (entry.some((path) => !safePath(path)) || paths.length > 32)
|
|
107
|
+
result.pathsTruncated = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const hint = HINTS.get(String(result.category)) ?? HINTS.get(record.code);
|
|
111
|
+
if (hint) result.hint = hint;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DesignPlanDiagnostic {
|
|
116
|
+
code: string;
|
|
117
|
+
taskId?: string;
|
|
118
|
+
phase?: string;
|
|
119
|
+
field?: string;
|
|
120
|
+
category?: string;
|
|
121
|
+
owner?: string;
|
|
122
|
+
verificationId?: string;
|
|
123
|
+
outputId?: string;
|
|
124
|
+
dependencyTaskId?: string;
|
|
125
|
+
producerTaskId?: string;
|
|
126
|
+
producerPhase?: string;
|
|
127
|
+
path?: string;
|
|
128
|
+
expectedPaths?: string[];
|
|
129
|
+
actualPaths?: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class DesignPlanValidationError extends Error {
|
|
133
|
+
readonly diagnostics: readonly DesignPlanDiagnostic[];
|
|
134
|
+
|
|
135
|
+
constructor(message: string, diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
136
|
+
const normalized = [
|
|
137
|
+
...new Map(
|
|
138
|
+
diagnostics
|
|
139
|
+
.map((diagnostic) => structuredClone(diagnostic))
|
|
140
|
+
.sort((left, right) =>
|
|
141
|
+
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
142
|
+
)
|
|
143
|
+
.map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
|
|
144
|
+
).values(),
|
|
145
|
+
];
|
|
146
|
+
super(message);
|
|
147
|
+
this.name = "DesignPlanValidationError";
|
|
148
|
+
this.diagnostics = Object.freeze(normalized);
|
|
149
|
+
}
|
|
150
|
+
}
|