@abelxiaoxing/cadence 1.8.4 → 2.0.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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +92 -20
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
package/src/contracts.ts DELETED
@@ -1,2466 +0,0 @@
1
- // Strict request/result contracts for the private orchestration kernel.
2
- // Structural validation only; no runtime dependency on a schema library.
3
-
4
- export type {
5
- ControlCommand,
6
- ControlCommandName,
7
- ControlCommandValidation,
8
- ControlStage,
9
- } from "./control-contracts.ts";
10
- export {
11
- assertControlCommand,
12
- CONTROL_COMMANDS,
13
- CONTROL_STAGES,
14
- controlRunKey,
15
- validateControlCommand,
16
- } from "./control-contracts.ts";
17
-
18
- export const STAGES = [
19
- "abel-design",
20
- "abel-implement",
21
- "abel-diagnose",
22
- ] as const;
23
- export const ROLES = [
24
- "design-explorer",
25
- "implementation-worker",
26
- "diagnosis-worker",
27
- ] as const;
28
- export const OUTPUT_KINDS = ["evidence", "diff"] as const;
29
- export const PHASES = ["evidence", "red", "green", "refactor"] as const;
30
-
31
- export const AGENTS_IMPACTS = [
32
- "none",
33
- "update-existing",
34
- "create-index",
35
- "remove-index",
36
- ] as const;
37
- export type AgentsImpact = (typeof AGENTS_IMPACTS)[number];
38
-
39
- export const IMPACT_SURFACES = [
40
- "none",
41
- "route-authorization",
42
- "page-state",
43
- "api-response",
44
- "public-html",
45
- ] as const;
46
- export type ImpactSurface = (typeof IMPACT_SURFACES)[number];
47
-
48
- export const LIMITS = {
49
- maxActiveChildSessions: 4,
50
- maxEnvelopeBytes: 64 * 1024,
51
- phaseTimeoutMs: 20 * 60 * 1000,
52
- maxCompleteResultBytes: 64 * 1024,
53
- } as const;
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
-
63
- export const ARTIFACT_FAILURE_CODES = [
64
- "agents-impact-mismatch",
65
- "agents-target-mismatch",
66
- "baseline-bound-mismatch",
67
- "baseline-deletion-kind",
68
- "baseline-directory-kind",
69
- "baseline-directory-mismatch",
70
- "baseline-directory-special-file",
71
- "baseline-directory-symlink",
72
- "baseline-file-kind",
73
- "baseline-shape",
74
- "candidate-check-failed",
75
- "checkout-special-file",
76
- "checkout-symlink",
77
- "dependency-bound-mismatch",
78
- "escaping-path",
79
- "git-apply-check-failed",
80
- "git-apply-failed",
81
- "git-numstat-rejected",
82
- "git-summary-rejected",
83
- "ignored-baseline-input",
84
- "incomplete-baseline",
85
- "invalid-approved-dependencies",
86
- "invalid-checkpoint-contract",
87
- "invalid-diff",
88
- "invalid-diff-bytes",
89
- "invalid-snapshot",
90
- "invalid-structural-result",
91
- "child-no-structural-submit",
92
- "structural-identity-mismatch",
93
- "invalid-verification-contract",
94
- "lockfile-bound-mismatch",
95
- "noncanonical-path",
96
- "nonregular-mode",
97
- "outside-managed-region",
98
- "package-bound-mismatch",
99
- "package-manifest-invalid",
100
- "parent-review-rejected",
101
- "producer-output-unavailable",
102
- "red-not-witnessed",
103
- "submodule",
104
- "target-baseline-missing",
105
- "verification-baseline-missing",
106
- "verification-input-unavailable",
107
- "verification-rejected",
108
- "verification-report-invalid",
109
- "verification-report-missing",
110
- "write-set-mismatch",
111
- ] as const;
112
- export const STALE_FAILURE_CODES = [
113
- "baseline-deletion-drift",
114
- "baseline-directory-drift",
115
- "baseline-file-drift",
116
- "git-apply-check-failed",
117
- "stale-snapshot",
118
- ] as const;
119
- export const ENVIRONMENT_FAILURE_CODES = [
120
- "child-provider-authentication-failed",
121
- "bubblewrap-launch-failed",
122
- "bubblewrap-or-dependency-unavailable",
123
- "bun-executable-unavailable",
124
- "checkpoint-unavailable",
125
- "checkout-cleanup-failed",
126
- "checkout-failed",
127
- "clone-alternates",
128
- "clone-failed",
129
- "dependency-path-unsafe",
130
- "git-apply-check-unavailable",
131
- "git-apply-failed",
132
- "git-apply-unavailable",
133
- "git-ignore-check-failed",
134
- "git-index-unavailable",
135
- "invalid-subagent-endpoint",
136
- "root-unavailable",
137
- "sandbox-runtime-unavailable",
138
- ] as const;
139
- export const APPROVAL_BOUNDARY_CODES = [
140
- "unapproved-dependency-change",
141
- "behavior-contract-insufficient",
142
- "architecture-contract-insufficient",
143
- "task-scope-insufficient",
144
- "conflict-resource-authority-insufficient",
145
- "verification-contract-insufficient",
146
- "agents-contract-insufficient",
147
- "irreversible-scope-insufficient",
148
- ] as const;
149
- export const VERIFICATION_ADAPTER_CODES = [
150
- "input-missing",
151
- "input-unsafe",
152
- "local-executable-missing",
153
- "runner-missing",
154
- "script-command-mismatch",
155
- "script-missing",
156
- "script-unsafe",
157
- "report-arguments-conflict",
158
- "verification-config-mismatch",
159
- "verification-config-unsafe",
160
- ] as const;
161
- export const CHILD_TRANSPORT_CODES = [
162
- "child-timeout",
163
- "transport-failure",
164
- ] as const;
165
- export const FAILURE_STAGES = [
166
- "child-timeout",
167
- "child-provider-stream",
168
- "candidate-diff",
169
- "candidate-apply",
170
- "phase-runtime",
171
- ] as const;
172
-
173
- export const SUBMIT_FINAL_CATEGORIES = ["text-only"] as const;
174
- export const SUBMIT_SCHEMA_STATES = [
175
- "not-submitted",
176
- "valid",
177
- "invalid",
178
- ] as const;
179
- export const IDENTITY_DIMENSIONS = [
180
- "request",
181
- "role",
182
- "task",
183
- "phase",
184
- ] as const;
185
-
186
- export type ArtifactFailureCode = (typeof ARTIFACT_FAILURE_CODES)[number];
187
- export type StaleFailureCode = (typeof STALE_FAILURE_CODES)[number];
188
- export type EnvironmentFailureCode = (typeof ENVIRONMENT_FAILURE_CODES)[number];
189
- export type ApprovalBoundaryCode = (typeof APPROVAL_BOUNDARY_CODES)[number];
190
- export type VerificationAdapterCode =
191
- (typeof VERIFICATION_ADAPTER_CODES)[number];
192
- export type ChildTransportCode = (typeof CHILD_TRANSPORT_CODES)[number];
193
- export type FailureStage = (typeof FAILURE_STAGES)[number];
194
- export type SubmitFinalCategory = (typeof SUBMIT_FINAL_CATEGORIES)[number];
195
- export type SubmitSchemaState = (typeof SUBMIT_SCHEMA_STATES)[number];
196
- export type IdentityDimension = (typeof IDENTITY_DIMENSIONS)[number];
197
-
198
- export interface SafeFailureDetails {
199
- finalCategory?: SubmitFinalCategory;
200
- submitAttempts?: number;
201
- schema?: SubmitSchemaState;
202
- identityMismatch?: IdentityDimension[];
203
- verificationId?: string;
204
- }
205
-
206
- export type EnvironmentFailure = {
207
- kind: "environment";
208
- code: EnvironmentFailureCode;
209
- message?: string;
210
- stage?: FailureStage;
211
- };
212
-
213
- export type CandidateFailure =
214
- | {
215
- kind: "artifact";
216
- code: ArtifactFailureCode;
217
- stage: FailureStage;
218
- details?: SafeFailureDetails;
219
- }
220
- | { kind: "stale"; code: StaleFailureCode; stage: FailureStage }
221
- | EnvironmentFailure
222
- | { kind: "verification-adapter"; code: VerificationAdapterCode }
223
- | { kind: "approval-boundary"; code: ApprovalBoundaryCode }
224
- | { kind: "cancelled"; code: "cancelled" }
225
- | { kind: "result-limit"; limitBytes: number };
226
-
227
- export type ChildFailure =
228
- | {
229
- kind: "execution-limit";
230
- code: "child-turn-limit" | "child-context-limit";
231
- }
232
- | CandidateFailure
233
- | {
234
- kind: "transport";
235
- code: ChildTransportCode;
236
- stage: FailureStage;
237
- details?: SafeFailureDetails;
238
- };
239
-
240
- const NONCANONICAL = /(^|\/)\.\.(\/|$)|(^|\/)\/|^\//;
241
-
242
- export type VerificationClassification =
243
- | "expected-red"
244
- | "expected-green"
245
- | "expected-refactor";
246
-
247
- export interface VerificationFailureSummary {
248
- verificationId: string;
249
- code: string;
250
- exitCode?: number;
251
- failures: string[];
252
- stdout: string;
253
- stderr: string;
254
- truncated: boolean;
255
- nextStep: string;
256
- }
257
-
258
- /** Runtime-owned evidence; unavailable checks never become product baselines. */
259
- export interface VerificationEvidence {
260
- id: string;
261
- exitCode: number;
262
- classification: VerificationClassification;
263
- tests?: number;
264
- failureIdentities: string[];
265
- policy: "report-file-v3";
266
- attributionReliable?: boolean;
267
- }
268
- export type VerificationObservation =
269
- | { kind: "accepted"; evidence: VerificationEvidence }
270
- | {
271
- kind: "rejected";
272
- code: "red-not-witnessed" | "verification-rejected";
273
- evidence: VerificationEvidence;
274
- diagnostic?: VerificationFailureSummary;
275
- }
276
- | {
277
- kind: "unavailable";
278
- category: "environment" | "adapter" | "resource";
279
- code: string;
280
- verificationId: string;
281
- diagnostic?: VerificationFailureSummary;
282
- inputObservation?: VerificationInputObservation;
283
- }
284
- | { kind: "cancelled" };
285
-
286
- /** Parent adapter observation, never a Worker assertion or an error-text guess. */
287
- export interface VerificationInputObservation {
288
- path: string;
289
- kind: "absent" | "unsafe";
290
- }
291
-
292
- interface VerificationBase {
293
- id: string;
294
- classification: VerificationClassification;
295
- expectedFailure?: string;
296
- executionBindings?: Record<string, string | null>;
297
- }
298
-
299
- export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
300
-
301
- export interface PackageScriptRunner {
302
- kind: "package-script";
303
- packageManager: PackageManager;
304
- script: string;
305
- command: string;
306
- }
307
-
308
- export type ExecutableRunner =
309
- | PackageScriptRunner
310
- | { kind: "local-binary"; executable: string }
311
- | { kind: "npx"; executable: string; noInstall: true };
312
-
313
- export interface VitestVerificationContract extends VerificationBase {
314
- kind: "vitest";
315
- runner: ExecutableRunner;
316
- testFiles: string[];
317
- args: string[];
318
- minTests: number;
319
- }
320
-
321
- export interface PackageScriptVerificationContract extends VerificationBase {
322
- kind: "package-script";
323
- packageManager: PackageManager;
324
- script: string;
325
- command: string;
326
- args: string[];
327
- }
328
-
329
- export interface StaticCheckVerificationContract extends VerificationBase {
330
- kind: "static-check";
331
- runner: ExecutableRunner | { kind: "node"; script: string };
332
- args: string[];
333
- }
334
-
335
- export type AtomicVerificationContract =
336
- | VitestVerificationContract
337
- | PackageScriptVerificationContract
338
- | StaticCheckVerificationContract;
339
-
340
- export interface VerificationStepsContract {
341
- kind: "steps";
342
- id: string;
343
- classification: VerificationClassification;
344
- steps: AtomicVerificationContract[];
345
- }
346
-
347
- export type StructuredVerificationContract =
348
- | AtomicVerificationContract
349
- | VerificationStepsContract;
350
-
351
- export type VerificationContract = StructuredVerificationContract;
352
-
353
- export interface ImpactClosureContract {
354
- changedSurfaces: ImpactSurface[];
355
- searchEvidence: string[];
356
- relatedTests: Array<{
357
- path: string;
358
- disposition: "current-task" | "regression-task" | "unaffected";
359
- evidence: string;
360
- regressionTaskId?: string;
361
- }>;
362
- affectedSuite: string[];
363
- }
364
-
365
- export const IMPLEMENTATION_PHASES = ["red", "green", "refactor"] as const;
366
- export type ImplementationPhase = (typeof IMPLEMENTATION_PHASES)[number];
367
-
368
- export type VerificationInputBinding =
369
- | { kind: "workspace"; path: string }
370
- | { kind: "output"; outputId: string };
371
-
372
- export interface PhaseBoundary {
373
- read: string[];
374
- write: string[];
375
- delete: string[];
376
- verification: VerificationContract;
377
- verificationInputs: VerificationInputBinding[];
378
- verificationLock?: string;
379
- }
380
-
381
- export interface TaskBoundary {
382
- verificationMode?: "behavior" | "mechanical" | "refactor";
383
- changeId: string;
384
- taskId: string;
385
- dependsOn: string[];
386
- objective: string;
387
- context: { agents: string; contract: string };
388
- roots: string[];
389
- phases: {
390
- red: PhaseBoundary;
391
- green: PhaseBoundary;
392
- refactor?: PhaseBoundary;
393
- };
394
- scheduling: { conflicts: string[]; resources: string[] };
395
- agents: {
396
- impact: AgentsImpact;
397
- target?: string;
398
- managedOnly: true;
399
- };
400
- approvedDependencies: string[];
401
- impactClosure: ImpactClosureContract;
402
- }
403
-
404
- export interface ImplementGraphOutput {
405
- id: string;
406
- path: string;
407
- producer: { taskId: string; phase: ImplementationPhase };
408
- postcondition: "regular-file";
409
- }
410
-
411
- export type ImplementTaskBoundary = Omit<TaskBoundary, "changeId">;
412
-
413
- export interface ImplementGraphBoundary {
414
- changeId: string;
415
- tasks: ImplementTaskBoundary[];
416
- outputs: ImplementGraphOutput[];
417
- }
418
-
419
- export const IMPLEMENT_GRAPH_READINESS_CODES = [
420
- "dependency-blocked",
421
- "dependency-cycle",
422
- "duplicate-output-id",
423
- "duplicate-task-id",
424
- "graph-hash-mismatch",
425
- "invalid-implement-graph",
426
- "invalid-output-path",
427
- "multiple-output-producers",
428
- "output-not-declared",
429
- "output-producer-invalid",
430
- "output-write-not-approved",
431
- "phase-blocked",
432
- "producer-not-dependency",
433
- "producer-output-unavailable",
434
- "producer-phase-after-consumer",
435
- "unknown-dependency",
436
- "verification-input-binding-mismatch",
437
- "workspace-input-has-producer",
438
- "workspace-input-unavailable",
439
- ] as const;
440
-
441
- export type ImplementGraphReadinessCode =
442
- (typeof IMPLEMENT_GRAPH_READINESS_CODES)[number];
443
-
444
- type ImplementGraphDiagnosticDetails = Partial<{
445
- taskId: string;
446
- phase: ImplementationPhase;
447
- verificationId: string;
448
- outputId: string;
449
- path: string;
450
- producerTaskId: string;
451
- producerPhase: ImplementationPhase;
452
- dependencyTaskId: string;
453
- field: string;
454
- expectedPaths: string[];
455
- actualPaths: string[];
456
- }>;
457
-
458
- export type ImplementGraphReadinessDiagnostic =
459
- | ({
460
- kind: "graph-readiness";
461
- code: ImplementGraphReadinessCode;
462
- } & ImplementGraphDiagnosticDetails)
463
- | ({
464
- kind: "verification-adapter";
465
- code: VerificationAdapterCode;
466
- taskId: string;
467
- phase: ImplementationPhase;
468
- verificationId: string;
469
- } & ImplementGraphDiagnosticDetails)
470
- | ({
471
- kind: "design-readiness";
472
- code: "verification-contract-unsupported";
473
- taskId: string;
474
- phase: ImplementationPhase;
475
- verificationId?: string;
476
- } & ImplementGraphDiagnosticDetails);
477
-
478
- export interface PacketEnvelope {
479
- stage: "abel-design" | "abel-diagnose";
480
- role: "design-explorer" | "diagnosis-worker";
481
- runId?: string;
482
- id: string;
483
- phase: string;
484
- objective: string;
485
- roots: string[];
486
- context: { agents: string; contract: string };
487
- declared: {
488
- read: string[];
489
- write: string[];
490
- conflicts: string[];
491
- resources: string[];
492
- verificationLock?: string;
493
- };
494
- output: "evidence" | "diff";
495
- }
496
-
497
- // JSON-schema counterpart for model-visible path arguments; keep its behavior
498
- // checked against isValidRelativePath rather than hiding constraints in prose.
499
- export const RELATIVE_PATH_PATTERN = String.raw`^(?:\.|(?!/)(?![a-zA-Z]:/)(?!\./)(?![\s\S]*\.\.)(?![\s\S]*[\\\u0000])(?![\s\S]*//)(?![\s\S]*/\.(?:/|$))(?![\s\S]*/$)[\s\S]+)$`;
500
-
501
- export function isValidRelativePath(p: unknown): p is string {
502
- return (
503
- typeof p === "string" &&
504
- p.length > 0 &&
505
- p.length <= 512 &&
506
- !NONCANONICAL.test(p) &&
507
- !p.startsWith("/") &&
508
- !/^[a-z]:\//iu.test(p) &&
509
- (p === "." ||
510
- (!p.startsWith("./") &&
511
- !p.endsWith("/") &&
512
- !p.split("/").includes("."))) &&
513
- !p.includes("..") &&
514
- !p.includes("\\") &&
515
- !p.includes("\u0000")
516
- );
517
- }
518
-
519
- export function isAgentsPath(p: unknown): p is string {
520
- return isValidRelativePath(p) && /(?:^|\/)AGENTS\.md$/u.test(p);
521
- }
522
-
523
- const UNSAFE_VERIFICATION_TOKEN = /[;&|`$<>\n\r\0]/u;
524
- const VERIFICATION_NAME = /^[a-z0-9][a-z0-9._:@/-]*$/iu;
525
- const EXECUTABLE_NAME = /^[a-z0-9][a-z0-9._-]*$/iu;
526
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
527
-
528
- function validVerificationToken(value: unknown): value is string {
529
- const optionValue =
530
- typeof value === "string" && value.includes("=")
531
- ? value.slice(value.indexOf("=") + 1)
532
- : value;
533
- return (
534
- typeof value === "string" &&
535
- value.length > 0 &&
536
- value.length <= 512 &&
537
- value.trim() === value &&
538
- !UNSAFE_VERIFICATION_TOKEN.test(value) &&
539
- !value.includes("\\") &&
540
- !value.startsWith("/") &&
541
- !/^[a-z]:\//iu.test(value) &&
542
- !/(^|\/)\.\.(\/|$)/u.test(value) &&
543
- typeof optionValue === "string" &&
544
- !optionValue.startsWith("/") &&
545
- !/^[a-z]:\//iu.test(optionValue) &&
546
- !/(^|\/)\.\.(\/|$)/u.test(optionValue)
547
- );
548
- }
549
-
550
- function validVerificationArgs(value: unknown): value is string[] {
551
- return (
552
- Array.isArray(value) &&
553
- value.length <= 128 &&
554
- value.every(validVerificationToken)
555
- );
556
- }
557
-
558
- function validScriptCommand(value: unknown): value is string {
559
- return (
560
- typeof value === "string" &&
561
- value.length > 0 &&
562
- value.length <= 1024 &&
563
- value.trim() === value &&
564
- !value.includes("\0")
565
- );
566
- }
567
-
568
- function validVerificationIdentity(contract: Record<string, unknown>): boolean {
569
- if (
570
- !validIdentifier(contract.id) ||
571
- !["expected-red", "expected-green", "expected-refactor"].includes(
572
- String(contract.classification),
573
- )
574
- ) {
575
- return false;
576
- }
577
- return contract.classification === "expected-red"
578
- ? typeof contract.expectedFailure === "string" &&
579
- contract.expectedFailure.length > 0 &&
580
- contract.expectedFailure.length <= 512
581
- : contract.expectedFailure === undefined;
582
- }
583
-
584
- function validatePackageScriptRunner(
585
- value: unknown,
586
- ): value is PackageScriptRunner {
587
- if (
588
- !hasExactKeys(value, ["kind", "packageManager", "script", "command"]) ||
589
- value.kind !== "package-script" ||
590
- !["bun", "npm", "pnpm", "yarn"].includes(String(value.packageManager)) ||
591
- typeof value.script !== "string" ||
592
- !VERIFICATION_NAME.test(value.script) ||
593
- value.script.includes("/") ||
594
- !validScriptCommand(value.command)
595
- ) {
596
- return false;
597
- }
598
- return true;
599
- }
600
-
601
- function validateExecutableRunner(value: unknown): value is ExecutableRunner {
602
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
603
- const runner = value as Record<string, unknown>;
604
- if (runner.kind === "package-script") {
605
- return validatePackageScriptRunner(runner);
606
- }
607
- if (
608
- runner.kind === "local-binary" &&
609
- hasExactKeys(runner, ["kind", "executable"])
610
- ) {
611
- return (
612
- typeof runner.executable === "string" &&
613
- EXECUTABLE_NAME.test(runner.executable)
614
- );
615
- }
616
- return (
617
- runner.kind === "npx" &&
618
- hasExactKeys(runner, ["kind", "executable", "noInstall"]) &&
619
- typeof runner.executable === "string" &&
620
- EXECUTABLE_NAME.test(runner.executable) &&
621
- runner.noInstall === true
622
- );
623
- }
624
-
625
- function validateVitestRunner(value: ExecutableRunner): boolean {
626
- if (value.kind === "package-script") {
627
- if (UNSAFE_VERIFICATION_TOKEN.test(value.command)) return false;
628
- const tokens = value.command.split(/\s+/u);
629
- return (
630
- tokens[0] === "vitest" ||
631
- (tokens[0] === "npx" &&
632
- tokens[1] === "--no-install" &&
633
- tokens[2] === "vitest")
634
- );
635
- }
636
- return value.executable === "vitest";
637
- }
638
-
639
- function validateAtomicVerification(
640
- value: unknown,
641
- ): value is AtomicVerificationContract {
642
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
643
- const contract = value as Record<string, unknown>;
644
- if (
645
- contract.executionBindings !== undefined &&
646
- (!contract.executionBindings ||
647
- typeof contract.executionBindings !== "object" ||
648
- Array.isArray(contract.executionBindings) ||
649
- Object.entries(contract.executionBindings).some(
650
- ([key, hash]) =>
651
- !VERIFICATION_CONFIGURATION_PATHS.includes(key) ||
652
- (hash !== null &&
653
- (typeof hash !== "string" || !/^[a-f0-9]{64}$/u.test(hash))),
654
- ))
655
- )
656
- return false;
657
- const expectedFailure = [
658
- ...(contract.classification === "expected-red" ? ["expectedFailure"] : []),
659
- ...(contract.executionBindings === undefined ? [] : ["executionBindings"]),
660
- ];
661
- if (contract.kind === "vitest") {
662
- const executableRunner = contract.runner as ExecutableRunner;
663
- return (
664
- hasExactKeys(contract, [
665
- "kind",
666
- "id",
667
- "runner",
668
- "testFiles",
669
- "args",
670
- "classification",
671
- ...expectedFailure,
672
- "minTests",
673
- ]) &&
674
- validVerificationIdentity(contract) &&
675
- validateExecutableRunner(contract.runner) &&
676
- validateVitestRunner(executableRunner) &&
677
- Array.isArray(contract.testFiles) &&
678
- contract.testFiles.length >= VERIFICATION_LIMITS.minTestFiles &&
679
- contract.testFiles.length <= VERIFICATION_LIMITS.maxTestFiles &&
680
- contract.testFiles.every(isValidRelativePath) &&
681
- new Set(contract.testFiles).size === contract.testFiles.length &&
682
- validVerificationArgs(contract.args) &&
683
- Number.isSafeInteger(contract.minTests) &&
684
- (contract.minTests as number) >= 1
685
- );
686
- }
687
- if (contract.kind === "package-script") {
688
- const runner = {
689
- kind: "package-script",
690
- packageManager: contract.packageManager,
691
- script: contract.script,
692
- command: contract.command,
693
- };
694
- return (
695
- hasExactKeys(contract, [
696
- "kind",
697
- "id",
698
- "packageManager",
699
- "script",
700
- "command",
701
- "args",
702
- "classification",
703
- ...expectedFailure,
704
- ]) &&
705
- validVerificationIdentity(contract) &&
706
- validatePackageScriptRunner(runner) &&
707
- validVerificationArgs(contract.args)
708
- );
709
- }
710
- if (contract.kind !== "static-check") return false;
711
- const runner = contract.runner as Record<string, unknown> | undefined;
712
- const validRunner =
713
- validateExecutableRunner(runner) ||
714
- (hasExactKeys(runner, ["kind", "script"]) &&
715
- runner.kind === "node" &&
716
- isValidRelativePath(runner.script));
717
- return (
718
- hasExactKeys(contract, [
719
- "kind",
720
- "id",
721
- "runner",
722
- "args",
723
- "classification",
724
- ...expectedFailure,
725
- ]) &&
726
- validVerificationIdentity(contract) &&
727
- validRunner &&
728
- validVerificationArgs(contract.args)
729
- );
730
- }
731
-
732
- function validateStructuredVerification(
733
- value: unknown,
734
- ): value is StructuredVerificationContract {
735
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
736
- const contract = value as Record<string, unknown>;
737
- if (contract.kind !== "steps") {
738
- return validateAtomicVerification(contract);
739
- }
740
- if (
741
- !hasExactKeys(contract, ["kind", "id", "classification", "steps"]) ||
742
- !validIdentifier(contract.id) ||
743
- !["expected-red", "expected-green", "expected-refactor"].includes(
744
- String(contract.classification),
745
- ) ||
746
- !Array.isArray(contract.steps) ||
747
- contract.steps.length < VERIFICATION_LIMITS.minSteps ||
748
- contract.steps.length > VERIFICATION_LIMITS.maxSteps ||
749
- !contract.steps.every((step) => validateAtomicVerification(step))
750
- ) {
751
- return false;
752
- }
753
- const steps = contract.steps as AtomicVerificationContract[];
754
- return (
755
- steps
756
- .slice(0, -1)
757
- .every((step) => step.classification === "expected-green") &&
758
- steps.at(-1)?.classification === contract.classification &&
759
- new Set(steps.map((step) => step.id)).size === steps.length
760
- );
761
- }
762
-
763
- export function validateVerificationContract(
764
- value: unknown,
765
- ):
766
- | { ok: true; value: StructuredVerificationContract }
767
- | { ok: false; reason: string } {
768
- if (validateStructuredVerification(value)) {
769
- return { ok: true, value: structuredClone(value) };
770
- }
771
- return {
772
- ok: false,
773
- reason: "verification contract is unsupported by the Implement runtime",
774
- };
775
- }
776
-
777
- export function verificationSteps(
778
- verification: VerificationContract,
779
- ): AtomicVerificationContract[] {
780
- const normalized = validateVerificationContract(verification);
781
- if (!normalized.ok) return [];
782
- return normalized.value.kind === "steps"
783
- ? structuredClone(normalized.value.steps)
784
- : [structuredClone(normalized.value)];
785
- }
786
-
787
- export const VERIFICATION_CONFIGURATION_PATHS: readonly string[] = [
788
- "package.json",
789
- "package-lock.json",
790
- "npm-shrinkwrap.json",
791
- "bun.lock",
792
- "bun.lockb",
793
- "pnpm-lock.yaml",
794
- "yarn.lock",
795
- ".npmrc",
796
- ".yarnrc",
797
- ".yarnrc.yml",
798
- "bunfig.toml",
799
- "pnpm-workspace.yaml",
800
- ];
801
-
802
- export function verificationBoundInputPaths(
803
- verification: VerificationContract,
804
- ): string[] {
805
- return [
806
- ...new Set([
807
- ...verificationInputPaths(verification),
808
- ...verificationSteps(verification).flatMap((step) =>
809
- step.kind === "vitest" ||
810
- step.kind === "package-script" ||
811
- ("runner" in step && step.runner.kind === "package-script")
812
- ? VERIFICATION_CONFIGURATION_PATHS
813
- : [],
814
- ),
815
- ]),
816
- ];
817
- }
818
-
819
- export function verificationInputPaths(
820
- verification: VerificationContract,
821
- ): string[] {
822
- return [
823
- ...new Set(
824
- verificationSteps(verification).flatMap((step) => {
825
- const paths =
826
- step.kind === "vitest"
827
- ? [...step.testFiles]
828
- : step.kind === "static-check" && step.runner.kind === "node"
829
- ? [step.runner.script]
830
- : [];
831
- if (
832
- step.kind === "package-script" ||
833
- ("runner" in step && step.runner.kind === "package-script")
834
- ) {
835
- paths.push("package.json");
836
- }
837
- return paths;
838
- }),
839
- ),
840
- ];
841
- }
842
-
843
- export function cloneVerificationContract(
844
- verification: VerificationContract,
845
- ): StructuredVerificationContract {
846
- const normalized = validateVerificationContract(verification);
847
- if (!normalized.ok) throw new Error(normalized.reason);
848
- return structuredClone(normalized.value);
849
- }
850
-
851
- function validateApprovedDependencies(value: unknown): string | null {
852
- if (
853
- !Array.isArray(value) ||
854
- value.some(
855
- (dependency) =>
856
- typeof dependency !== "string" ||
857
- !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/iu.test(
858
- dependency,
859
- ),
860
- ) ||
861
- new Set(value).size !== value.length
862
- ) {
863
- return "invalid approved dependencies";
864
- }
865
- return null;
866
- }
867
-
868
- function validateImpactClosure(
869
- value: unknown,
870
- declared: Record<string, unknown>,
871
- snapshot: unknown,
872
- ): string | null {
873
- if (
874
- !hasExactKeys(value, [
875
- "changedSurfaces",
876
- "searchEvidence",
877
- "relatedTests",
878
- "affectedSuite",
879
- ])
880
- ) {
881
- return "invalid impact closure";
882
- }
883
- const closure = value as Record<string, unknown>;
884
- const surfaces = closure.changedSurfaces;
885
- const searchEvidence = closure.searchEvidence;
886
- const relatedTests = closure.relatedTests;
887
- const affectedSuite = closure.affectedSuite;
888
- if (
889
- !Array.isArray(surfaces) ||
890
- surfaces.length === 0 ||
891
- surfaces.some(
892
- (surface) => !(IMPACT_SURFACES as readonly unknown[]).includes(surface),
893
- ) ||
894
- new Set(surfaces).size !== surfaces.length ||
895
- (surfaces.includes("none") && surfaces.length !== 1) ||
896
- !Array.isArray(searchEvidence) ||
897
- searchEvidence.some(
898
- (evidence) => typeof evidence !== "string" || evidence.length === 0,
899
- ) ||
900
- !Array.isArray(relatedTests) ||
901
- !Array.isArray(affectedSuite) ||
902
- affectedSuite.some(
903
- (test) => !isValidRelativePath(test) || !isTestPath(test),
904
- ) ||
905
- new Set(affectedSuite).size !== affectedSuite.length
906
- ) {
907
- return "invalid impact closure";
908
- }
909
-
910
- const scope = new Set([
911
- ...((declared.read as string[]) ?? []),
912
- ...((declared.write as string[]) ?? []),
913
- ]);
914
- const writes = new Set((declared.write as string[]) ?? []);
915
- const relatedPaths = new Set<string>();
916
- for (const item of relatedTests) {
917
- if (!item || typeof item !== "object" || Array.isArray(item)) {
918
- return "invalid impact closure related test";
919
- }
920
- const related = item as Record<string, unknown>;
921
- const relatedKeys =
922
- related.disposition === "regression-task"
923
- ? ["path", "disposition", "evidence", "regressionTaskId"]
924
- : ["path", "disposition", "evidence"];
925
- if (
926
- !hasExactKeys(related, relatedKeys) ||
927
- !isValidRelativePath(related.path) ||
928
- !isTestPath(related.path) ||
929
- relatedPaths.has(related.path) ||
930
- !scope.has(related.path) ||
931
- !["current-task", "regression-task", "unaffected"].includes(
932
- String(related.disposition),
933
- ) ||
934
- typeof related.evidence !== "string" ||
935
- related.evidence.length === 0
936
- ) {
937
- return "invalid impact closure related test";
938
- }
939
- relatedPaths.add(related.path);
940
- if (related.disposition === "current-task" && !writes.has(related.path)) {
941
- return "current-task related test is outside the write set";
942
- }
943
- if (
944
- related.disposition === "regression-task" &&
945
- (typeof related.regressionTaskId !== "string" ||
946
- related.regressionTaskId.length === 0)
947
- ) {
948
- return "regression task identity is required";
949
- }
950
- if (
951
- related.disposition !== "regression-task" &&
952
- related.regressionTaskId !== undefined
953
- ) {
954
- return "unexpected regression task identity";
955
- }
956
- }
957
-
958
- if (surfaces[0] === "none") return null;
959
- if (
960
- searchEvidence.length === 0 ||
961
- relatedTests.length === 0 ||
962
- affectedSuite.length === 0 ||
963
- affectedSuite.some((test) => !relatedPaths.has(test))
964
- ) {
965
- return "public impact closure is incomplete";
966
- }
967
- const bounds = snapshot as Record<string, { kind?: unknown }> | undefined;
968
- if (
969
- bounds !== undefined &&
970
- !affectedSuite.some((test) => bounds[test]?.kind === "file")
971
- ) {
972
- return "affected suite contains no existing test evidence";
973
- }
974
- return null;
975
- }
976
-
977
- function isTestPath(path: string): boolean {
978
- return /^(?:test|tests)\//u.test(path);
979
- }
980
-
981
- function hasExactKeys(
982
- value: unknown,
983
- required: readonly string[],
984
- optional: readonly string[] = [],
985
- ): value is Record<string, unknown> {
986
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
987
- const record = value as Record<string, unknown>;
988
- const allowed = new Set([...required, ...optional]);
989
- return (
990
- required.every((key) => Object.hasOwn(record, key)) &&
991
- Object.keys(record).every((key) => allowed.has(key))
992
- );
993
- }
994
-
995
- function validIdentifier(value: unknown): value is string {
996
- return typeof value === "string" && IDENTIFIER.test(value);
997
- }
998
-
999
- function validatePathSet(value: unknown): value is string[] {
1000
- return (
1001
- Array.isArray(value) &&
1002
- value.every(isValidRelativePath) &&
1003
- new Set(value).size === value.length
1004
- );
1005
- }
1006
-
1007
- function validateIdentifierSet(value: unknown): value is string[] {
1008
- return (
1009
- Array.isArray(value) &&
1010
- value.every(validIdentifier) &&
1011
- new Set(value).size === value.length
1012
- );
1013
- }
1014
-
1015
- function validateVerificationInputBindings(
1016
- value: unknown,
1017
- ): value is VerificationInputBinding[] {
1018
- if (!Array.isArray(value)) return false;
1019
- const keys = new Set<string>();
1020
- for (const binding of value) {
1021
- if (
1022
- binding &&
1023
- typeof binding === "object" &&
1024
- !Array.isArray(binding) &&
1025
- (binding as { kind?: unknown }).kind === "workspace" &&
1026
- hasExactKeys(binding, ["kind", "path"]) &&
1027
- isValidRelativePath(binding.path)
1028
- ) {
1029
- if (keys.has(`workspace:${binding.path}`)) return false;
1030
- keys.add(`workspace:${binding.path}`);
1031
- continue;
1032
- }
1033
- if (
1034
- binding &&
1035
- typeof binding === "object" &&
1036
- !Array.isArray(binding) &&
1037
- (binding as { kind?: unknown }).kind === "output" &&
1038
- hasExactKeys(binding, ["kind", "outputId"]) &&
1039
- validIdentifier(binding.outputId)
1040
- ) {
1041
- if (keys.has(`output:${binding.outputId}`)) return false;
1042
- keys.add(`output:${binding.outputId}`);
1043
- continue;
1044
- }
1045
- return false;
1046
- }
1047
- return true;
1048
- }
1049
-
1050
- function validateRoots(value: unknown): value is string[] {
1051
- if (!validatePathSet(value) || value.length === 0) return false;
1052
- if (
1053
- value.some(
1054
- (root) => (root !== "." && root.startsWith("./")) || root.endsWith("/"),
1055
- )
1056
- ) {
1057
- return false;
1058
- }
1059
- return !value.some((root, index) =>
1060
- value.some(
1061
- (other, otherIndex) =>
1062
- index !== otherIndex && (root === "." || other.startsWith(`${root}/`)),
1063
- ),
1064
- );
1065
- }
1066
-
1067
- function isWithinRoots(path: string, roots: string[]): boolean {
1068
- return roots.some(
1069
- (root) => root === "." || path === root || path.startsWith(`${root}/`),
1070
- );
1071
- }
1072
-
1073
- function validatePhaseBoundary(
1074
- value: unknown,
1075
- phase: ImplementationPhase,
1076
- mode: unknown = "behavior",
1077
- ): value is PhaseBoundary {
1078
- if (
1079
- !hasExactKeys(
1080
- value,
1081
- ["read", "write", "delete", "verification", "verificationInputs"],
1082
- ["verificationLock"],
1083
- )
1084
- ) {
1085
- return false;
1086
- }
1087
- if (
1088
- !validatePathSet(value.read) ||
1089
- !validatePathSet(value.write) ||
1090
- !validatePathSet(value.delete) ||
1091
- !validateVerificationInputBindings(value.verificationInputs) ||
1092
- value.write.some(isAgentsPath) ||
1093
- value.delete.some(isAgentsPath) ||
1094
- value.write.some((path) => (value.delete as string[]).includes(path)) ||
1095
- (value.verificationLock !== undefined &&
1096
- !isValidRelativePath(value.verificationLock))
1097
- ) {
1098
- return false;
1099
- }
1100
- const verification = validateVerificationContract(value.verification);
1101
- if (!verification.ok) return false;
1102
- const classification = {
1103
- red:
1104
- mode !== undefined && mode !== "behavior"
1105
- ? "expected-green"
1106
- : "expected-red",
1107
- green: "expected-green",
1108
- refactor: "expected-refactor",
1109
- } as const;
1110
- return verification.value.classification === classification[phase];
1111
- }
1112
-
1113
- function validateTaskBoundary(
1114
- value: unknown,
1115
- snapshot: unknown,
1116
- ): value is TaskBoundary {
1117
- if (
1118
- !hasExactKeys(
1119
- value,
1120
- [
1121
- "changeId",
1122
- "taskId",
1123
- "dependsOn",
1124
- "objective",
1125
- "context",
1126
- "roots",
1127
- "phases",
1128
- "scheduling",
1129
- "agents",
1130
- "approvedDependencies",
1131
- "impactClosure",
1132
- ],
1133
- ["verificationMode"],
1134
- ) ||
1135
- (value.verificationMode !== undefined &&
1136
- !["behavior", "mechanical", "refactor"].includes(
1137
- String(value.verificationMode),
1138
- )) ||
1139
- !validIdentifier(value.changeId) ||
1140
- !validIdentifier(value.taskId) ||
1141
- !validateIdentifierSet(value.dependsOn) ||
1142
- typeof value.objective !== "string" ||
1143
- value.objective.length === 0 ||
1144
- value.objective.length > 4096 ||
1145
- !hasExactKeys(value.context, ["agents", "contract"]) ||
1146
- typeof value.context.agents !== "string" ||
1147
- typeof value.context.contract !== "string" ||
1148
- !validateRoots(value.roots) ||
1149
- !hasExactKeys(value.phases, ["red", "green"], ["refactor"]) ||
1150
- !validatePhaseBoundary(value.phases.red, "red", value.verificationMode) ||
1151
- !validatePhaseBoundary(value.phases.green, "green") ||
1152
- (value.phases.refactor !== undefined &&
1153
- !validatePhaseBoundary(value.phases.refactor, "refactor")) ||
1154
- !hasExactKeys(value.scheduling, ["conflicts", "resources"]) ||
1155
- !validatePathSet(value.scheduling.conflicts) ||
1156
- !validatePathSet(value.scheduling.resources) ||
1157
- !hasExactKeys(value.agents, ["impact", "managedOnly"], ["target"]) ||
1158
- !(AGENTS_IMPACTS as readonly unknown[]).includes(value.agents.impact) ||
1159
- value.agents.managedOnly !== true
1160
- ) {
1161
- return false;
1162
- }
1163
- if (
1164
- value.agents.impact === "none"
1165
- ? value.agents.target !== undefined
1166
- : !isAgentsPath(value.agents.target)
1167
- ) {
1168
- return false;
1169
- }
1170
- if (validateApprovedDependencies(value.approvedDependencies) !== null) {
1171
- return false;
1172
- }
1173
- const phases = [
1174
- value.phases.red,
1175
- value.phases.green,
1176
- ...(value.phases.refactor ? [value.phases.refactor] : []),
1177
- ];
1178
- const roots = value.roots as string[];
1179
- if (
1180
- phases.some((phase) =>
1181
- [
1182
- ...phase.read,
1183
- ...phase.write,
1184
- ...phase.delete,
1185
- ...verificationInputPaths(phase.verification),
1186
- ].some((path) => !isWithinRoots(path, roots)),
1187
- )
1188
- ) {
1189
- return false;
1190
- }
1191
- if (
1192
- phases.some((phase) => {
1193
- if (phase.verification.kind === undefined) return false;
1194
- const declared = new Set([...phase.read, ...phase.write]);
1195
- return verificationInputPaths(phase.verification).some(
1196
- (input) => !declared.has(input),
1197
- );
1198
- })
1199
- ) {
1200
- return false;
1201
- }
1202
- const declared = {
1203
- read: [...new Set(phases.flatMap((entry) => entry.read))],
1204
- write: [
1205
- ...new Set(phases.flatMap((entry) => [...entry.write, ...entry.delete])),
1206
- ],
1207
- };
1208
- return (
1209
- validateImpactClosure(value.impactClosure, declared, snapshot) === null
1210
- );
1211
- }
1212
-
1213
- export type ImplementGraphContractDiagnostic =
1214
- | {
1215
- code: "invalid-implement-graph";
1216
- taskId?: string;
1217
- phase?: ImplementationPhase;
1218
- field?: string;
1219
- category?: string;
1220
- outputId?: string;
1221
- path?: string;
1222
- expectedPaths?: string[];
1223
- actualPaths?: string[];
1224
- allowedValues?: string[];
1225
- }
1226
- | {
1227
- code: "invalid-output-path";
1228
- outputId?: string;
1229
- field: "outputs.path";
1230
- category: "path";
1231
- };
1232
-
1233
- function phaseBoundaryContractDiagnostic(
1234
- value: unknown,
1235
- phase: ImplementationPhase,
1236
- mode: unknown = "behavior",
1237
- ): Omit<
1238
- Extract<
1239
- ImplementGraphContractDiagnostic,
1240
- { code: "invalid-implement-graph" }
1241
- >,
1242
- "code"
1243
- > | null {
1244
- const prefix = `phases.${phase}`;
1245
- if (
1246
- !hasExactKeys(
1247
- value,
1248
- ["read", "write", "delete", "verification", "verificationInputs"],
1249
- ["verificationLock"],
1250
- )
1251
- ) {
1252
- return { phase, field: prefix, category: "shape" };
1253
- }
1254
- if (!validatePathSet(value.read)) {
1255
- return { phase, field: `${prefix}.read`, category: "path-set" };
1256
- }
1257
- if (!validatePathSet(value.write) || value.write.some(isAgentsPath)) {
1258
- return { phase, field: `${prefix}.write`, category: "path-set" };
1259
- }
1260
- if (!validatePathSet(value.delete) || value.delete.some(isAgentsPath)) {
1261
- return { phase, field: `${prefix}.delete`, category: "path-set" };
1262
- }
1263
- const deletions = value.delete as string[];
1264
- if (value.write.some((candidate) => deletions.includes(candidate))) {
1265
- return { phase, field: prefix, category: "write-delete-overlap" };
1266
- }
1267
- if (!validateVerificationInputBindings(value.verificationInputs)) {
1268
- return {
1269
- phase,
1270
- field: `${prefix}.verificationInputs`,
1271
- category: "verification-input-binding",
1272
- };
1273
- }
1274
- if (
1275
- value.verificationLock !== undefined &&
1276
- !isValidRelativePath(value.verificationLock)
1277
- ) {
1278
- return {
1279
- phase,
1280
- field: `${prefix}.verificationLock`,
1281
- category: "path",
1282
- };
1283
- }
1284
- const verification = validateVerificationContract(value.verification);
1285
- if (!verification.ok) {
1286
- return {
1287
- phase,
1288
- field: `${prefix}.verification`,
1289
- category: "verification-contract",
1290
- };
1291
- }
1292
- const classification = {
1293
- red:
1294
- mode !== undefined && mode !== "behavior"
1295
- ? "expected-green"
1296
- : "expected-red",
1297
- green: "expected-green",
1298
- refactor: "expected-refactor",
1299
- } as const;
1300
- if (verification.value.classification !== classification[phase]) {
1301
- return {
1302
- phase,
1303
- field: `${prefix}.verification.classification`,
1304
- category: "verification-classification",
1305
- };
1306
- }
1307
- return null;
1308
- }
1309
-
1310
- function impactClosureCategory(reason: string): string {
1311
- const categories: Record<string, string> = {
1312
- "invalid impact closure": "shape",
1313
- "invalid impact closure related test": "related-test",
1314
- "current-task related test is outside the write set":
1315
- "current-task-outside-write-set",
1316
- "regression task identity is required": "regression-task-identity",
1317
- "unexpected regression task identity": "regression-task-identity",
1318
- "public impact closure is incomplete": "public-impact-incomplete",
1319
- "affected suite contains no existing test evidence":
1320
- "existing-test-evidence",
1321
- };
1322
- return categories[reason] ?? "contract";
1323
- }
1324
-
1325
- function taskBoundaryContractDiagnostic(
1326
- value: Record<string, unknown>,
1327
- snapshot: unknown,
1328
- ): Omit<
1329
- Extract<
1330
- ImplementGraphContractDiagnostic,
1331
- { code: "invalid-implement-graph" }
1332
- >,
1333
- "code"
1334
- > | null {
1335
- const taskId = validIdentifier(value.taskId) ? value.taskId : undefined;
1336
- const diagnostic = (
1337
- field: string,
1338
- category: string,
1339
- phase?: ImplementationPhase,
1340
- ) => ({
1341
- ...(taskId ? { taskId } : {}),
1342
- ...(phase ? { phase } : {}),
1343
- field,
1344
- category,
1345
- });
1346
- if (
1347
- !hasExactKeys(
1348
- value,
1349
- [
1350
- "changeId",
1351
- "taskId",
1352
- "dependsOn",
1353
- "objective",
1354
- "context",
1355
- "roots",
1356
- "phases",
1357
- "scheduling",
1358
- "agents",
1359
- "approvedDependencies",
1360
- "impactClosure",
1361
- ],
1362
- ["verificationMode"],
1363
- )
1364
- ) {
1365
- return diagnostic("task", "shape");
1366
- }
1367
- if (
1368
- value.verificationMode !== undefined &&
1369
- !["behavior", "mechanical", "refactor"].includes(
1370
- String(value.verificationMode),
1371
- )
1372
- )
1373
- return diagnostic("verificationMode", "shape");
1374
- if (!validIdentifier(value.changeId)) {
1375
- return diagnostic("changeId", "identifier");
1376
- }
1377
- if (!validIdentifier(value.taskId)) {
1378
- return diagnostic("taskId", "identifier");
1379
- }
1380
- if (!validateIdentifierSet(value.dependsOn)) {
1381
- return diagnostic("dependsOn", "identifier-set");
1382
- }
1383
- if (
1384
- typeof value.objective !== "string" ||
1385
- value.objective.length === 0 ||
1386
- value.objective.length > 4096
1387
- ) {
1388
- return diagnostic("objective", "text-boundary");
1389
- }
1390
- if (
1391
- !hasExactKeys(value.context, ["agents", "contract"]) ||
1392
- typeof value.context.agents !== "string" ||
1393
- typeof value.context.contract !== "string"
1394
- ) {
1395
- return diagnostic("context", "shape");
1396
- }
1397
- if (!validateRoots(value.roots)) {
1398
- return diagnostic("roots", "path-set");
1399
- }
1400
- if (!hasExactKeys(value.phases, ["red", "green"], ["refactor"])) {
1401
- return diagnostic("phases", "shape");
1402
- }
1403
- for (const phase of IMPLEMENTATION_PHASES) {
1404
- const boundary = value.phases[phase];
1405
- if (phase === "refactor" && boundary === undefined) continue;
1406
- const phaseDiagnostic = phaseBoundaryContractDiagnostic(
1407
- boundary,
1408
- phase,
1409
- value.verificationMode,
1410
- );
1411
- if (phaseDiagnostic)
1412
- return { ...diagnostic("phases", "contract"), ...phaseDiagnostic };
1413
- }
1414
- if (
1415
- !hasExactKeys(value.scheduling, ["conflicts", "resources"]) ||
1416
- !validatePathSet(value.scheduling.conflicts) ||
1417
- !validatePathSet(value.scheduling.resources)
1418
- ) {
1419
- return diagnostic("scheduling", "path-set");
1420
- }
1421
- if (
1422
- !hasExactKeys(value.agents, ["impact", "managedOnly"], ["target"]) ||
1423
- !(AGENTS_IMPACTS as readonly unknown[]).includes(value.agents.impact) ||
1424
- value.agents.managedOnly !== true ||
1425
- (value.agents.impact === "none"
1426
- ? value.agents.target !== undefined
1427
- : !isAgentsPath(value.agents.target))
1428
- ) {
1429
- return diagnostic("agents", "contract");
1430
- }
1431
- if (validateApprovedDependencies(value.approvedDependencies) !== null) {
1432
- return diagnostic("approvedDependencies", "dependency-set");
1433
- }
1434
- const roots = value.roots as string[];
1435
- for (const phase of IMPLEMENTATION_PHASES) {
1436
- const boundary = value.phases[phase] as PhaseBoundary | undefined;
1437
- if (!boundary) continue;
1438
- for (const [field, candidates] of [
1439
- ["read", boundary.read],
1440
- ["write", boundary.write],
1441
- ["delete", boundary.delete],
1442
- ["verification", verificationInputPaths(boundary.verification)],
1443
- ] as const) {
1444
- if (candidates.some((candidate) => !isWithinRoots(candidate, roots))) {
1445
- return diagnostic(`phases.${phase}.${field}`, "outside-roots", phase);
1446
- }
1447
- }
1448
- if (boundary.verification.kind !== undefined) {
1449
- const declared = new Set([...boundary.read, ...boundary.write]);
1450
- if (
1451
- verificationInputPaths(boundary.verification).some(
1452
- (input) => !declared.has(input),
1453
- )
1454
- ) {
1455
- return diagnostic(
1456
- `phases.${phase}.verification`,
1457
- "verification-input-not-declared",
1458
- phase,
1459
- );
1460
- }
1461
- }
1462
- }
1463
- const taskPhases = value.phases as Record<
1464
- ImplementationPhase,
1465
- PhaseBoundary | undefined
1466
- >;
1467
- const phases = IMPLEMENTATION_PHASES.flatMap((phase) => {
1468
- const boundary = taskPhases[phase];
1469
- return boundary ? [boundary] : [];
1470
- });
1471
- const declared = {
1472
- read: [...new Set(phases.flatMap((entry) => entry.read))],
1473
- write: [
1474
- ...new Set(phases.flatMap((entry) => [...entry.write, ...entry.delete])),
1475
- ],
1476
- };
1477
- const impactReason = validateImpactClosure(
1478
- value.impactClosure,
1479
- declared,
1480
- snapshot,
1481
- );
1482
- if (impactReason !== null) {
1483
- if (impactReason === "current-task related test is outside the write set") {
1484
- const related = (value.impactClosure as ImpactClosureContract)
1485
- .relatedTests;
1486
- const index = related.findIndex(
1487
- (test) =>
1488
- test.disposition === "current-task" &&
1489
- !declared.write.includes(test.path),
1490
- );
1491
- const test = related[index];
1492
- if (test)
1493
- return {
1494
- ...diagnostic(
1495
- `impactClosure.relatedTests.${index}.disposition`,
1496
- "current-task-outside-write-set",
1497
- ),
1498
- path: test.path,
1499
- expectedPaths: [...declared.write].sort(),
1500
- actualPaths: [test.path],
1501
- };
1502
- }
1503
- return diagnostic("impactClosure", impactClosureCategory(impactReason));
1504
- }
1505
- return validateTaskBoundary(value, snapshot)
1506
- ? null
1507
- : diagnostic("task", "contract");
1508
- }
1509
-
1510
- /** Refine rejected author input; this never admits a partial or repaired task. */
1511
- function taskFieldDiagnostics(
1512
- task: Record<string, unknown>,
1513
- ): ImplementGraphContractDiagnostic[] {
1514
- const diagnostics: ImplementGraphContractDiagnostic[] = [];
1515
- const add = (
1516
- detail: Omit<
1517
- Extract<
1518
- ImplementGraphContractDiagnostic,
1519
- { code: "invalid-implement-graph" }
1520
- >,
1521
- "code"
1522
- >,
1523
- ) => {
1524
- if (diagnostics.length < 64)
1525
- diagnostics.push({
1526
- code: "invalid-implement-graph",
1527
- ...(validIdentifier(task.taskId) ? { taskId: task.taskId } : {}),
1528
- ...detail,
1529
- });
1530
- };
1531
- if (task.phases && typeof task.phases === "object") {
1532
- const phases = task.phases as Record<string, unknown>;
1533
- for (const phase of IMPLEMENTATION_PHASES) {
1534
- const boundary = phases[phase];
1535
- if (!boundary || typeof boundary !== "object") continue;
1536
- for (const field of ["read", "write", "delete"] as const) {
1537
- const paths = (boundary as Record<string, unknown>)[field];
1538
- if (!Array.isArray(paths)) continue;
1539
- const seen = new Set<string>();
1540
- for (const [index, file] of paths.entries()) {
1541
- if (!isValidRelativePath(file)) continue;
1542
- if (seen.has(file))
1543
- add({
1544
- phase,
1545
- field: `phases.${phase}.${field}.${index}`,
1546
- category: "duplicate-path",
1547
- path: file,
1548
- });
1549
- seen.add(file);
1550
- }
1551
- }
1552
- }
1553
- }
1554
- if (task.impactClosure && typeof task.impactClosure === "object") {
1555
- const closure = task.impactClosure as Record<string, unknown>;
1556
- if (Array.isArray(closure.changedSurfaces)) {
1557
- for (const [index, surface] of closure.changedSurfaces.entries()) {
1558
- if (!(IMPACT_SURFACES as readonly unknown[]).includes(surface))
1559
- add({
1560
- field: `impactClosure.changedSurfaces.${index}`,
1561
- category: "enum",
1562
- allowedValues: [...IMPACT_SURFACES],
1563
- });
1564
- }
1565
- const publicImpact = closure.changedSurfaces.some(
1566
- (surface) =>
1567
- surface !== "none" &&
1568
- (IMPACT_SURFACES as readonly unknown[]).includes(surface),
1569
- );
1570
- if (publicImpact) {
1571
- for (const [field, category] of [
1572
- ["searchEvidence", "search-evidence-required"],
1573
- ["relatedTests", "related-tests-required"],
1574
- ["affectedSuite", "affected-suite-required"],
1575
- ] as const) {
1576
- if (Array.isArray(closure[field]) && closure[field].length === 0)
1577
- add({ field: `impactClosure.${field}`, category });
1578
- }
1579
- }
1580
- if (
1581
- publicImpact &&
1582
- Array.isArray(closure.affectedSuite) &&
1583
- Array.isArray(closure.relatedTests)
1584
- ) {
1585
- const related = new Set(
1586
- closure.relatedTests.flatMap((item) =>
1587
- item && typeof item === "object" ? [item.path] : [],
1588
- ),
1589
- );
1590
- for (const [index, file] of closure.affectedSuite.entries())
1591
- if (isValidRelativePath(file) && !related.has(file))
1592
- add({
1593
- field: `impactClosure.affectedSuite.${index}`,
1594
- category: "related-test-missing",
1595
- path: file,
1596
- });
1597
- }
1598
- }
1599
- }
1600
- return diagnostics;
1601
- }
1602
-
1603
- export function validateImplementGraphBoundary(value: unknown):
1604
- | { ok: true; value: ImplementGraphBoundary }
1605
- | {
1606
- ok: false;
1607
- reason: string;
1608
- diagnostic: ImplementGraphContractDiagnostic;
1609
- diagnostics?: ImplementGraphContractDiagnostic[];
1610
- } {
1611
- if (
1612
- !hasExactKeys(value, ["changeId", "tasks", "outputs"]) ||
1613
- !validIdentifier(value.changeId) ||
1614
- !Array.isArray(value.tasks) ||
1615
- value.tasks.length === 0 ||
1616
- value.tasks.length > 128 ||
1617
- !Array.isArray(value.outputs) ||
1618
- value.outputs.length > 512
1619
- )
1620
- return {
1621
- ok: false,
1622
- reason: "invalid Implement graph boundary",
1623
- diagnostic: { code: "invalid-implement-graph" },
1624
- };
1625
-
1626
- // Collect independent task/output failures without admitting a partial graph.
1627
- const failures: Array<{
1628
- reason: string;
1629
- diagnostic: ImplementGraphContractDiagnostic;
1630
- }> = [];
1631
- for (const candidate of value.tasks) {
1632
- if (
1633
- !candidate ||
1634
- typeof candidate !== "object" ||
1635
- Array.isArray(candidate)
1636
- ) {
1637
- failures.push({
1638
- reason: "invalid Implement task boundary",
1639
- diagnostic: {
1640
- code: "invalid-implement-graph",
1641
- field: "tasks",
1642
- category: "shape",
1643
- },
1644
- });
1645
- continue;
1646
- }
1647
- const task = candidate as Record<string, unknown>;
1648
- const diagnostic = Object.hasOwn(task, "changeId")
1649
- ? {
1650
- ...(validIdentifier(task.taskId) ? { taskId: task.taskId } : {}),
1651
- field: "changeId",
1652
- category: "unexpected-field",
1653
- }
1654
- : taskBoundaryContractDiagnostic(
1655
- { changeId: value.changeId, ...task },
1656
- undefined,
1657
- );
1658
- if (diagnostic) {
1659
- const fields = taskFieldDiagnostics(task);
1660
- // Replace a broad diagnostic only when these details refine that field;
1661
- // retain unrelated shape/authority failures in the same batch.
1662
- if (
1663
- !fields.some(
1664
- (detail) =>
1665
- diagnostic.field &&
1666
- detail.field?.startsWith(`${diagnostic.field}.`),
1667
- )
1668
- )
1669
- fields.push({ code: "invalid-implement-graph", ...diagnostic });
1670
- for (const detail of fields)
1671
- failures.push({
1672
- reason: "invalid Implement task boundary",
1673
- diagnostic: detail,
1674
- });
1675
- }
1676
- }
1677
- for (const candidate of value.outputs) {
1678
- if (
1679
- !candidate ||
1680
- typeof candidate !== "object" ||
1681
- Array.isArray(candidate)
1682
- ) {
1683
- failures.push({
1684
- reason: "invalid Implement graph output",
1685
- diagnostic: { code: "invalid-implement-graph" },
1686
- });
1687
- continue;
1688
- }
1689
- const output = candidate as Record<string, unknown>;
1690
- if (!isValidRelativePath(output.path)) {
1691
- failures.push({
1692
- reason: "invalid Implement graph output path",
1693
- diagnostic: {
1694
- code: "invalid-output-path",
1695
- ...(validIdentifier(output.id) ? { outputId: output.id } : {}),
1696
- field: "outputs.path",
1697
- category: "path",
1698
- },
1699
- });
1700
- continue;
1701
- }
1702
- if (
1703
- !hasExactKeys(output, ["id", "path", "producer", "postcondition"]) ||
1704
- !validIdentifier(output.id) ||
1705
- !hasExactKeys(output.producer, ["taskId", "phase"]) ||
1706
- !validIdentifier(output.producer.taskId) ||
1707
- !(IMPLEMENTATION_PHASES as readonly unknown[]).includes(
1708
- output.producer.phase,
1709
- ) ||
1710
- output.postcondition !== "regular-file"
1711
- ) {
1712
- failures.push({
1713
- reason: "invalid Implement graph output",
1714
- diagnostic: {
1715
- code: "invalid-implement-graph",
1716
- ...(validIdentifier(output.id) ? { outputId: output.id } : {}),
1717
- field: "outputs",
1718
- category: "shape",
1719
- },
1720
- });
1721
- }
1722
- }
1723
- const first = failures[0];
1724
- if (first)
1725
- return {
1726
- ok: false,
1727
- ...first,
1728
- ...(failures.length > 1
1729
- ? { diagnostics: failures.map((failure) => failure.diagnostic) }
1730
- : {}),
1731
- };
1732
- return {
1733
- ok: true,
1734
- value: structuredClone(value) as unknown as ImplementGraphBoundary,
1735
- };
1736
- }
1737
-
1738
- export function validatePacketEnvelope(
1739
- value: unknown,
1740
- ): { ok: true; value: PacketEnvelope } | { ok: false; reason: string } {
1741
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1742
- return { ok: false, reason: "missing request envelope" };
1743
- }
1744
- const packet = value as Record<string, unknown>;
1745
- const baseRequired = [
1746
- "stage",
1747
- "role",
1748
- "id",
1749
- "phase",
1750
- "objective",
1751
- "roots",
1752
- "context",
1753
- "declared",
1754
- "output",
1755
- ] as const;
1756
- const required =
1757
- packet.stage === "abel-design"
1758
- ? ([
1759
- ...baseRequired.slice(0, 2),
1760
- "runId",
1761
- ...baseRequired.slice(2),
1762
- ] as const)
1763
- : baseRequired;
1764
- for (const field of required) {
1765
- if (packet[field] === undefined) {
1766
- return { ok: false, reason: `missing required field: ${field}` };
1767
- }
1768
- }
1769
- if (!hasExactKeys(packet, required)) {
1770
- return { ok: false, reason: "invalid packet fields" };
1771
- }
1772
-
1773
- if (packet.stage !== "abel-design" && packet.stage !== "abel-diagnose") {
1774
- return {
1775
- ok: false,
1776
- reason: `unsupported packet stage: ${String(packet.stage)}`,
1777
- };
1778
- }
1779
- if (
1780
- (packet.stage === "abel-design" && packet.role !== "design-explorer") ||
1781
- (packet.stage === "abel-diagnose" && packet.role !== "diagnosis-worker")
1782
- ) {
1783
- return { ok: false, reason: "packet role does not match stage" };
1784
- }
1785
- if (
1786
- (packet.stage === "abel-design" && !validIdentifier(packet.runId)) ||
1787
- (packet.stage === "abel-diagnose" && packet.runId !== undefined)
1788
- ) {
1789
- return { ok: false, reason: "invalid Design run identity" };
1790
- }
1791
- if (!validIdentifier(packet.id)) {
1792
- return { ok: false, reason: "invalid request id" };
1793
- }
1794
- if (
1795
- typeof packet.phase !== "string" ||
1796
- !(PHASES as readonly string[]).includes(packet.phase)
1797
- ) {
1798
- return { ok: false, reason: `invalid phase: ${String(packet.phase)}` };
1799
- }
1800
- if (
1801
- typeof packet.objective !== "string" ||
1802
- packet.objective.length === 0 ||
1803
- packet.objective.length > 4096
1804
- ) {
1805
- return { ok: false, reason: "invalid objective" };
1806
- }
1807
- let serialized: string;
1808
- try {
1809
- serialized = JSON.stringify(packet);
1810
- } catch {
1811
- return { ok: false, reason: "request envelope is not serializable" };
1812
- }
1813
- if (serialized.length > LIMITS.maxEnvelopeBytes) {
1814
- return {
1815
- ok: false,
1816
- reason: `request envelope exceeds ${LIMITS.maxEnvelopeBytes / 1024} KiB`,
1817
- };
1818
- }
1819
- if (
1820
- !Array.isArray(packet.roots) ||
1821
- packet.roots.length === 0 ||
1822
- !packet.roots.every(isValidRelativePath)
1823
- ) {
1824
- return { ok: false, reason: "invalid path roots" };
1825
- }
1826
- if (
1827
- !hasExactKeys(packet.context, ["agents", "contract"]) ||
1828
- typeof packet.context.agents !== "string" ||
1829
- typeof packet.context.contract !== "string"
1830
- ) {
1831
- return { ok: false, reason: "invalid context" };
1832
- }
1833
- if (
1834
- !hasExactKeys(
1835
- packet.declared,
1836
- ["read", "write", "conflicts", "resources"],
1837
- ["verificationLock"],
1838
- )
1839
- ) {
1840
- return { ok: false, reason: "invalid declared sets" };
1841
- }
1842
- for (const key of ["read", "write", "conflicts", "resources"] as const) {
1843
- const entries = packet.declared[key];
1844
- if (
1845
- !Array.isArray(entries) ||
1846
- !entries.every(isValidRelativePath) ||
1847
- new Set(entries).size !== entries.length
1848
- ) {
1849
- return { ok: false, reason: `invalid declared ${key} set` };
1850
- }
1851
- }
1852
- if ((packet.declared.write as unknown[]).some(isAgentsPath)) {
1853
- return { ok: false, reason: "subagents cannot declare AGENTS writes" };
1854
- }
1855
- if (
1856
- packet.declared.verificationLock !== undefined &&
1857
- !isValidRelativePath(packet.declared.verificationLock)
1858
- ) {
1859
- return { ok: false, reason: "invalid verification lock" };
1860
- }
1861
- if (!(OUTPUT_KINDS as readonly unknown[]).includes(packet.output)) {
1862
- return {
1863
- ok: false,
1864
- reason: `invalid output kind: ${String(packet.output)}`,
1865
- };
1866
- }
1867
- if (
1868
- packet.stage === "abel-design" &&
1869
- (packet.phase !== "evidence" ||
1870
- packet.output !== "evidence" ||
1871
- (packet.declared.write as unknown[]).length !== 0)
1872
- ) {
1873
- return { ok: false, reason: "invalid Design packet contract" };
1874
- }
1875
- if (
1876
- packet.stage === "abel-diagnose" &&
1877
- (packet.output !== "evidence" ||
1878
- (packet.declared.write as unknown[]).length !== 0)
1879
- ) {
1880
- return {
1881
- ok: false,
1882
- reason: "invalid Diagnose evidence-only packet contract",
1883
- };
1884
- }
1885
-
1886
- return {
1887
- ok: true,
1888
- value: structuredClone(packet) as unknown as PacketEnvelope,
1889
- };
1890
- }
1891
-
1892
- /** Extract proposed write paths from ordinary unified diff header pairs. */
1893
- export function diffWritePaths(diffText: string): {
1894
- paths: string[];
1895
- kind: string;
1896
- } {
1897
- if (typeof diffText !== "string" || diffText.length === 0) {
1898
- throw new Error("empty diff");
1899
- }
1900
- const hasFinalLf = diffText.endsWith("\n");
1901
- const lines = (hasFinalLf ? diffText.slice(0, -1) : diffText).split("\n");
1902
-
1903
- for (const line of lines) {
1904
- if (/^(?:GIT binary patch|Binary files )/.test(line)) {
1905
- throw new Error("binary diff is not supported");
1906
- }
1907
- if (/^(?:copy|rename) (?:from|to) /.test(line)) {
1908
- throw new Error(`${line.split(" ", 1)[0]} records are not supported`);
1909
- }
1910
- if (/^(?:old|new) mode /.test(line)) {
1911
- throw new Error("mode transitions are not supported");
1912
- }
1913
- const mode = /^(?:new file mode|deleted file mode) (.+)$/.exec(line);
1914
- if (mode) requireRegularFileMode(mode[1]);
1915
- }
1916
-
1917
- const paths: string[] = [];
1918
- let i = 0;
1919
- while (i < lines.length) {
1920
- if (!isFileSectionStart(lines[i])) {
1921
- if (paths.length === 0) {
1922
- throw new Error("diff has no ordinary text headers");
1923
- }
1924
- throw new Error(`unconsumed diff content: ${lines[i]}`);
1925
- }
1926
-
1927
- let gitTarget: string | null = null;
1928
- if (lines[i].startsWith("diff --git ")) {
1929
- gitTarget = parseGitTarget(lines[i]);
1930
- i++;
1931
- }
1932
-
1933
- let operation: "add" | "delete" | null = null;
1934
- const mode = /^(new file mode|deleted file mode) (.+)$/.exec(
1935
- lines[i] ?? "",
1936
- );
1937
- if (mode) {
1938
- operation = mode[1] === "new file mode" ? "add" : "delete";
1939
- i++;
1940
- }
1941
- if (lines[i]?.startsWith("index ")) {
1942
- parseIndexMetadata(lines[i]);
1943
- i++;
1944
- }
1945
-
1946
- if (!lines[i]?.startsWith("--- ")) {
1947
- throw new Error("file section is missing an ordinary --- header");
1948
- }
1949
- const oldPath = parseHeaderPath(lines[i].slice(4), "a");
1950
- i++;
1951
- if (!lines[i]?.startsWith("+++ ")) {
1952
- throw new Error("diff header pair is malformed");
1953
- }
1954
- const newPath = parseHeaderPath(lines[i].slice(4), "b");
1955
- i++;
1956
-
1957
- if (oldPath === null && newPath === null) {
1958
- throw new Error("diff header pair has no target");
1959
- }
1960
- if (oldPath !== null && newPath !== null && oldPath !== newPath) {
1961
- throw new Error("mismatched old/new diff targets");
1962
- }
1963
- const target = oldPath ?? newPath;
1964
- if (target === null) throw new Error("diff header pair has no target");
1965
- if (gitTarget !== null && gitTarget !== target) {
1966
- throw new Error("mismatched diff --git and text targets");
1967
- }
1968
- if (
1969
- (operation === "add" && oldPath !== null) ||
1970
- (operation === "delete" && newPath !== null)
1971
- ) {
1972
- throw new Error("create/delete metadata does not match text headers");
1973
- }
1974
- if (paths.includes(target)) throw new Error("duplicate diff targets");
1975
- paths.push(target);
1976
-
1977
- let sawHunk = false;
1978
- while (lines[i]?.startsWith("@@")) {
1979
- const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/.exec(
1980
- lines[i],
1981
- );
1982
- if (!hunk) throw new Error(`invalid hunk header: ${lines[i]}`);
1983
- const range = [
1984
- Number(hunk[1]),
1985
- Number(hunk[2] ?? "1"),
1986
- Number(hunk[3]),
1987
- Number(hunk[4] ?? "1"),
1988
- ];
1989
- if (!range.every(Number.isSafeInteger)) {
1990
- throw new Error("hunk range exceeds supported integer bounds");
1991
- }
1992
- let oldRemaining = range[1];
1993
- let newRemaining = range[3];
1994
- sawHunk = true;
1995
- i++;
1996
-
1997
- while (oldRemaining > 0 || newRemaining > 0) {
1998
- if (i >= lines.length) throw new Error("hunk body underflow");
1999
- const body = lines[i];
2000
- switch (body[0]) {
2001
- case " ":
2002
- if (oldRemaining === 0 || newRemaining === 0) {
2003
- throw new Error("hunk body overflow");
2004
- }
2005
- oldRemaining--;
2006
- newRemaining--;
2007
- break;
2008
- case "-":
2009
- if (oldRemaining === 0) throw new Error("hunk body overflow");
2010
- oldRemaining--;
2011
- break;
2012
- case "+":
2013
- if (newRemaining === 0) throw new Error("hunk body overflow");
2014
- newRemaining--;
2015
- break;
2016
- default:
2017
- throw new Error("hunk body underflow");
2018
- }
2019
- i++;
2020
- if (lines[i] === "\") i++;
2021
- }
2022
- }
2023
- if (!sawHunk) {
2024
- throw new Error("file section has no valid unified-diff hunk");
2025
- }
2026
- }
2027
-
2028
- if (!hasFinalLf) throw new Error("diff must end with LF");
2029
- return { paths, kind: "text" };
2030
- }
2031
-
2032
- function isFileSectionStart(line: string | undefined): boolean {
2033
- return (
2034
- line?.startsWith("diff --git ") === true ||
2035
- line?.startsWith("new file mode ") === true ||
2036
- line?.startsWith("deleted file mode ") === true ||
2037
- line?.startsWith("index ") === true ||
2038
- line?.startsWith("--- ") === true
2039
- );
2040
- }
2041
-
2042
- function parseGitTarget(line: string): string {
2043
- const prefix = "diff --git a/";
2044
- if (!line.startsWith(prefix)) throw new Error("malformed diff --git header");
2045
- const rest = line.slice(prefix.length);
2046
- for (
2047
- let separator = rest.indexOf(" b/");
2048
- separator >= 0;
2049
- separator = rest.indexOf(" b/", separator + 1)
2050
- ) {
2051
- const oldPath = rest.slice(0, separator);
2052
- const newPath = rest.slice(separator + 3);
2053
- if (oldPath === newPath) return requireCanonicalDiffPath(oldPath);
2054
- }
2055
- throw new Error("mismatched diff --git targets");
2056
- }
2057
-
2058
- function parseIndexMetadata(line: string): void {
2059
- const index = /^index [0-9a-f]+\.\.[0-9a-f]+(?: ([0-7]{6}))?$/.exec(line);
2060
- if (!index) throw new Error("malformed index metadata");
2061
- if (index[1]) requireRegularFileMode(index[1]);
2062
- }
2063
-
2064
- function requireRegularFileMode(mode: string): void {
2065
- if (mode === "160000") {
2066
- throw new Error("submodule mode is not supported: 160000");
2067
- }
2068
- if (mode !== "100644" && mode !== "100755") {
2069
- throw new Error(`non-regular file mode is not supported: ${mode}`);
2070
- }
2071
- }
2072
-
2073
- function parseHeaderPath(header: string, prefix: "a" | "b"): string | null {
2074
- if (header === "/dev/null") return null;
2075
- const other = prefix === "a" ? "b" : "a";
2076
- if (header.startsWith(`${other}/`)) {
2077
- throw new Error("mismatched old/new diff header prefixes");
2078
- }
2079
- const path = header.startsWith(`${prefix}/`) ? header.slice(2) : header;
2080
- return requireCanonicalDiffPath(path);
2081
- }
2082
-
2083
- function requireCanonicalDiffPath(path: string): string {
2084
- if (
2085
- !isValidRelativePath(path) ||
2086
- path.trim() !== path ||
2087
- /[\r\n\t]/.test(path)
2088
- ) {
2089
- throw new Error(`noncanonical or escaping diff path: ${path}`);
2090
- }
2091
- return path;
2092
- }
2093
-
2094
- export interface CompactEvidenceResult {
2095
- id: string;
2096
- role: string;
2097
- kind: "evidence";
2098
- conclusions: string[];
2099
- citations: { path: string; lines: string }[];
2100
- constraints: string[];
2101
- dependencies: string[];
2102
- risks: string[];
2103
- blockingQuestions: string[];
2104
- hints: { writeSet: string[]; verification: string; agentsImpact: string };
2105
- }
2106
-
2107
- export interface DesignEvidenceResult {
2108
- id: string;
2109
- role: "design-explorer";
2110
- kind: "evidence";
2111
- packet_id: string;
2112
- module_name: string;
2113
- scope: string[];
2114
- files_read: string[];
2115
- evidence: Array<{
2116
- claim: string;
2117
- path: string;
2118
- line_start: number;
2119
- line_end: number;
2120
- }>;
2121
- existing_structures: string[];
2122
- existing_conventions: string[];
2123
- constraints_discovered: string[];
2124
- open_questions: string[];
2125
- dependencies: string[];
2126
- write_set_hints: string[];
2127
- validation_hints: string[];
2128
- agents_impact_hints: string[];
2129
- risks: string[];
2130
- success_criteria_hints: string[];
2131
- }
2132
-
2133
- export type EvidenceResult = CompactEvidenceResult | DesignEvidenceResult;
2134
-
2135
- export interface DiffResult {
2136
- id: string;
2137
- role: string;
2138
- kind: "diff";
2139
- taskId: string;
2140
- phase: string;
2141
- summary: string;
2142
- diff: string;
2143
- expectedVerification: string;
2144
- risks: string[];
2145
- contractCompliant: boolean;
2146
- }
2147
-
2148
- // Diagnostics contain only code-owned field names, never unexpected keys or values.
2149
- function evidenceFieldsFailure(
2150
- value: unknown,
2151
- fields: readonly string[],
2152
- location: string,
2153
- ): { ok: false; reason: string } {
2154
- if (!value || typeof value !== "object" || Array.isArray(value))
2155
- return { ok: false, reason: `${location}: expected an object` };
2156
- const missing = fields.filter((field) => !Object.hasOwn(value, field));
2157
- return {
2158
- ok: false,
2159
- reason:
2160
- missing.length > 0
2161
- ? `${location}: missing fields ${missing.join(", ")}`
2162
- : `${location}: unexpected fields; allowed fields ${fields.join(", ")}`,
2163
- };
2164
- }
2165
-
2166
- export function validateEvidenceResult(value: unknown): {
2167
- ok: boolean;
2168
- reason?: string;
2169
- failure?: CandidateFailure;
2170
- } {
2171
- if (value === null || typeof value !== "object")
2172
- return { ok: false, reason: "missing result" };
2173
- const r = value as Record<string, unknown>;
2174
- if (r.kind !== "evidence") return { ok: false, reason: "wrong result kind" };
2175
- if (r.role === "design-explorer") return validateDesignEvidenceResult(r);
2176
- const fields = [
2177
- "id",
2178
- "role",
2179
- "kind",
2180
- "conclusions",
2181
- "citations",
2182
- "constraints",
2183
- "dependencies",
2184
- "risks",
2185
- "blockingQuestions",
2186
- "hints",
2187
- ] as const;
2188
- if (!hasExactKeys(r, fields)) {
2189
- return evidenceFieldsFailure(r, fields, "result");
2190
- }
2191
- if (!validIdentifier(r.id) || typeof r.role !== "string")
2192
- return { ok: false, reason: "invalid identity" };
2193
- if (!(ROLES as readonly string[]).includes(r.role))
2194
- return { ok: false, reason: "unknown role" };
2195
- for (const field of [
2196
- "conclusions",
2197
- "constraints",
2198
- "dependencies",
2199
- "risks",
2200
- "blockingQuestions",
2201
- ] as const) {
2202
- if (
2203
- !Array.isArray(r[field]) ||
2204
- !r[field].every((entry) => typeof entry === "string")
2205
- ) {
2206
- return { ok: false, reason: `invalid evidence field: ${field}` };
2207
- }
2208
- }
2209
- if (!Array.isArray(r.citations))
2210
- return { ok: false, reason: "citations: expected an array" };
2211
- for (const [index, citation] of r.citations.entries()) {
2212
- const field = `citations[${index}]`;
2213
- if (!hasExactKeys(citation, ["path", "lines"]))
2214
- return evidenceFieldsFailure(citation, ["path", "lines"], field);
2215
- if (!isValidRelativePath(citation.path))
2216
- return {
2217
- ok: false,
2218
- reason: `${field}.path: expected a canonical relative path`,
2219
- };
2220
- if (typeof citation.lines !== "string")
2221
- return { ok: false, reason: `${field}.lines: expected a string` };
2222
- }
2223
- if (!hasExactKeys(r.hints, ["writeSet", "verification", "agentsImpact"]))
2224
- return evidenceFieldsFailure(
2225
- r.hints,
2226
- ["writeSet", "verification", "agentsImpact"],
2227
- "hints",
2228
- );
2229
- if (!validatePathSet(r.hints.writeSet))
2230
- return {
2231
- ok: false,
2232
- reason: "hints.writeSet: expected unique canonical relative paths",
2233
- };
2234
- if (typeof r.hints.verification !== "string")
2235
- return { ok: false, reason: "hints.verification: expected a string" };
2236
- if (!(AGENTS_IMPACTS as readonly unknown[]).includes(r.hints.agentsImpact))
2237
- return {
2238
- ok: false,
2239
- reason: `hints.agentsImpact: expected one of ${AGENTS_IMPACTS.join(", ")}`,
2240
- };
2241
- const serialized = JSON.stringify(r);
2242
- if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
2243
- return {
2244
- ok: false,
2245
- reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
2246
- failure: {
2247
- kind: "result-limit",
2248
- limitBytes: LIMITS.maxCompleteResultBytes,
2249
- },
2250
- };
2251
- }
2252
- return { ok: true };
2253
- }
2254
-
2255
- function validateDesignEvidenceResult(r: Record<string, unknown>): {
2256
- ok: boolean;
2257
- reason?: string;
2258
- failure?: CandidateFailure;
2259
- } {
2260
- const fields = [
2261
- "id",
2262
- "role",
2263
- "kind",
2264
- "packet_id",
2265
- "module_name",
2266
- "scope",
2267
- "files_read",
2268
- "evidence",
2269
- "existing_structures",
2270
- "existing_conventions",
2271
- "constraints_discovered",
2272
- "open_questions",
2273
- "dependencies",
2274
- "write_set_hints",
2275
- "validation_hints",
2276
- "agents_impact_hints",
2277
- "risks",
2278
- "success_criteria_hints",
2279
- ] as const;
2280
- if (!hasExactKeys(r, fields)) {
2281
- return evidenceFieldsFailure(r, fields, "result");
2282
- }
2283
- if (!validIdentifier(r.id))
2284
- return { ok: false, reason: "id: expected a valid identifier" };
2285
- if (r.packet_id !== r.id)
2286
- return { ok: false, reason: "packet_id: must equal id" };
2287
- if (!isValidRelativePath(r.module_name))
2288
- return {
2289
- ok: false,
2290
- reason: "module_name: expected a canonical relative path or module slug",
2291
- };
2292
- if (!validatePathSet(r.scope) || r.scope.length === 0) {
2293
- return {
2294
- ok: false,
2295
- reason:
2296
- "scope: expected a nonempty set of unique canonical relative paths",
2297
- };
2298
- }
2299
- for (const field of ["files_read", "write_set_hints"] as const) {
2300
- if (!validatePathSet(r[field]))
2301
- return {
2302
- ok: false,
2303
- reason: `${field}: expected unique canonical relative paths`,
2304
- };
2305
- }
2306
- if (!Array.isArray(r.evidence))
2307
- return { ok: false, reason: "evidence: expected an array" };
2308
- for (const [index, entry] of r.evidence.entries()) {
2309
- const field = `evidence[${index}]`;
2310
- if (!hasExactKeys(entry, ["claim", "path", "line_start", "line_end"])) {
2311
- return evidenceFieldsFailure(
2312
- entry,
2313
- ["claim", "path", "line_start", "line_end"],
2314
- field,
2315
- );
2316
- }
2317
- if (typeof entry.claim !== "string" || entry.claim.length === 0)
2318
- return { ok: false, reason: `${field}.claim: expected nonempty text` };
2319
- if (!isValidRelativePath(entry.path))
2320
- return {
2321
- ok: false,
2322
- reason: `${field}.path: expected a canonical relative path`,
2323
- };
2324
- if (
2325
- !Number.isSafeInteger(entry.line_start) ||
2326
- (entry.line_start as number) < 1
2327
- )
2328
- return {
2329
- ok: false,
2330
- reason: `${field}.line_start: expected a positive safe integer`,
2331
- };
2332
- if (
2333
- !Number.isSafeInteger(entry.line_end) ||
2334
- (entry.line_end as number) < (entry.line_start as number)
2335
- )
2336
- return {
2337
- ok: false,
2338
- reason: `${field}.line_end: expected a safe integer >= line_start`,
2339
- };
2340
- }
2341
- for (const field of [
2342
- "existing_structures",
2343
- "existing_conventions",
2344
- "constraints_discovered",
2345
- "open_questions",
2346
- "dependencies",
2347
- "validation_hints",
2348
- "agents_impact_hints",
2349
- "risks",
2350
- "success_criteria_hints",
2351
- ] as const) {
2352
- if (
2353
- !Array.isArray(r[field]) ||
2354
- !r[field].every((entry) => typeof entry === "string")
2355
- ) {
2356
- return {
2357
- ok: false,
2358
- reason: `invalid Design evidence field: ${field}`,
2359
- };
2360
- }
2361
- }
2362
- const serialized = JSON.stringify(r);
2363
- if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
2364
- return {
2365
- ok: false,
2366
- reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
2367
- failure: {
2368
- kind: "result-limit",
2369
- limitBytes: LIMITS.maxCompleteResultBytes,
2370
- },
2371
- };
2372
- }
2373
- return { ok: true };
2374
- }
2375
-
2376
- export function validateDiffResult(value: unknown): {
2377
- ok: boolean;
2378
- reason?: string;
2379
- paths?: string[];
2380
- failure?: CandidateFailure;
2381
- } {
2382
- if (value === null || typeof value !== "object")
2383
- return { ok: false, reason: "missing result" };
2384
- const r = value as Record<string, unknown>;
2385
- if (r.kind !== "diff") return { ok: false, reason: "wrong result kind" };
2386
- const fields = [
2387
- "id",
2388
- "role",
2389
- "kind",
2390
- "taskId",
2391
- "phase",
2392
- "summary",
2393
- "diff",
2394
- "expectedVerification",
2395
- "risks",
2396
- "contractCompliant",
2397
- ] as const;
2398
- if (!hasExactKeys(r, fields)) {
2399
- return { ok: false, reason: "invalid diff result fields" };
2400
- }
2401
- for (const field of fields) {
2402
- if (r[field] === undefined)
2403
- return { ok: false, reason: `missing field: ${field}` };
2404
- }
2405
- if (
2406
- typeof r.id !== "string" ||
2407
- r.id.length === 0 ||
2408
- r.id.length > 128 ||
2409
- typeof r.role !== "string" ||
2410
- r.role.length === 0 ||
2411
- typeof r.taskId !== "string" ||
2412
- r.taskId.length === 0
2413
- ) {
2414
- return { ok: false, reason: "invalid identity" };
2415
- }
2416
- if (!(ROLES as readonly string[]).includes(r.role))
2417
- return { ok: false, reason: "unknown role" };
2418
- if (
2419
- typeof r.phase !== "string" ||
2420
- !(PHASES as readonly string[]).includes(r.phase)
2421
- ) {
2422
- return { ok: false, reason: "invalid phase" };
2423
- }
2424
- if (
2425
- typeof r.summary !== "string" ||
2426
- r.summary.length === 0 ||
2427
- typeof r.expectedVerification !== "string" ||
2428
- r.expectedVerification.length === 0
2429
- ) {
2430
- return { ok: false, reason: "invalid diff result text" };
2431
- }
2432
- if (
2433
- !Array.isArray(r.risks) ||
2434
- !(r.risks as unknown[]).every((x) => typeof x === "string")
2435
- ) {
2436
- return { ok: false, reason: "invalid risks" };
2437
- }
2438
- if (r.contractCompliant !== true)
2439
- return { ok: false, reason: "contract compliance not affirmed" };
2440
- let paths: string[];
2441
- try {
2442
- paths = diffWritePaths(r.diff as string).paths;
2443
- } catch (err) {
2444
- return {
2445
- ok: false,
2446
- reason: `invalid diff: ${(err as Error).message}`,
2447
- failure: {
2448
- kind: "artifact",
2449
- code: "invalid-diff",
2450
- stage: "candidate-diff",
2451
- },
2452
- };
2453
- }
2454
- const serialized = JSON.stringify(r);
2455
- if (Buffer.byteLength(serialized, "utf8") > LIMITS.maxCompleteResultBytes) {
2456
- return {
2457
- ok: false,
2458
- reason: `result exceeds ${LIMITS.maxCompleteResultBytes} bytes`,
2459
- failure: {
2460
- kind: "result-limit",
2461
- limitBytes: LIMITS.maxCompleteResultBytes,
2462
- },
2463
- };
2464
- }
2465
- return { ok: true, paths };
2466
- }