@tea-agent/loop-agent 0.43.0 → 0.44.0-next.10

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 (226) hide show
  1. package/CHANGELOG.md +408 -367
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -208,3 +208,24 @@ export function formatBudgetReportMarkdown(ledger) {
208
208
  }
209
209
  return `${lines.join("\n")}\n`;
210
210
  }
211
+ export const frontendObservedSessionIdentitySchema = z.object({
212
+ sourceExecutionId: z.string().uuid().optional(),
213
+ sessionId: z.string().min(1), phase: z.string().min(1), executionKind: z.enum(["model", "runtime"]), dispatchReason: z.enum(["initial", "correction"]),
214
+ scopeIds: z.array(z.string()), taskId: z.string().nullable(), runId: z.string().nullable(), nodeId: z.string().nullable(), attempt: z.number().int().nullable(),
215
+ sourceDigest: z.string().nullable(), contractDigest: z.string().nullable(),
216
+ }).strict();
217
+ /** Durable task request authority and the source-bound snapshot carried by a DAG. */
218
+ export const sourceProviderBudgetSchema = z.object({
219
+ schemaVersion: z.literal(1), taskId: z.string().min(1), budgetId: z.string().uuid(),
220
+ maxProviderRequests: z.number().int().positive(), reservedRequests: z.number().int().nonnegative(),
221
+ requestSessions: z.record(z.object({ identity: frontendObservedSessionIdentitySchema, reservedRequests: z.number().int().positive() }).strict()).optional(),
222
+ }).strict().refine(value => value.reservedRequests <= value.maxProviderRequests && Object.entries(value.requestSessions ?? {}).every(([key, session]) => key === `${session.identity.runId ?? "task"}:${session.identity.sessionId}` && session.identity.executionKind === "model" && session.identity.taskId === value.taskId) && Object.values(value.requestSessions ?? {}).reduce((sum, session) => sum + session.reservedRequests, 0) <= value.reservedRequests);
223
+ export const sourceProviderBudgetBindingSchema = z.object({
224
+ ledger: sourceProviderBudgetSchema,
225
+ contract: z.object({ revision: z.number().int().nonnegative(), canonicalHash: z.string().regex(/^[a-f0-9]{64}$/) }).strict().optional(),
226
+ sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/),
227
+ evidence: z.object({ executions: z.array(z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), content: z.string() }).strict()).optional(), receipts: z.array(z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), content: z.string() }).strict()), diagnostics: z.array(z.string()) }).strict().optional(),
228
+ }).strict();
229
+ export function sourceBudgetContractIdentity(binding) {
230
+ return binding ? { revision: binding.revision, canonicalHash: binding.canonicalHash } : undefined;
231
+ }
@@ -9,24 +9,19 @@ function normalizeTask(task) {
9
9
  featureId: task.featureId ?? null,
10
10
  };
11
11
  }
