@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.
Files changed (76) hide show
  1. package/README.md +172 -13
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. package/src/parent-payload-bridge.ts +0 -523
package/src/contracts.ts CHANGED
@@ -52,6 +52,14 @@ export const LIMITS = {
52
52
  maxCompleteResultBytes: 64 * 1024,
53
53
  } as const;
54
54
 
55
+ /** Shared collection bounds for runtime verification and author-facing schemas. */
56
+ export const VERIFICATION_LIMITS = Object.freeze({
57
+ minSteps: 1,
58
+ maxSteps: 8,
59
+ minTestFiles: 1,
60
+ maxTestFiles: 64,
61
+ });
62
+
55
63
  export const ARTIFACT_FAILURE_CODES = [
56
64
  "agents-impact-mismatch",
57
65
  "agents-target-mismatch",
@@ -117,7 +125,6 @@ export const ENVIRONMENT_FAILURE_CODES = [
117
125
  "checkout-failed",
118
126
  "clone-alternates",
119
127
  "clone-failed",
120
- "child-session-create-failed",
121
128
  "dependency-path-unsafe",
122
129
  "git-apply-check-unavailable",
123
130
  "git-apply-failed",
@@ -125,11 +132,6 @@ export const ENVIRONMENT_FAILURE_CODES = [
125
132
  "git-ignore-check-failed",
126
133
  "git-index-unavailable",
127
134
  "invalid-subagent-endpoint",
128
- "parent-bridge-capture-not-ready",
129
- "parent-bridge-generation-invalidated",
130
- "parent-bridge-model-key-mismatch",
131
- "parent-bridge-provider-not-installed",
132
- "parent-bridge-session-unavailable",
133
135
  "root-unavailable",
134
136
  "sandbox-runtime-unavailable",
135
137
  ] as const;
@@ -150,8 +152,14 @@ export const VERIFICATION_ADAPTER_CODES = [
150
152
  "script-command-mismatch",
151
153
  "script-missing",
152
154
  "script-unsafe",
155
+ "report-arguments-conflict",
156
+ "verification-config-mismatch",
157
+ "verification-config-unsafe",
153
158
  ] as const;
154
159
  export const CHILD_TRANSPORT_CODES = [
160
+ "first-progress-timeout",
161
+ "stream-idle-timeout",
162
+ "attempt-timeout",
155
163
  "child-no-final-assistant",
156
164
  "child-provider-stream-aborted",
157
165
  "child-provider-stream-error",
@@ -234,6 +242,10 @@ export type CandidateFailure =
234
242
  | { kind: "result-limit"; limitBytes: number };
235
243
 
236
244
  export type ChildFailure =
245
+ | {
246
+ kind: "execution-limit";
247
+ code: "child-turn-limit" | "child-context-limit";
248
+ }
237
249
  | CandidateFailure
238
250
  | {
239
251
  kind: "transport";
@@ -249,10 +261,35 @@ export type VerificationClassification =
249
261
  | "expected-green"
250
262
  | "expected-refactor";
251
263
 
264
+ /** Runtime-owned evidence; unavailable checks never become product baselines. */
265
+ export interface VerificationEvidence {
266
+ id: string;
267
+ exitCode: number;
268
+ classification: VerificationClassification;
269
+ tests?: number;
270
+ failureIdentities: string[];
271
+ policy: "report-file-v3";
272
+ }
273
+ export type VerificationObservation =
274
+ | { kind: "accepted"; evidence: VerificationEvidence }
275
+ | {
276
+ kind: "rejected";
277
+ code: "red-not-witnessed" | "verification-rejected";
278
+ evidence: VerificationEvidence;
279
+ }
280
+ | {
281
+ kind: "unavailable";
282
+ category: "environment" | "adapter" | "resource";
283
+ code: string;
284
+ verificationId: string;
285
+ }
286
+ | { kind: "cancelled" };
287
+
252
288
  interface VerificationBase {
253
289
  id: string;
254
290
  classification: VerificationClassification;
255
291
  expectedFailure?: string;
292
+ executionBindings?: Record<string, string | null>;
256
293
  }
257
294
 
258
295
  export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
@@ -338,6 +375,7 @@ export interface PhaseBoundary {
338
375
  }
339
376
 
340
377
  export interface TaskBoundary {
378
+ verificationMode?: "behavior" | "mechanical" | "refactor";
341
379
  changeId: string;
342
380
  taskId: string;
343
381
  dependsOn: string[];
@@ -408,6 +446,9 @@ type ImplementGraphDiagnosticDetails = Partial<{
408
446
  producerTaskId: string;
409
447
  producerPhase: ImplementationPhase;
410
448
  dependencyTaskId: string;
449
+ field: string;
450
+ expectedPaths: string[];
451
+ actualPaths: string[];
411
452
  }>;
412
453
 
413
454
  export type ImplementGraphReadinessDiagnostic =
@@ -449,6 +490,10 @@ export interface PacketEnvelope {
449
490
  output: "evidence" | "diff";
450
491
  }
451
492
 
493
+ // JSON-schema counterpart for model-visible path arguments; keep its behavior
494
+ // checked against isValidRelativePath rather than hiding constraints in prose.
495
+ export const RELATIVE_PATH_PATTERN = String.raw`^(?:\.|(?!/)(?![a-zA-Z]:/)(?!\./)(?![\s\S]*\.\.)(?![\s\S]*[\\\u0000])(?![\s\S]*//)(?![\s\S]*/\.(?:/|$))(?![\s\S]*/$)[\s\S]+)$`;
496
+
452
497
  export function isValidRelativePath(p: unknown): p is string {
453
498
  return (
454
499
  typeof p === "string" &&
@@ -512,7 +557,7 @@ function validScriptCommand(value: unknown): value is string {
512
557
  value.length > 0 &&
513
558
  value.length <= 1024 &&
514
559
  value.trim() === value &&
515
- !UNSAFE_VERIFICATION_TOKEN.test(value)
560
+ !value.includes("\0")
516
561
  );
517
562
  }
518
563
 
@@ -575,6 +620,7 @@ function validateExecutableRunner(value: unknown): value is ExecutableRunner {
575
620
 
576
621
  function validateVitestRunner(value: ExecutableRunner): boolean {
577
622
  if (value.kind === "package-script") {
623
+ if (UNSAFE_VERIFICATION_TOKEN.test(value.command)) return false;
578
624
  const tokens = value.command.split(/\s+/u);
579
625
  return (
580
626
  tokens[0] === "vitest" ||
@@ -591,8 +637,23 @@ function validateAtomicVerification(
591
637
  ): value is AtomicVerificationContract {
592
638
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
593
639
  const contract = value as Record<string, unknown>;
594
- const expectedFailure =
595
- contract.classification === "expected-red" ? ["expectedFailure"] : [];
640
+ if (
641
+ contract.executionBindings !== undefined &&
642
+ (!contract.executionBindings ||
643
+ typeof contract.executionBindings !== "object" ||
644
+ Array.isArray(contract.executionBindings) ||
645
+ Object.entries(contract.executionBindings).some(
646
+ ([key, hash]) =>
647
+ !VERIFICATION_CONFIGURATION_PATHS.includes(key) ||
648
+ (hash !== null &&
649
+ (typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash))),
650
+ ))
651
+ )
652
+ return false;
653
+ const expectedFailure = [
654
+ ...(contract.classification === "expected-red" ? ["expectedFailure"] : []),
655
+ ...(contract.executionBindings === undefined ? [] : ["executionBindings"]),
656
+ ];
596
657
  if (contract.kind === "vitest") {
597
658
  const executableRunner = contract.runner as ExecutableRunner;
598
659
  return (
@@ -610,8 +671,8 @@ function validateAtomicVerification(
610
671
  validateExecutableRunner(contract.runner) &&
611
672
  validateVitestRunner(executableRunner) &&
612
673
  Array.isArray(contract.testFiles) &&
613
- contract.testFiles.length > 0 &&
614
- contract.testFiles.length <= 64 &&
674
+ contract.testFiles.length >= VERIFICATION_LIMITS.minTestFiles &&
675
+ contract.testFiles.length <= VERIFICATION_LIMITS.maxTestFiles &&
615
676
  contract.testFiles.every(isValidRelativePath) &&
616
677
  new Set(contract.testFiles).size === contract.testFiles.length &&
617
678
  validVerificationArgs(contract.args) &&
@@ -679,8 +740,8 @@ function validateStructuredVerification(
679
740
  String(contract.classification),
680
741
  ) ||
681
742
  !Array.isArray(contract.steps) ||
682
- contract.steps.length === 0 ||
683
- contract.steps.length > 8 ||
743
+ contract.steps.length < VERIFICATION_LIMITS.minSteps ||
744
+ contract.steps.length > VERIFICATION_LIMITS.maxSteps ||
684
745
  !contract.steps.every((step) => validateAtomicVerification(step))
685
746
  ) {
686
747
  return false;
@@ -719,6 +780,38 @@ export function verificationSteps(
719
780
  : [structuredClone(normalized.value)];
720
781
  }
721
782
 
783
+ export const VERIFICATION_CONFIGURATION_PATHS: readonly string[] = [
784
+ "package.json",
785
+ "package-lock.json",
786
+ "npm-shrinkwrap.json",
787
+ "bun.lock",
788
+ "bun.lockb",
789
+ "pnpm-lock.yaml",
790
+ "yarn.lock",
791
+ ".npmrc",
792
+ ".yarnrc",
793
+ ".yarnrc.yml",
794
+ "bunfig.toml",
795
+ "pnpm-workspace.yaml",
796
+ ];
797
+
798
+ export function verificationBoundInputPaths(
799
+ verification: VerificationContract,
800
+ ): string[] {
801
+ return [
802
+ ...new Set([
803
+ ...verificationInputPaths(verification),
804
+ ...verificationSteps(verification).flatMap((step) =>
805
+ step.kind === "vitest" ||
806
+ step.kind === "package-script" ||
807
+ ("runner" in step && step.runner.kind === "package-script")
808
+ ? VERIFICATION_CONFIGURATION_PATHS
809
+ : [],
810
+ ),
811
+ ]),
812
+ ];
813
+ }
814
+
722
815
  export function verificationInputPaths(
723
816
  verification: VerificationContract,
724
817
  ): string[] {
@@ -976,6 +1069,7 @@ function isWithinRoots(path: string, roots: string[]): boolean {
976
1069
  function validatePhaseBoundary(
977
1070
  value: unknown,
978
1071
  phase: ImplementationPhase,
1072
+ mode: unknown = "behavior",
979
1073
  ): value is PhaseBoundary {
980
1074
  if (
981
1075
  !hasExactKeys(
@@ -1002,7 +1096,10 @@ function validatePhaseBoundary(
1002
1096
  const verification = validateVerificationContract(value.verification);
1003
1097
  if (!verification.ok) return false;
1004
1098
  const classification = {
1005
- red: "expected-red",
1099
+ red:
1100
+ mode !== undefined && mode !== "behavior"
1101
+ ? "expected-green"
1102
+ : "expected-red",
1006
1103
  green: "expected-green",
1007
1104
  refactor: "expected-refactor",
1008
1105
  } as const;
@@ -1014,19 +1111,27 @@ function validateTaskBoundary(
1014
1111
  snapshot: unknown,
1015
1112
  ): value is TaskBoundary {
1016
1113
  if (
1017
- !hasExactKeys(value, [
1018
- "changeId",
1019
- "taskId",
1020
- "dependsOn",
1021
- "objective",
1022
- "context",
1023
- "roots",
1024
- "phases",
1025
- "scheduling",
1026
- "agents",
1027
- "approvedDependencies",
1028
- "impactClosure",
1029
- ]) ||
1114
+ !hasExactKeys(
1115
+ value,
1116
+ [
1117
+ "changeId",
1118
+ "taskId",
1119
+ "dependsOn",
1120
+ "objective",
1121
+ "context",
1122
+ "roots",
1123
+ "phases",
1124
+ "scheduling",
1125
+ "agents",
1126
+ "approvedDependencies",
1127
+ "impactClosure",
1128
+ ],
1129
+ ["verificationMode"],
1130
+ ) ||
1131
+ (value.verificationMode !== undefined &&
1132
+ !["behavior", "mechanical", "refactor"].includes(
1133
+ String(value.verificationMode),
1134
+ )) ||
1030
1135
  !validIdentifier(value.changeId) ||
1031
1136
  !validIdentifier(value.taskId) ||
1032
1137
  !validateIdentifierSet(value.dependsOn) ||
@@ -1038,7 +1143,7 @@ function validateTaskBoundary(
1038
1143
  typeof value.context.contract !== "string" ||
1039
1144
  !validateRoots(value.roots) ||
1040
1145
  !hasExactKeys(value.phases, ["red", "green"], ["refactor"]) ||
1041
- !validatePhaseBoundary(value.phases.red, "red") ||
1146
+ !validatePhaseBoundary(value.phases.red, "red", value.verificationMode) ||
1042
1147
  !validatePhaseBoundary(value.phases.green, "green") ||
1043
1148
  (value.phases.refactor !== undefined &&
1044
1149
  !validatePhaseBoundary(value.phases.refactor, "refactor")) ||
@@ -1109,6 +1214,9 @@ export type ImplementGraphContractDiagnostic =
1109
1214
  field?: string;
1110
1215
  category?: string;
1111
1216
  outputId?: string;
1217
+ path?: string;
1218
+ expectedPaths?: string[];
1219
+ actualPaths?: string[];
1112
1220
  }
1113
1221
  | {
1114
1222
  code: "invalid-output-path";
@@ -1120,6 +1228,7 @@ export type ImplementGraphContractDiagnostic =
1120
1228
  function phaseBoundaryContractDiagnostic(
1121
1229
  value: unknown,
1122
1230
  phase: ImplementationPhase,
1231
+ mode: unknown = "behavior",
1123
1232
  ): Omit<
1124
1233
  Extract<
1125
1234
  ImplementGraphContractDiagnostic,
@@ -1176,7 +1285,10 @@ function phaseBoundaryContractDiagnostic(
1176
1285
  };
1177
1286
  }
1178
1287
  const classification = {
1179
- red: "expected-red",
1288
+ red:
1289
+ mode !== undefined && mode !== "behavior"
1290
+ ? "expected-green"
1291
+ : "expected-red",
1180
1292
  green: "expected-green",
1181
1293
  refactor: "expected-refactor",
1182
1294
  } as const;
@@ -1227,22 +1339,33 @@ function taskBoundaryContractDiagnostic(
1227
1339
  category,
1228
1340
  });
1229
1341
  if (
1230
- !hasExactKeys(value, [
1231
- "changeId",
1232
- "taskId",
1233
- "dependsOn",
1234
- "objective",
1235
- "context",
1236
- "roots",
1237
- "phases",
1238
- "scheduling",
1239
- "agents",
1240
- "approvedDependencies",
1241
- "impactClosure",
1242
- ])
1342
+ !hasExactKeys(
1343
+ value,
1344
+ [
1345
+ "changeId",
1346
+ "taskId",
1347
+ "dependsOn",
1348
+ "objective",
1349
+ "context",
1350
+ "roots",
1351
+ "phases",
1352
+ "scheduling",
1353
+ "agents",
1354
+ "approvedDependencies",
1355
+ "impactClosure",
1356
+ ],
1357
+ ["verificationMode"],
1358
+ )
1243
1359
  ) {
1244
1360
  return diagnostic("task", "shape");
1245
1361
  }
1362
+ if (
1363
+ value.verificationMode !== undefined &&
1364
+ !["behavior", "mechanical", "refactor"].includes(
1365
+ String(value.verificationMode),
1366
+ )
1367
+ )
1368
+ return diagnostic("verificationMode", "shape");
1246
1369
  if (!validIdentifier(value.changeId)) {
1247
1370
  return diagnostic("changeId", "identifier");
1248
1371
  }
@@ -1275,7 +1398,11 @@ function taskBoundaryContractDiagnostic(
1275
1398
  for (const phase of IMPLEMENTATION_PHASES) {
1276
1399
  const boundary = value.phases[phase];
1277
1400
  if (phase === "refactor" && boundary === undefined) continue;
1278
- const phaseDiagnostic = phaseBoundaryContractDiagnostic(boundary, phase);
1401
+ const phaseDiagnostic = phaseBoundaryContractDiagnostic(
1402
+ boundary,
1403
+ phase,
1404
+ value.verificationMode,
1405
+ );
1279
1406
  if (phaseDiagnostic)
1280
1407
  return { ...diagnostic("phases", "contract"), ...phaseDiagnostic };
1281
1408
  }
@@ -1348,6 +1475,26 @@ function taskBoundaryContractDiagnostic(
1348
1475
  snapshot,
1349
1476
  );
1350
1477
  if (impactReason !== null) {
1478
+ if (impactReason === "current-task related test is outside the write set") {
1479
+ const related = (value.impactClosure as ImpactClosureContract)
1480
+ .relatedTests;
1481
+ const index = related.findIndex(
1482
+ (test) =>
1483
+ test.disposition === "current-task" &&
1484
+ !declared.write.includes(test.path),
1485
+ );
1486
+ const test = related[index];
1487
+ if (test)
1488
+ return {
1489
+ ...diagnostic(
1490
+ `impactClosure.relatedTests.${index}.disposition`,
1491
+ "current-task-outside-write-set",
1492
+ ),
1493
+ path: test.path,
1494
+ expectedPaths: [...declared.write].sort(),
1495
+ actualPaths: [test.path],
1496
+ };
1497
+ }
1351
1498
  return diagnostic("impactClosure", impactClosureCategory(impactReason));
1352
1499
  }
1353
1500
  return validateTaskBoundary(value, snapshot)
@@ -1361,6 +1508,7 @@ export function validateImplementGraphBoundary(value: unknown):
1361
1508
  ok: false;
1362
1509
  reason: string;
1363
1510
  diagnostic: ImplementGraphContractDiagnostic;
1511
+ diagnostics?: ImplementGraphContractDiagnostic[];
1364
1512
  } {
1365
1513
  if (
1366
1514
  !hasExactKeys(value, ["changeId", "tasks", "outputs"]) ||
@@ -1370,29 +1518,33 @@ export function validateImplementGraphBoundary(value: unknown):
1370
1518
  value.tasks.length > 128 ||
1371
1519
  !Array.isArray(value.outputs) ||
1372
1520
  value.outputs.length > 512
1373
- ) {
1521
+ )
1374
1522
  return {
1375
1523
  ok: false,
1376
1524
  reason: "invalid Implement graph boundary",
1377
1525
  diagnostic: { code: "invalid-implement-graph" },
1378
1526
  };
1379
- }
1380
1527
 
1528
+ // Collect independent task/output failures without admitting a partial graph.
1529
+ const failures: Array<{
1530
+ reason: string;
1531
+ diagnostic: ImplementGraphContractDiagnostic;
1532
+ }> = [];
1381
1533
  for (const candidate of value.tasks) {
1382
1534
  if (
1383
1535
  !candidate ||
1384
1536
  typeof candidate !== "object" ||
1385
1537
  Array.isArray(candidate)
1386
1538
  ) {
1387
- return {
1388
- ok: false,
1539
+ failures.push({
1389
1540
  reason: "invalid Implement task boundary",
1390
1541
  diagnostic: {
1391
1542
  code: "invalid-implement-graph",
1392
1543
  field: "tasks",
1393
1544
  category: "shape",
1394
1545
  },
1395
- };
1546
+ });
1547
+ continue;
1396
1548
  }
1397
1549
  const task = candidate as Record<string, unknown>;
1398
1550
  const diagnostic = Object.hasOwn(task, "changeId")
@@ -1405,31 +1557,27 @@ export function validateImplementGraphBoundary(value: unknown):
1405
1557
  { changeId: value.changeId, ...task },
1406
1558
  undefined,
1407
1559
  );
1408
- if (diagnostic) {
1409
- return {
1410
- ok: false,
1560
+ if (diagnostic)
1561
+ failures.push({
1411
1562
  reason: "invalid Implement task boundary",
1412
1563
  diagnostic: { code: "invalid-implement-graph", ...diagnostic },
1413
- };
1414
- }
1564
+ });
1415
1565
  }
1416
-
1417
1566
  for (const candidate of value.outputs) {
1418
1567
  if (
1419
1568
  !candidate ||
1420
1569
  typeof candidate !== "object" ||
1421
1570
  Array.isArray(candidate)
1422
1571
  ) {
1423
- return {
1424
- ok: false,
1572
+ failures.push({
1425
1573
  reason: "invalid Implement graph output",
1426
1574
  diagnostic: { code: "invalid-implement-graph" },
1427
- };
1575
+ });
1576
+ continue;
1428
1577
  }
1429
1578
  const output = candidate as Record<string, unknown>;
1430
1579
  if (!isValidRelativePath(output.path)) {
1431
- return {
1432
- ok: false,
1580
+ failures.push({
1433
1581
  reason: "invalid Implement graph output path",
1434
1582
  diagnostic: {
1435
1583
  code: "invalid-output-path",
@@ -1437,7 +1585,8 @@ export function validateImplementGraphBoundary(value: unknown):
1437
1585
  field: "outputs.path",
1438
1586
  category: "path",
1439
1587
  },
1440
- };
1588
+ });
1589
+ continue;
1441
1590
  }
1442
1591
  if (
1443
1592
  !hasExactKeys(output, ["id", "path", "producer", "postcondition"]) ||
@@ -1449,8 +1598,7 @@ export function validateImplementGraphBoundary(value: unknown):
1449
1598
  ) ||
1450
1599
  output.postcondition !== "regular-file"
1451
1600
  ) {
1452
- return {
1453
- ok: false,
1601
+ failures.push({
1454
1602
  reason: "invalid Implement graph output",
1455
1603
  diagnostic: {
1456
1604
  code: "invalid-implement-graph",
@@ -1458,10 +1606,18 @@ export function validateImplementGraphBoundary(value: unknown):
1458
1606
  field: "outputs",
1459
1607
  category: "shape",
1460
1608
  },
1461
- };
1609
+ });
1462
1610
  }
1463
1611
  }
1464
-
1612
+ const first = failures[0];
1613
+ if (first)
1614
+ return {
1615
+ ok: false,
1616
+ ...first,
1617
+ ...(failures.length > 1
1618
+ ? { diagnostics: failures.map((failure) => failure.diagnostic) }
1619
+ : {}),
1620
+ };
1465
1621
  return {
1466
1622
  ok: true,
1467
1623
  value: structuredClone(value) as unknown as ImplementGraphBoundary,
@@ -1868,6 +2024,24 @@ export interface DiffResult {
1868
2024
  contractCompliant: boolean;
1869
2025
  }
1870
2026
 
2027
+ // Diagnostics contain only code-owned field names, never unexpected keys or values.
2028
+ function evidenceFieldsFailure(
2029
+ value: unknown,
2030
+ fields: readonly string[],
2031
+ location: string,
2032
+ ): { ok: false; reason: string } {
2033
+ if (!value || typeof value !== "object" || Array.isArray(value))
2034
+ return { ok: false, reason: `${location}: expected an object` };
2035
+ const missing = fields.filter((field) => !Object.hasOwn(value, field));
2036
+ return {
2037
+ ok: false,
2038
+ reason:
2039
+ missing.length > 0
2040
+ ? `${location}: missing fields ${missing.join(", ")}`
2041
+ : `${location}: unexpected fields; allowed fields ${fields.join(", ")}`,
2042
+ };
2043
+ }
2044
+
1871
2045
  export function validateEvidenceResult(value: unknown): {
1872
2046
  ok: boolean;
1873
2047
  reason?: string;
@@ -1891,7 +2065,7 @@ export function validateEvidenceResult(value: unknown): {
1891
2065
  "hints",
1892
2066
  ] as const;
1893
2067
  if (!hasExactKeys(r, fields)) {
1894
- return { ok: false, reason: "invalid evidence result fields" };
2068
+ return evidenceFieldsFailure(r, fields, "result");
1895
2069
  }
1896
2070
  if (!validIdentifier(r.id) || typeof r.role !== "string")
1897
2071
  return { ok: false, reason: "invalid identity" };
@@ -1911,25 +2085,38 @@ export function validateEvidenceResult(value: unknown): {
1911
2085
  return { ok: false, reason: `invalid evidence field: ${field}` };
1912
2086
  }
1913
2087
  }
1914
- if (
1915
- !Array.isArray(r.citations) ||
1916
- r.citations.some(
1917
- (citation) =>
1918
- !hasExactKeys(citation, ["path", "lines"]) ||
1919
- !isValidRelativePath(citation.path) ||
1920
- typeof citation.lines !== "string",
1921
- )
1922
- ) {
1923
- return { ok: false, reason: "invalid evidence citations" };
1924
- }
1925
- if (
1926
- !hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]) ||
1927
- !validatePathSet(r.hints.writeSet) ||
1928
- typeof r.hints.verification !== "string" ||
1929
- !(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact)
1930
- ) {
1931
- return { ok: false, reason: "invalid evidence hints" };
1932
- }
2088
+ if (!Array.isArray(r.citations))
2089
+ return { ok: false, reason: "citations: expected an array" };
2090
+ for (const [index, citation] of r.citations.entries()) {
2091
+ const field = `citations[${index}]`;
2092
+ if (!hasExactKeys(citation, ["path", "lines"]))
2093
+ return evidenceFieldsFailure(citation, ["path", "lines"], field);
2094
+ if (!isValidRelativePath(citation.path))
2095
+ return {
2096
+ ok: false,
2097
+ reason: `${field}.path: expected a canonical relative path`,
2098
+ };
2099
+ if (typeof citation.lines !== "string")
2100
+ return { ok: false, reason: `${field}.lines: expected a string` };
2101
+ }
2102
+ if (!hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]))
2103
+ return evidenceFieldsFailure(
2104
+ r.hints,
2105
+ ["writeSet", "verification", "agentsImpact"],
2106
+ "hints",
2107
+ );
2108
+ if (!validatePathSet(r.hints.writeSet))
2109
+ return {
2110
+ ok: false,
2111
+ reason: "hints.writeSet: expected unique canonical relative paths",
2112
+ };
2113
+ if (typeof r.hints.verification !== "string")
2114
+ return { ok: false, reason: "hints.verification: expected a string" };
2115
+ if (!(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact))
2116
+ return {
2117
+ ok: false,
2118
+ reason: `hints.agentsImpact: expected one of ${AGENTS_IMPACTS.join(", ")}`,
2119
+ };
1933
2120
  const serialized = JSON.stringify(r);
1934
2121
  if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
1935
2122
  return {
@@ -1970,36 +2157,65 @@ function validateDesignEvidenceResult(r: Record<string, unknown>): {
1970
2157
  "success_criteria_hints",
1971
2158
  ] as const;
1972
2159
  if (!hasExactKeys(r, fields)) {
1973
- return { ok: false, reason: "invalid Design evidence result fields" };
1974
- }
1975
- if (
1976
- !validIdentifier(r.id) ||
1977
- r.packet_id !== r.id ||
1978
- !isValidRelativePath(r.module_name)
1979
- ) {
1980
- return { ok: false, reason: "invalid Design evidence identity" };
2160
+ return evidenceFieldsFailure(r, fields, "result");
1981
2161
  }
2162
+ if (!validIdentifier(r.id))
2163
+ return { ok: false, reason: "id: expected a valid identifier" };
2164
+ if (r.packet_id !== r.id)
2165
+ return { ok: false, reason: "packet_id: must equal id" };
2166
+ if (!isValidRelativePath(r.module_name))
2167
+ return {
2168
+ ok: false,
2169
+ reason: "module_name: expected a canonical relative path or module slug",
2170
+ };
1982
2171
  if (!validatePathSet(r.scope) || r.scope.length === 0) {
1983
- return { ok: false, reason: "invalid Design evidence scope" };
2172
+ return {
2173
+ ok: false,
2174
+ reason:
2175
+ "scope: expected a nonempty set of unique canonical relative paths",
2176
+ };
1984
2177
  }
1985
- if (!validatePathSet(r.files_read) || !validatePathSet(r.write_set_hints)) {
1986
- return { ok: false, reason: "invalid Design evidence path set" };
2178
+ for (const field of ["files_read", "write_set_hints"] as const) {
2179
+ if (!validatePathSet(r[field]))
2180
+ return {
2181
+ ok: false,
2182
+ reason: `${field}: expected unique canonical relative paths`,
2183
+ };
1987
2184
  }
1988
- if (
1989
- !Array.isArray(r.evidence) ||
1990
- r.evidence.some(
1991
- (entry) =>
1992
- !hasExactKeys(entry, ["claim", "path", "line_start", "line_end"]) ||
1993
- typeof entry.claim !== "string" ||
1994
- entry.claim.length === 0 ||
1995
- !isValidRelativePath(entry.path) ||
1996
- !Number.isSafeInteger(entry.line_start) ||
1997
- !Number.isSafeInteger(entry.line_end) ||
1998
- (entry.line_start as number) < 1 ||
1999
- (entry.line_end as number) < (entry.line_start as number),
2185
+ if (!Array.isArray(r.evidence))
2186
+ return { ok: false, reason: "evidence: expected an array" };
2187
+ for (const [index, entry] of r.evidence.entries()) {
2188
+ const field = `evidence[${index}]`;
2189
+ if (!hasExactKeys(entry, ["claim", "path", "line_start", "line_end"])) {
2190
+ return evidenceFieldsFailure(
2191
+ entry,
2192
+ ["claim", "path", "line_start", "line_end"],
2193
+ field,
2194
+ );
2195
+ }
2196
+ if (typeof entry.claim !== "string" || entry.claim.length === 0)
2197
+ return { ok: false, reason: `${field}.claim: expected nonempty text` };
2198
+ if (!isValidRelativePath(entry.path))
2199
+ return {
2200
+ ok: false,
2201
+ reason: `${field}.path: expected a canonical relative path`,
2202
+ };
2203
+ if (
2204
+ !Number.isSafeInteger(entry.line_start) ||
2205
+ (entry.line_start as number) < 1
2000
2206
  )
2001
- ) {
2002
- return { ok: false, reason: "invalid Design evidence citations" };
2207
+ return {
2208
+ ok: false,
2209
+ reason: `${field}.line_start: expected a positive safe integer`,
2210
+ };
2211
+ if (
2212
+ !Number.isSafeInteger(entry.line_end) ||
2213
+ (entry.line_end as number) < (entry.line_start as number)
2214
+ )
2215
+ return {
2216
+ ok: false,
2217
+ reason: `${field}.line_end: expected a safe integer >= line_start`,
2218
+ };
2003
2219
  }
2004
2220
  for (const field of [
2005
2221
  "existing_structures",