12
- /**
13
- * Canonical payload for corpusHash.
14
- * Excludes corpusId, createdAt, description, corpusHash, and free-text notes.
15
- */
12
+ /** Excludes display metadata, but binds every frontend input and oracle ref. */
16
13
  export function buildCanonicalCorpusPayload(manifest) {
17
- const tasks = manifest.tasks
18
- .map((task) => normalizeTask(task))
19
- .sort((a, b) => (a.taskRef < b.taskRef ? -1 : a.taskRef > b.taskRef ? 1 : 0));
20
- return {
21
- schemaVersion: 1,
22
- tasks,
23
- };
14
+ const files = (refs) => refs.map(ref => ({ path: ref.path, sha256: ref.sha256 })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
15
+ const tasks = (manifest.schemaVersion === 2
16
+ ? manifest.tasks.map(task => ({ ...normalizeTask(task), category: task.category,
17
+ requirements: files(task.requirements), initialFiles: files(task.initialFiles), oracleFiles: files(task.oracleFiles) }))
18
+ : manifest.tasks.map(normalizeTask))
19
+ .sort((a, b) => a.taskRef < b.taskRef ? -1 : a.taskRef > b.taskRef ? 1 : 0);
20
+ return { schemaVersion: manifest.schemaVersion,
21
+ ...(manifest.schemaVersion === 2 ? { purpose: manifest.purpose } : {}), tasks };
24
22
  }
25
23
  export function computeCorpusHash(manifest) {
26
- const payload = buildCanonicalCorpusPayload(manifest);
27
- const digest = createHash("sha256")
28
- .update(stableStringify(payload))
29
- .digest("hex");
24
+ const digest = createHash("sha256").update(stableStringify(buildCanonicalCorpusPayload(manifest))).digest("hex");
30
25
  return formatContentSha(digest);
31
26
  }
32
27
  export function assertCorpusHashMatches(manifest) {
@@ -1,7 +1,8 @@
1
- import { loadCorpusManifestFromPath, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
1
+ import { loadCorpusManifestFromPath, assertCorpusArtifacts, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
2
2
  export async function validateCorpus(input) {
3
3
  const raw = await loadCorpusManifestFromPath(input.repoRoot, input.manifestPath);
4
4
  const manifest = await materializeCorpusManifest(raw);
5
+ await assertCorpusArtifacts(input.repoRoot, manifest);
5
6
  return { manifest, summary: summarizeCorpus(manifest) };
6
7
  }
7
8
  export async function registerCorpus(input) {
@@ -0,0 +1,77 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { execFileSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
7
+ const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
8
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
9
+ const refSchema = z.object({ path: z.string().min(1), sha256: digest }).strict();
10
+ const files = ["index.html", "src/app.js", "test/counter.test.js", "acceptance-prd.md"];
11
+ const requiredChecks = ["initial", "idle", "limit", "reset", "loading", "empty", "real-integration"];
12
+ const additionalChecks = ["noPageErrors", "preservesQuery", "loadedState", "disabledWhileLoading", "replacesOldItems", "errorState", "errorClearsOldItems"];
13
+ const checksSchema = (keys) => z.object(Object.fromEntries(keys.map(key => [key, z.boolean()]))).strict();
14
+ /** Adapter only for the frozen counter corpus and independent browser script. */
15
+ export async function readFrontendBrowserAcceptance(input) {
16
+ const observed = [];
17
+ const read = async (root, name, expected) => {
18
+ const target = path.resolve(root, name);
19
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: target, label: "frontend browser acceptance" });
20
+ if (expected && sha(bytes) !== expected)
21
+ throw Error(`browser evidence hash mismatch: ${name}`);
22
+ observed.push({ root, target, bytes });
23
+ return bytes;
24
+ };
25
+ const readRef = (value) => { const ref = refSchema.parse(value); return read(input.repoRoot, ref.path, ref.sha256); };
26
+ const contract = z.object({ schemaVersion: z.literal(1), kind: z.literal("counter-browser-acceptance-v1"), checker: refSchema, oracle: refSchema }).strict().parse(JSON.parse((await readRef(input.contract)).toString("utf8")));
27
+ await readRef(contract.checker);
28
+ await readRef(contract.oracle);
29
+ const raw = JSON.parse((await readRef(input.report)).toString("utf8"));
30
+ const header = z.object({ schemaVersion: z.literal(1), evidenceKind: z.literal("actual-task-artifact-browser"), runId: z.string().min(1), runStatus: z.enum(["finished", "failed", "partial_failed"]), stateSha256: digest, checkpointSha256: digest, checkerSha256: digest, frozenOracleSha256: digest,
31
+ sourceHashes: z.object(Object.fromEntries(files.map(file => [file, digest]))).strict(), pass: z.boolean() }).passthrough().parse(raw);
32
+ if (header.checkerSha256 !== contract.checker.sha256 || header.frozenOracleSha256 !== contract.oracle.sha256)
33
+ throw Error("browser checker not registered");
34
+ const state = JSON.parse((await read(input.runDir, "state.json", header.stateSha256)).toString("utf8"));
35
+ if (typeof state.cwd !== "string" || await realpath(state.cwd) !== await realpath(input.repoRoot))
36
+ throw Error("browser workspace mismatch");
37
+ if (state.runId !== header.runId || state.status !== header.runStatus || !state.finishedAt)
38
+ throw Error("browser run identity mismatch");
39
+ const checkpoint = z.object({ gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/), changedPaths: z.array(z.object({ path: z.string(), contentSha256: digest.optional() }).passthrough()) }).passthrough().parse(JSON.parse((await read(input.runDir, ".runtime/workspace-checkpoint-terminal.json", header.checkpointSha256)).toString("utf8")));
40
+ for (const name of files) {
41
+ const changed = checkpoint.changedPaths.filter(row => row.path === name);
42
+ if (changed.length > 1)
43
+ throw Error("ambiguous browser checkpoint path");
44
+ const expected = changed.length ? changed[0].contentSha256 : sha(execFileSync("git", ["show", `${checkpoint.gitHeadSha}:${name}`], { cwd: input.repoRoot }));
45
+ if (header.sourceHashes[name] !== expected)
46
+ throw Error(`browser artifact not bound to terminal checkpoint: ${name}`);
47
+ }
48
+ let passed;
49
+ const diagnostics = ["counter-browser-only-product-gates-pending"];
50
+ if (typeof raw.error === "string") {
51
+ if (raw.result !== undefined || raw.additional !== undefined)
52
+ throw Error("browser execution error conflicts with completed checks");
53
+ if (header.pass)
54
+ throw Error("browser error cannot pass");
55
+ passed = null;
56
+ diagnostics.push("browser-execution-error");
57
+ }
58
+ else {
59
+ const result = z.object({ checks: checksSchema(requiredChecks), failedChecks: z.array(z.string()) }).passthrough().parse(raw.result);
60
+ const additional = checksSchema(additionalChecks).parse(raw.additional);
61
+ const errors = z.array(z.string()).parse(raw.errors);
62
+ const failed = Object.entries(result.checks).filter(([, value]) => !value).map(([key]) => key).sort();
63
+ if (JSON.stringify(failed) !== JSON.stringify([...result.failedChecks].sort()))
64
+ throw Error("browser failed-check list mismatch");
65
+ passed = failed.length === 0 && Object.values(additional).every(Boolean) && errors.length === 0;
66
+ if (header.pass !== passed)
67
+ throw Error("browser summary contradicts checks");
68
+ if (passed && state.status !== "finished")
69
+ throw Error("failed run cannot become successful browser delivery");
70
+ }
71
+ for (const entry of observed) {
72
+ const latest = await readFileBoundarySafe({ boundaryRoot: entry.root, targetPath: entry.target, label: "browser evidence recheck" });
73
+ if (!latest.equals(entry.bytes))
74
+ throw Error("browser evidence changed during collection");
75
+ }
76
+ return { passed, diagnostics, runId: header.runId, reportSha256: input.report.sha256, contractSha256: input.contract.sha256 };
77
+ }
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
7
+ import { dagEvaluationBindingSchema } from "../../workflows/dag/types.js";
8
+ import { verifyControllerIdentityForResume } from "../../workflows/dag/controller-identity.js";
9
+ import { assertFrontendPairExecution } from "./frontend-pair-registration.js";
10
+ import { corpusManifestSchema } from "./types.js";
11
+ import { assertCorpusHashMatches } from "./corpus-hash.js";
12
+ const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
13
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
14
+ const ref = z.object({ path: z.string().min(1), sha256: digest }).strict();
15
+ const contractSchema = z.object({ schemaVersion: z.literal(1), kind: z.literal("frontend-corpus-browser-acceptance-v1"), corpus: ref, checker: ref, oracle: ref, coverage: ref }).strict();
16
+ const observationSchema = z.object({
17
+ schemaVersion: z.literal(1), evidenceKind: z.literal("unbound-corpus-browser-observation"), runId: z.null(), caseId: z.string(),
18
+ browser: z.string().nullable(), pass: z.boolean(), checks: z.record(z.boolean()), failedChecks: z.array(z.string()),
19
+ pageErrors: z.array(z.string()), resourceErrors: z.array(z.string()), servedFiles: z.record(digest), checkerSha256: digest,
20
+ infrastructureError: z.string().optional(),
21
+ }).passthrough();
22
+ const reportSchema = z.object({
23
+ schemaVersion: z.literal(1), evidenceKind: z.literal("actual-corpus-task-browser"), caseId: z.string(), runId: z.string().min(1),
24
+ runStatus: z.enum(["finished", "failed", "partial_failed"]), taskRef: z.string(), cellId: z.string(), artifactHashes: z.record(digest),
25
+ stateSha256: digest, runSha256: digest, checkpointSha256: digest, registration: ref, contractSha256: digest, corpusSha256: digest,
26
+ checkerSha256: digest, oracleSha256: digest, coverageSha256: digest, pass: z.boolean(), allChecksPresent: z.boolean(), observation: observationSchema,
27
+ }).strict();
28
+ const webAsset = (name) => name === "index.html" || ["src/", "data/", "public/", "assets/"].some(prefix => name.startsWith(prefix));
29
+ function safeName(name) {
30
+ if (!name || name.includes("\\") || name.includes(":") || name.includes("\0") || name.startsWith("/") || name.split("/").some(part => !part || part === "." || part === ".."))
31
+ throw Error("invalid corpus browser artifact path");
32
+ return name;
33
+ }
34
+ /** Validates browser facts for a registered run. Success here does not prove
35
+ * source fidelity, writer boundaries, Node-test quality, or model execution. */
36
+ export async function readFrontendCorpusBrowserAcceptance(input) {
37
+ const observed = [];
38
+ const read = async (root, name, expected) => {
39
+ const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: path.resolve(root, name), label: "corpus browser evidence" });
40
+ if (expected !== undefined && sha(bytes) !== expected)
41
+ throw Error(`corpus browser evidence hash mismatch: ${name}`);
42
+ observed.push({ root, name, bytes });
43
+ return bytes;
44
+ };
45
+ const readRef = (value) => { const parsed = ref.parse(value); return read(input.repoRoot, parsed.path, parsed.sha256); };
46
+ const report = reportSchema.parse(JSON.parse((await readRef(input.report)).toString("utf8")));
47
+ const contract = contractSchema.parse(JSON.parse((await readRef(input.contract)).toString("utf8")));
48
+ if (report.contractSha256 !== input.contract.sha256 || report.corpusSha256 !== contract.corpus.sha256 ||
49
+ report.checkerSha256 !== contract.checker.sha256 || report.oracleSha256 !== contract.oracle.sha256 ||
50
+ report.coverageSha256 !== contract.coverage.sha256 || report.observation.checkerSha256 !== contract.oracle.sha256)
51
+ throw Error("corpus browser checker identity mismatch");
52
+ await readRef(contract.checker);
53
+ await readRef(contract.oracle);
54
+ const corpus = corpusManifestSchema.parse(JSON.parse((await readRef(contract.corpus)).toString("utf8")));
55
+ if (corpus.schemaVersion !== 2)
56
+ throw Error("corpus browser requires frontend corpus");
57
+ assertCorpusHashMatches(corpus);
58
+ for (const task of corpus.tasks)
59
+ for (const value of [...task.requirements, ...task.initialFiles, ...task.oracleFiles])
60
+ await readRef(value);
61
+ const coverage = z.object({ schemaVersion: z.literal(1), cases: z.record(z.record(z.array(z.string().min(1)).min(1))) }).passthrough()
62
+ .parse(JSON.parse((await readRef(contract.coverage)).toString("utf8")));
63
+ const state = JSON.parse((await read(input.runDir, "state.json", report.stateSha256)).toString("utf8"));
64
+ const spec = JSON.parse((await read(input.runDir, "run.json", report.runSha256)).toString("utf8"));
65
+ if (state.evidenceKind?.includes("selftest"))
66
+ throw Error("selftest cannot be corpus run evidence");
67
+ if (typeof state.cwd !== "string" || await realpath(state.cwd) !== await realpath(input.repoRoot))
68
+ throw Error("corpus browser workspace mismatch");
69
+ if (state.runId !== report.runId || state.status !== report.runStatus || !Number.isFinite(Date.parse(state.startedAt)) ||
70
+ !state.finishedAt || !Number.isFinite(Date.parse(state.finishedAt)) || Date.parse(state.finishedAt) < Date.parse(state.startedAt))
71
+ throw Error("corpus browser terminal identity mismatch");
72
+ const binding = dagEvaluationBindingSchema.parse(state.evaluation);
73
+ if (!binding.frontendPair || JSON.stringify(binding) !== JSON.stringify(spec.evaluation) || binding.taskRef !== report.taskRef ||
74
+ binding.frontendPair.cellId !== report.cellId || binding.frontendPair.registration.path !== report.registration.path ||
75
+ binding.frontendPair.registration.sha256 !== report.registration.sha256)
76
+ throw Error("corpus browser frozen evaluation mismatch");
77
+ const controller = JSON.parse((await read(input.runDir, "controller-identity.json")).toString("utf8"));
78
+ if (state.controllerIdentityRef?.path !== "controller-identity.json" || controller.status !== "pinned" || !controller.identity)
79
+ throw Error("corpus browser controller not pinned");
80
+ const pinned = await verifyControllerIdentityForResume({ runDir: input.runDir, state, currentIdentity: controller.identity });
81
+ const registration = await assertFrontendPairExecution({ repoRoot: input.repoRoot, binding, controllerFingerprint: pinned.identity.packageFingerprint.value, startedAt: state.startedAt });
82
+ if (registration.shared.acceptance !== input.contract.sha256)
83
+ throw Error("corpus browser acceptance contract not registered");
84
+ await readRef(report.registration);
85
+ const tasks = corpus.tasks.filter(task => task.taskRef === binding.taskRef);
86
+ if (tasks.length !== 1)
87
+ throw Error("corpus browser task missing");
88
+ const task = tasks[0];
89
+ if (task.category !== report.caseId || task.category !== report.observation.caseId || task.split !== binding.split || !task.seeds.includes(binding.seed))
90
+ throw Error("corpus browser case mismatch");
91
+ for (const key of ["checker", "oracle", "coverage"])
92
+ if (!task.oracleFiles.some(value => value.path === contract[key].path && value.sha256 === contract[key].sha256))
93
+ throw Error("corpus browser checker not owned by task");
94
+ const caseCoverage = coverage.cases[task.category];
95
+ if (!caseCoverage || !Object.keys(caseCoverage).length)
96
+ throw Error("corpus browser coverage missing");
97
+ const required = new Set([...Object.values(caseCoverage).flat(), "no-page-errors", "bound-resources"]);
98
+ const observation = report.observation;
99
+ if (Object.keys(observation.checks).some(name => !required.has(name) && name !== "interaction-completed"))
100
+ throw Error("unknown corpus browser check");
101
+ if (observation.checks["interaction-completed"] === true)
102
+ throw Error("invalid corpus interaction diagnostic");
103
+ // The producer records infrastructure errors before case interactions. It
104
+ // cannot relabel completed business checks as an environment-only failure.
105
+ if (observation.infrastructureError !== undefined && Object.keys(observation.checks).some(name => name !== "bound-resources"))
106
+ throw Error("corpus browser infrastructure phase contradicts checks");
107
+ const allPresent = [...required].every(name => Object.hasOwn(observation.checks, name));
108
+ if (allPresent !== report.allChecksPresent)
109
+ throw Error("corpus browser check coverage mismatch");
110
+ const failed = Object.entries(observation.checks).filter(([, passed]) => !passed).map(([name]) => name).sort();
111
+ if (JSON.stringify(failed) !== JSON.stringify([...observation.failedChecks].sort()))
112
+ throw Error("corpus browser failed-check list mismatch");
113
+ for (const [name, errors] of [["no-page-errors", observation.pageErrors], ["bound-resources", observation.resourceErrors]])
114
+ if (Object.hasOwn(observation.checks, name) && observation.checks[name] !== (errors.length === 0))
115
+ throw Error("corpus browser error flags mismatch");
116
+ const observedPass = observation.infrastructureError === undefined && Object.keys(observation.checks).length > 0 && failed.length === 0;
117
+ if (observation.pass !== observedPass || report.pass !== (state.status === "finished" && observedPass && allPresent))
118
+ throw Error("corpus browser summary contradicts checks");
119
+ const checkpoint = z.object({ schemaVersion: z.literal(1), gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/),
120
+ changedPaths: z.array(z.object({ path: z.string(), state: z.string(), contentSha256: digest.optional(), mode: z.string().optional() }).passthrough()) }).passthrough()
121
+ .parse(JSON.parse((await read(input.runDir, ".runtime/workspace-checkpoint-terminal.json", report.checkpointSha256)).toString("utf8")));
122
+ const changes = new Map();
123
+ for (const row of checkpoint.changedPaths) {
124
+ safeName(row.path);
125
+ if (changes.has(row.path))
126
+ throw Error("duplicate corpus checkpoint path");
127
+ changes.set(row.path, row);
128
+ }
129
+ const entries = new Map();
130
+ const tree = execFileSync("git", ["ls-tree", "-rz", "--full-tree", checkpoint.gitHeadSha], { cwd: input.repoRoot, maxBuffer: 16 * 1024 * 1024 }).toString("utf8");
131
+ for (const item of tree.split("\0").filter(Boolean)) {
132
+ const tab = item.indexOf("\t"), name = item.slice(tab + 1), [mode, type] = item.slice(0, tab).split(" ");
133
+ if (webAsset(name))
134
+ entries.set(safeName(name), { mode, type: type });
135
+ }
136
+ for (const [name, row] of changes)
137
+ if (webAsset(name)) {
138
+ if (row.state === "deleted")
139
+ entries.delete(name);
140
+ else
141
+ entries.set(name, { mode: row.mode, type: "blob" });
142
+ }
143
+ if (!entries.has("index.html") || !entries.has("src/app.js") || JSON.stringify([...entries.keys()].sort()) !== JSON.stringify(Object.keys(report.artifactHashes).sort()))
144
+ throw Error("corpus browser terminal resource set mismatch");
145
+ for (const [name, entry] of entries) {
146
+ if (entry.type !== "blob" || entry.mode && !["100644", "100755"].includes(entry.mode))
147
+ throw Error("unsupported corpus browser resource mode");
148
+ const changed = changes.get(name);
149
+ if (changed && !changed.contentSha256)
150
+ throw Error("corpus browser checkpoint digest missing");
151
+ const expected = changed ? changed.contentSha256 : sha(execFileSync("git", ["show", `${checkpoint.gitHeadSha}:${name}`], { cwd: input.repoRoot, maxBuffer: 16 * 1024 * 1024 }));
152
+ if (report.artifactHashes[name] !== expected)
153
+ throw Error("corpus browser terminal resource hash mismatch");
154
+ }
155
+ for (const [name, value] of Object.entries(observation.servedFiles))
156
+ if (!entries.has(safeName(name)) || report.artifactHashes[name] !== value)
157
+ throw Error("corpus browser served unbound resource");
158
+ if (report.pass && (!observation.servedFiles["index.html"] || !observation.servedFiles["src/app.js"]))
159
+ throw Error("corpus browser entry request missing");
160
+ for (const snapshot of observed) {
161
+ const latest = await readFileBoundarySafe({ boundaryRoot: snapshot.root, targetPath: path.resolve(snapshot.root, snapshot.name), label: "corpus browser recheck" });
162
+ if (!latest.equals(snapshot.bytes))
163
+ throw Error("corpus browser evidence changed during collection");
164
+ }
165
+ const diagnostics = ["corpus-browser-only-product-gates-pending"];
166
+ let browserPassed = failed.length || observation.pageErrors.length || observation.resourceErrors.length ? false : allPresent ? true : null;
167
+ if (observation.infrastructureError !== undefined) {
168
+ browserPassed = observation.resourceErrors.length ? false : null;
169
+ diagnostics.push("corpus-browser-infrastructure-error");
170
+ }
171
+ else if (!allPresent)
172
+ diagnostics.push("corpus-browser-checks-incomplete");
173
+ return { browserPassed, diagnostics, runId: report.runId, caseId: report.caseId, taskRef: report.taskRef, cellId: report.cellId,
174
+ corpusHash: corpus.corpusHash, reportSha256: input.report.sha256, contractSha256: input.contract.sha256, artifactHashes: report.artifactHashes };
175
+ }
@@ -0,0 +1,226 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { readFileBoundarySafe } from '../../shared/path-safety.js';
5
+ import { campaignPlanSchema } from './types.js';
6
+ import { computeCampaignPlanHash } from './campaign-hash.js';
7
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
8
+ const count = z.number().int().nonnegative().safe();
9
+ const ref = z.object({ path: z.string().min(1), sha256: digest }).strict();
10
+ const bindingSchema = z.object({ campaignId: z.string().min(1), cellId: z.string().min(1), taskRef: z.string().min(1), planHash: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).strict();
11
+ const contractSchema = z.object({ schemaVersion: z.literal(3), evidenceBinding: bindingSchema, upstreamKind: z.enum(['meter', 'provider']), model: z.string().min(1), upstream: z.string().url(), maxTokens: count.positive(), maxRequests: count.positive(), maxWallMs: count.positive(), requestTokenCeiling: count.positive(), payloadByteLimit: count.positive().optional(), maxOutputTokens: count.positive().optional() }).strict();
12
+ const stateSchema = z.object({ schemaVersion: z.literal(1), id: z.string().uuid(), contract: digest, startedAt: count.nullable(), requests: count, tokens: count, inflight: z.null(), stopped: z.string().nullable(), actualModels: z.array(z.string()) }).strict();
13
+ const rowSchema = z.object({ schemaVersion: z.literal(3), evidenceKind: z.literal('gateway-fetch-attempt'), meterId: z.string().uuid(), contract: digest, evidenceBinding: bindingSchema, upstreamKind: z.enum(['meter', 'provider']), requestId: z.string().uuid(), parentRequestId: z.string().uuid().nullable(), request: count.positive(), startedAt: count, finishedAt: count, httpStatus: z.number().int().min(100).max(599).nullable(), totalTokens: count.nullable(), responseUsage: z.record(count), actualModels: z.array(z.string()), error: z.string().nullable(), stopped: z.string().nullable(), wireRequest: z.object({ model: z.string().min(1), payloadSha256: digest, bytes: count, stream: z.boolean(), parameterCoverage: z.literal('selected-explicit-parameters'), explicitParameters: z.record(z.unknown()) }).strict() }).strict();
14
+ /** A single meter's frozen observations, not a task-wide request count or a
15
+ * billing result. The caller must bind expected identity and artifact hashes
16
+ * to its preregistered protocol, then validate topology and run coverage. */
17
+ export async function readFrontendGatewayEvidence(input) {
18
+ const expected = bindingSchema.parse(input.expectedBinding);
19
+ const snapshots = [];
20
+ const read = async (raw) => {
21
+ const value = ref.parse(raw), target = path.resolve(input.repoRoot, value.path);
22
+ const bytes = await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: target, label: 'gateway evidence' });
23
+ if (createHash('sha256').update(bytes).digest('hex') !== value.sha256)
24
+ throw Error('gateway evidence hash mismatch');
25
+ snapshots.push({ target, bytes });
26
+ return bytes.toString('utf8');
27
+ };
28
+ const contract = contractSchema.parse(JSON.parse(await read(input.contract)));
29
+ const state = stateSchema.parse(JSON.parse(await read(input.state)));
30
+ const text = await read(input.requests);
31
+ const rows = text.trim() ? text.trim().split('\n').map(line => rowSchema.parse(JSON.parse(line))) : [];
32
+ const sameBinding = (value) => JSON.stringify(value) === JSON.stringify(expected);
33
+ if (!sameBinding(contract.evidenceBinding) || state.contract !== input.contract.sha256)
34
+ throw Error('gateway contract identity mismatch');
35
+ if (state.requests !== rows.length || state.requests > contract.maxRequests)
36
+ throw Error('gateway request ledger mismatch');
37
+ if (state.startedAt !== (rows[0]?.startedAt ?? null))
38
+ throw Error('gateway start time mismatch');
39
+ const seen = new Set(), models = new Set();
40
+ let accountedTokens = 0, knownTokens = 0, unknownUsageAttempts = 0, previousFinished = 0;
41
+ for (const [index, row] of rows.entries()) {
42
+ if (row.meterId !== state.id || row.contract !== state.contract || !sameBinding(row.evidenceBinding) || row.upstreamKind !== contract.upstreamKind || row.wireRequest.model !== contract.model)
43
+ throw Error('gateway row identity mismatch');
44
+ if (row.request !== index + 1 || seen.has(row.requestId))
45
+ throw Error('gateway duplicate or missing request');
46
+ if (row.finishedAt < row.startedAt || row.startedAt < previousFinished)
47
+ throw Error('gateway request timing mismatch');
48
+ if (row.totalTokens !== null && row.responseUsage.total_tokens !== row.totalTokens || row.totalTokens === null && row.responseUsage.total_tokens !== undefined)
49
+ throw Error('gateway reported usage mismatch');
50
+ seen.add(row.requestId);
51
+ previousFinished = row.finishedAt;
52
+ for (const model of row.actualModels)
53
+ models.add(model);
54
+ if (row.totalTokens === null)
55
+ unknownUsageAttempts++;
56
+ else
57
+ knownTokens += row.totalTokens;
58
+ accountedTokens += row.totalTokens ?? contract.requestTokenCeiling;
59
+ }
60
+ if (!Number.isSafeInteger(accountedTokens) || state.tokens !== accountedTokens)
61
+ throw Error('gateway token ledger mismatch');
62
+ if (unknownUsageAttempts && !state.stopped)
63
+ throw Error('gateway unknown usage without stop');
64
+ if (JSON.stringify([...models].sort()) !== JSON.stringify([...new Set(state.actualModels)].sort()))
65
+ throw Error('gateway response model ledger mismatch');
66
+ for (const snapshot of snapshots) {
67
+ const latest = await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: snapshot.target, label: 'gateway evidence recheck' });
68
+ if (!latest.equals(snapshot.bytes))
69
+ throw Error('gateway evidence changed during collection');
70
+ }
71
+ return { evidenceKind: 'single-meter-observations', meterId: state.id, upstreamKind: contract.upstreamKind, upstream: contract.upstream, binding: expected,
72
+ observedFetchAttempts: rows.length, accountedTokens, reportedTokens: unknownUsageAttempts ? null : knownTokens, unknownUsageAttempts,
73
+ stopped: state.stopped, rows };
74
+ }
75
+ const topologySchema = z.object({ schemaVersion: z.literal(2), kind: z.literal('frontend-gateway-topology'), campaignId: z.string().min(1), registeredAt: z.string().datetime(), meters: z.array(z.object({ meterId: z.string().uuid(), role: z.enum(['cell', 'shared']), endpoint: z.string().url(), contract: ref, binding: bindingSchema, upstreamMeterId: z.string().uuid().nullable() }).strict()).min(1) }).strict();
76
+ /** Plan precedes gateway topology, which precedes pair registration. Gateway
77
+ * contracts reference planHash, never the registration that contains them. */
78
+ export async function assertFrontendGatewayTopology(input) {
79
+ const plan = campaignPlanSchema.parse(input.plan);
80
+ if (computeCampaignPlanHash(plan.campaignId, plan.cells) !== plan.planHash)
81
+ throw Error('gateway plan hash mismatch');
82
+ const snapshots = [];
83
+ const read = async (raw) => {
84
+ const value = ref.parse(raw), target = path.resolve(input.repoRoot, value.path);
85
+ const bytes = await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: target, label: 'registered gateway topology' });
86
+ if (createHash('sha256').update(bytes).digest('hex') !== value.sha256)
87
+ throw Error('gateway topology hash mismatch');
88
+ snapshots.push({ target, bytes });
89
+ return JSON.parse(bytes.toString('utf8'));
90
+ };
91
+ const topology = topologySchema.parse(await read(input.topology));
92
+ const registeredAt = Date.parse(z.string().datetime().parse(input.registrationCreatedAt));
93
+ if (topology.campaignId !== plan.campaignId || Date.parse(topology.registeredAt) < Date.parse(plan.createdAt) || Date.parse(topology.registeredAt) > registeredAt)
94
+ throw Error('gateway topology registration order mismatch');
95
+ const meters = new Map(topology.meters.map(m => [m.meterId, m])), endpoints = new Set(topology.meters.map(m => m.endpoint));
96
+ if (meters.size !== topology.meters.length || endpoints.size !== meters.size)
97
+ throw Error('gateway topology duplicates');
98
+ const cells = plan.cells.filter(cell => cell.executionRole === 'candidate_run'), matched = new Set();
99
+ for (const meter of meters.values()) {
100
+ if (meter.binding.campaignId !== plan.campaignId || meter.binding.planHash !== plan.planHash || new URL(meter.endpoint).origin !== meter.endpoint)
101
+ throw Error('gateway topology plan binding mismatch');
102
+ const contract = contractSchema.parse(await read(meter.contract));
103
+ if (JSON.stringify(contract.evidenceBinding) !== JSON.stringify(meter.binding))
104
+ throw Error('gateway contract binding mismatch');
105
+ const upstream = new URL(contract.upstream);
106
+ if (meter.upstreamMeterId !== null) {
107
+ if (contract.upstreamKind !== 'meter' || upstream.origin !== meters.get(meter.upstreamMeterId)?.endpoint || !['/chat/completions', '/v1/chat/completions'].includes(upstream.pathname) || upstream.search || upstream.hash || upstream.username || upstream.password)
108
+ throw Error('gateway upstream mismatch');
109
+ }
110
+ else if (contract.upstreamKind !== 'provider' || endpoints.has(upstream.origin))
111
+ throw Error('gateway provider edge mislabeled');
112
+ const seen = new Set();
113
+ let next = meter.meterId;
114
+ while (next !== null) {
115
+ if (seen.has(next) || !meters.has(next))
116
+ throw Error('gateway topology cyclic or incomplete');
117
+ seen.add(next);
118
+ next = meters.get(next).upstreamMeterId;
119
+ }
120
+ if (meter.role === 'cell') {
121
+ const candidates = cells.filter(cell => cell.cellId === meter.binding.cellId && cell.taskRef === meter.binding.taskRef);
122
+ if (candidates.length !== 1 || matched.has(meter.binding.cellId))
123
+ throw Error('gateway topology cell mismatch');
124
+ matched.add(meter.binding.cellId);
125
+ }
126
+ }
127
+ if (matched.size !== cells.length)
128
+ throw Error('gateway topology does not cover planned cells');
129
+ for (const snapshot of snapshots)
130
+ if (!(await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: snapshot.target, label: 'gateway admission recheck' })).equals(snapshot.bytes))
131
+ throw Error('gateway admission evidence changed');
132
+ return { topologySha256: input.topology.sha256, cellIds: [...matched] };
133
+ }
134
+ /** Verify the supplied snapshot graph. This does not establish that a runtime
135
+ * used only these gateways, nor that the snapshots cover its whole lifetime. */
136
+ export async function readFrontendGatewayGraph(input) {
137
+ const topologyRef = ref.parse(input.topology), protocol = z.string().regex(/^sha256:[a-f0-9]{64}$/).parse(input.expectedPlanHash);
138
+ const target = path.resolve(input.repoRoot, topologyRef.path);
139
+ const bytes = await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: target, label: 'gateway topology' });
140
+ if (createHash('sha256').update(bytes).digest('hex') !== topologyRef.sha256)
141
+ throw Error('gateway topology hash mismatch');
142
+ const topology = topologySchema.parse(JSON.parse(bytes.toString('utf8')));
143
+ const declared = new Map(topology.meters.map(m => [m.meterId, m]));
144
+ const supplied = new Map(input.evidence.map(m => [m.meterId, m]));
145
+ if (declared.size !== topology.meters.length || supplied.size !== input.evidence.length || declared.size !== supplied.size || [...supplied.keys()].some(id => !declared.has(id)))
146
+ throw Error('gateway topology meter set mismatch');
147
+ const endpoints = new Set();
148
+ for (const meter of declared.values()) {
149
+ if (new URL(meter.endpoint).origin !== meter.endpoint || endpoints.has(meter.endpoint))
150
+ throw Error('gateway endpoint invalid or duplicated');
151
+ endpoints.add(meter.endpoint);
152
+ if (meter.binding.campaignId !== topology.campaignId || meter.binding.planHash !== protocol)
153
+ throw Error('gateway topology protocol mismatch');
154
+ const seen = new Set();
155
+ let next = meter.meterId;
156
+ while (next !== null) {
157
+ if (seen.has(next))
158
+ throw Error('gateway topology cycle');
159
+ seen.add(next);
160
+ const item = declared.get(next);
161
+ if (!item)
162
+ throw Error('gateway topology upstream missing');
163
+ next = item.upstreamMeterId;
164
+ }
165
+ }
166
+ const snapshots = [];
167
+ for (const meter of declared.values()) {
168
+ const evidence = supplied.get(meter.meterId);
169
+ const snapshot = await readFrontendGatewayEvidence({ repoRoot: input.repoRoot, expectedBinding: meter.binding, contract: meter.contract, state: evidence.state, requests: evidence.requests });
170
+ if (snapshot.meterId !== meter.meterId)
171
+ throw Error('gateway topology meter identity mismatch');
172
+ const upstream = new URL(snapshot.upstream);
173
+ if (meter.upstreamMeterId !== null) {
174
+ if (snapshot.upstreamKind !== 'meter' || upstream.origin !== declared.get(meter.upstreamMeterId).endpoint || !['/chat/completions', '/v1/chat/completions'].includes(upstream.pathname) || upstream.search || upstream.hash || upstream.username || upstream.password)
175
+ throw Error('gateway topology upstream endpoint mismatch');
176
+ }
177
+ else if (snapshot.upstreamKind !== 'provider' || endpoints.has(upstream.origin))
178
+ throw Error('gateway provider edge mislabeled');
179
+ if (snapshot.rows.some(row => Date.parse(topology.registeredAt) >= row.startedAt))
180
+ throw Error('gateway topology registered after dispatch');
181
+ snapshots.push(snapshot);
182
+ }
183
+ const rows = new Map();
184
+ for (const row of snapshots.flatMap(s => s.rows)) {
185
+ if (rows.has(row.requestId))
186
+ throw Error('gateway request ID reused across meters');
187
+ rows.set(row.requestId, row);
188
+ }
189
+ const children = new Map();
190
+ for (const row of rows.values()) {
191
+ if (row.parentRequestId === null) {
192
+ if (declared.get(row.meterId).role !== 'cell')
193
+ throw Error('shared gateway request lacks originating cell');
194
+ continue;
195
+ }
196
+ const parent = rows.get(row.parentRequestId);
197
+ if (!parent || declared.get(parent.meterId).upstreamMeterId !== row.meterId || children.has(parent.requestId))
198
+ throw Error('gateway parent edge missing or ambiguous');
199
+ if (parent.wireRequest.payloadSha256 !== row.wireRequest.payloadSha256 || parent.wireRequest.bytes !== row.wireRequest.bytes || parent.wireRequest.model !== row.wireRequest.model || row.startedAt < parent.startedAt || row.finishedAt > parent.finishedAt)
200
+ throw Error('gateway forwarded request mismatch');
201
+ children.set(parent.requestId, row);
202
+ }
203
+ const diagnostics = [];
204
+ for (const row of rows.values())
205
+ if (row.upstreamKind === 'meter' && !children.has(row.requestId)) {
206
+ if (row.httpStatus !== null && row.httpStatus >= 200 && row.httpStatus < 300)
207
+ throw Error('successful gateway hop missing');
208
+ diagnostics.push(`unresolved-meter-hop:${row.requestId}`);
209
+ }
210
+ const providerAttempts = [...rows.values()].filter(row => row.upstreamKind === 'provider').map(row => {
211
+ let origin = row;
212
+ while (origin.parentRequestId !== null)
213
+ origin = rows.get(origin.parentRequestId);
214
+ return { ...row, originCellId: origin.evidenceBinding.cellId, originTaskRef: origin.evidenceBinding.taskRef };
215
+ });
216
+ // Re-read against the same hashes after graph validation, so independent
217
+ // meter snapshots cannot silently change while other meters are collected.
218
+ for (const meter of declared.values()) {
219
+ const evidence = supplied.get(meter.meterId);
220
+ await readFrontendGatewayEvidence({ repoRoot: input.repoRoot, expectedBinding: meter.binding, contract: meter.contract, state: evidence.state, requests: evidence.requests });
221
+ }
222
+ if (!(await readFileBoundarySafe({ boundaryRoot: input.repoRoot, targetPath: target, label: 'gateway topology recheck' })).equals(bytes))
223
+ throw Error('gateway topology changed during collection');
224
+ return { evidenceKind: 'observed-gateway-request-graph', topologySha256: topologyRef.sha256, observedProviderAttempts: providerAttempts.length,
225
+ reportedProviderTokens: diagnostics.length || providerAttempts.some(row => row.totalTokens === null) ? null : providerAttempts.reduce((sum, row) => sum + row.totalTokens, 0), providerAttempts, diagnostics };
226
+ }