@tea-agent/loop-agent 0.44.0-next.1 → 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.
- package/CHANGELOG.md +379 -416
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
- package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/plan/record-tools.js +861 -0
- package/dist/executors/dag-pi/plan/schema.js +129 -0
- package/dist/executors/dag-pi/plan/session.js +1 -0
- package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +398 -3384
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +112 -19
- package/dist/executors/shell-executor.js +88 -12
- package/dist/executors/shell-presets.js +1 -1
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +48 -14
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +482 -0
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +51 -0
- package/dist/task/config-types.js +22 -8
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -7
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +62 -10
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/db-connections.js +854 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
- package/dist/worker/console/chat/pi-runtime.js +445 -36
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +577 -72
- package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/timeline-message-order.js +22 -0
- package/dist/worker/console/chat/tools.js +34 -0
- package/dist/worker/console/chat/turn-process.js +237 -66
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/dag-confirmation.js +3 -0
- package/dist/worker/console/dag-execution-receipt.js +3 -0
- package/dist/worker/console/operator-actions.js +32 -3
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/prd-intake-bridge.js +5 -12
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
- package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-context.js +2 -0
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
- package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +3 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -66
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/dist/workflows/loop/benchmark.js +11 -8
- package/docs/README.md +1 -1
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/docs/templates/backend-test-dag.json +5 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +6 -4
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
5
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
6
|
+
import { campaignPlanSchema, candidateIdSchema } from "./types.js";
|
|
7
|
+
import { computeCampaignPlanHash } from "./campaign-hash.js";
|
|
8
|
+
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
9
|
+
const bundleHash = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
10
|
+
const ref = z.object({ path: z.string().min(1), sha256: hash }).strict();
|
|
11
|
+
// Keep registration schemas independent of DAG evidence readers: DAG types
|
|
12
|
+
// import this module while the readers themselves depend on DAG lifecycle.
|
|
13
|
+
export const frontendSharedArtifactsSchema = z.object({ source: ref, budget: ref, model: ref, initialTree: ref }).strict();
|
|
14
|
+
const sharedSchema = z.object({ source: hash, model: hash, budget: hash, initialTree: hash, acceptance: hash }).strict();
|
|
15
|
+
const armSchema = z.object({ candidateId: candidateIdSchema, bundleHash, controllerFingerprint: bundleHash, controller: ref, promptTools: ref }).strict();
|
|
16
|
+
const registrationSchema = z.object({
|
|
17
|
+
schemaVersion: z.literal(1), kind: z.literal("frontend-task-pair-registration"),
|
|
18
|
+
createdAt: z.string().datetime(), campaignId: z.string().min(1), planHash: bundleHash,
|
|
19
|
+
baseline: armSchema, candidate: armSchema, shared: sharedSchema, sharedArtifacts: frontendSharedArtifactsSchema.optional(),
|
|
20
|
+
gatewayTopology: ref.optional(),
|
|
21
|
+
}).strict();
|
|
22
|
+
const observedSchema = sharedSchema.extend({ protocol: hash, controller: hash, promptTools: hash }).strict();
|
|
23
|
+
/** Identity admission only, not acceptance or promotion. `identities` must be
|
|
24
|
+
* collected by a reader which verifies the run and all referenced artifacts;
|
|
25
|
+
* this gate binds those identities to predeclared, rehashed candidate bytes. */
|
|
26
|
+
export async function validateFrontendPairRegistration(input) {
|
|
27
|
+
const registrationRef = ref.parse(input.registration);
|
|
28
|
+
const read = async () => {
|
|
29
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: input.repoRoot,
|
|
30
|
+
targetPath: path.resolve(input.repoRoot, registrationRef.path), label: "frontend pair registration" });
|
|
31
|
+
if (createHash("sha256").update(bytes).digest("hex") !== registrationRef.sha256)
|
|
32
|
+
throw Error("pair registration hash mismatch");
|
|
33
|
+
return registrationSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
34
|
+
};
|
|
35
|
+
const registration = await read();
|
|
36
|
+
const plan = campaignPlanSchema.parse(input.plan);
|
|
37
|
+
if (computeCampaignPlanHash(plan.campaignId, plan.cells) !== plan.planHash)
|
|
38
|
+
throw Error("pair plan content hash mismatch");
|
|
39
|
+
if (registration.campaignId !== plan.campaignId || registration.planHash !== plan.planHash)
|
|
40
|
+
throw Error("pair registration plan mismatch");
|
|
41
|
+
if (registration.baseline.candidateId === registration.candidate.candidateId)
|
|
42
|
+
throw Error("distinct registered candidates required");
|
|
43
|
+
const startedAt = Date.parse(z.string().datetime().parse(input.earliestRunStartedAt));
|
|
44
|
+
const registeredAt = Date.parse(registration.createdAt), plannedAt = Date.parse(plan.createdAt);
|
|
45
|
+
if (!Number.isFinite(plannedAt) || plannedAt > registeredAt || registeredAt >= startedAt)
|
|
46
|
+
throw Error("pair must be planned and registered before execution");
|
|
47
|
+
const baselineCells = plan.cells.filter(cell => cell.cellId === input.baselineCellId);
|
|
48
|
+
const candidateCells = plan.cells.filter(cell => cell.cellId === input.candidateCellId);
|
|
49
|
+
if (baselineCells.length !== 1 || candidateCells.length !== 1)
|
|
50
|
+
throw Error("pair cells missing or duplicated");
|
|
51
|
+
const baseline = baselineCells[0], candidate = candidateCells[0];
|
|
52
|
+
if (baseline.executionRole !== "candidate_run" || candidate.executionRole !== "candidate_run" ||
|
|
53
|
+
baseline.taskRef !== candidate.taskRef || baseline.seed !== candidate.seed || baseline.split !== candidate.split)
|
|
54
|
+
throw Error("pair cells do not share task/seed/split");
|
|
55
|
+
for (const [side, cell] of [["baseline", baseline], ["candidate", candidate]]) {
|
|
56
|
+
const arm = registration[side];
|
|
57
|
+
if (cell.candidateId !== arm.candidateId || cell.bundleHash !== arm.bundleHash)
|
|
58
|
+
throw Error("pair cell candidate mismatch");
|
|
59
|
+
const record = await readCandidateRecord(input.repoRoot, arm.candidateId);
|
|
60
|
+
if (record.manifest.bundleHash !== arm.bundleHash)
|
|
61
|
+
throw Error("registered candidate bundle mismatch");
|
|
62
|
+
const candidateTime = Date.parse(record.manifest.createdAt);
|
|
63
|
+
const recordedAt = Date.parse(record.events[0]?.at ?? "");
|
|
64
|
+
if (!Number.isFinite(candidateTime) || !Number.isFinite(recordedAt) ||
|
|
65
|
+
candidateTime > registeredAt || recordedAt > registeredAt)
|
|
66
|
+
throw Error("candidate registered after pair protocol");
|
|
67
|
+
const identity = observedSchema.parse(input.identities[side]);
|
|
68
|
+
if (identity.protocol !== registrationRef.sha256)
|
|
69
|
+
throw Error("observed protocol mismatch");
|
|
70
|
+
for (const key of ["source", "model", "budget", "initialTree", "acceptance"]) {
|
|
71
|
+
if (identity[key] !== registration.shared[key])
|
|
72
|
+
throw Error(`shared pair identity mismatch: ${key}`);
|
|
73
|
+
}
|
|
74
|
+
for (const key of ["controller", "promptTools"]) {
|
|
75
|
+
const declared = arm[key];
|
|
76
|
+
if (!record.manifest.contentRefs.some(content => content.path === declared.path && content.sha256 === `sha256:${declared.sha256}`))
|
|
77
|
+
throw Error(`candidate does not own registered ${key} snapshot`);
|
|
78
|
+
if (identity[key] !== declared.sha256)
|
|
79
|
+
throw Error(`observed candidate ${key} mismatch`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (registration.gatewayTopology) {
|
|
83
|
+
const { assertFrontendGatewayTopology } = await import("./frontend-gateway-evidence.js");
|
|
84
|
+
await assertFrontendGatewayTopology({ repoRoot: input.repoRoot, topology: registration.gatewayTopology, plan, registrationCreatedAt: registration.createdAt });
|
|
85
|
+
}
|
|
86
|
+
await read();
|
|
87
|
+
return { campaignId: plan.campaignId, planHash: plan.planHash, registrationSha256: registrationRef.sha256,
|
|
88
|
+
baselineCandidateId: baseline.candidateId, candidateId: candidate.candidateId };
|
|
89
|
+
}
|
|
90
|
+
export const frontendPairExecutionBindingSchema = z.object({
|
|
91
|
+
registration: ref, cellId: z.string().min(1),
|
|
92
|
+
}).strict();
|
|
93
|
+
/** Called by the runner before allocation and on resume. */
|
|
94
|
+
export async function assertFrontendPairExecution(input) {
|
|
95
|
+
const pair = frontendPairExecutionBindingSchema.parse(input.binding.frontendPair);
|
|
96
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: input.repoRoot,
|
|
97
|
+
targetPath: path.resolve(input.repoRoot, pair.registration.path), label: "frontend execution registration" });
|
|
98
|
+
if (createHash("sha256").update(bytes).digest("hex") !== pair.registration.sha256)
|
|
99
|
+
throw Error("execution registration hash mismatch");
|
|
100
|
+
const protocol = registrationSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
101
|
+
const { readCampaignPlan } = await import("../../infrastructure/evaluation/campaign-store.js");
|
|
102
|
+
const stored = await readCampaignPlan(input.repoRoot, input.binding.campaignId);
|
|
103
|
+
if (!stored)
|
|
104
|
+
throw Error("frontend pair campaign plan missing");
|
|
105
|
+
const plan = campaignPlanSchema.parse(stored);
|
|
106
|
+
if (protocol.campaignId !== input.binding.campaignId || protocol.planHash !== plan.planHash ||
|
|
107
|
+
computeCampaignPlanHash(plan.campaignId, plan.cells) !== plan.planHash)
|
|
108
|
+
throw Error("execution pair plan mismatch");
|
|
109
|
+
const registeredAt = Date.parse(protocol.createdAt), startedAt = Date.parse(input.startedAt), plannedAt = Date.parse(plan.createdAt);
|
|
110
|
+
if (![registeredAt, startedAt, plannedAt].every(Number.isFinite) || plannedAt > registeredAt || registeredAt >= startedAt)
|
|
111
|
+
throw Error("execution pair protocol must predate run");
|
|
112
|
+
const arms = [protocol.baseline, protocol.candidate].filter(arm => arm.candidateId === input.binding.candidateId);
|
|
113
|
+
const cells = plan.cells.filter(cell => cell.cellId === pair.cellId);
|
|
114
|
+
if (arms.length !== 1 || cells.length !== 1)
|
|
115
|
+
throw Error("execution pair arm or cell missing/ambiguous");
|
|
116
|
+
const arm = arms[0], cell = cells[0];
|
|
117
|
+
if (cell.executionRole !== "candidate_run" || cell.candidateId !== arm.candidateId || cell.bundleHash !== arm.bundleHash ||
|
|
118
|
+
input.binding.bundleHash !== arm.bundleHash || cell.seed !== input.binding.seed || cell.split !== input.binding.split || cell.taskRef !== input.binding.taskRef)
|
|
119
|
+
throw Error("execution pair cell binding mismatch");
|
|
120
|
+
if (protocol.baseline.candidateId === protocol.candidate.candidateId)
|
|
121
|
+
throw Error("execution pair requires distinct arms");
|
|
122
|
+
for (const member of [protocol.baseline, protocol.candidate]) {
|
|
123
|
+
const partners = plan.cells.filter(row => row.executionRole === "candidate_run" && row.candidateId === member.candidateId &&
|
|
124
|
+
row.taskRef === cell.taskRef && row.seed === cell.seed && row.split === cell.split);
|
|
125
|
+
if (partners.length !== 1 || partners[0].bundleHash !== member.bundleHash)
|
|
126
|
+
throw Error("execution pair counterpart missing or mismatched");
|
|
127
|
+
}
|
|
128
|
+
if (arm.controllerFingerprint !== input.controllerFingerprint)
|
|
129
|
+
throw Error("execution pair controller fingerprint mismatch");
|
|
130
|
+
const record = await readCandidateRecord(input.repoRoot, arm.candidateId);
|
|
131
|
+
const recordedAt = Date.parse(record.events[0]?.at ?? "");
|
|
132
|
+
if (record.manifest.bundleHash !== arm.bundleHash || !Number.isFinite(recordedAt) || recordedAt > registeredAt)
|
|
133
|
+
throw Error("execution pair candidate registration mismatch");
|
|
134
|
+
for (const key of ["controller", "promptTools"]) {
|
|
135
|
+
if (!record.manifest.contentRefs.some(content => content.path === arm[key].path && content.sha256 === `sha256:${arm[key].sha256}`))
|
|
136
|
+
throw Error("execution pair snapshot not owned by candidate");
|
|
137
|
+
}
|
|
138
|
+
if (protocol.gatewayTopology) {
|
|
139
|
+
const { assertFrontendGatewayTopology } = await import("./frontend-gateway-evidence.js");
|
|
140
|
+
await assertFrontendGatewayTopology({ repoRoot: input.repoRoot, topology: protocol.gatewayTopology, plan, registrationCreatedAt: protocol.createdAt });
|
|
141
|
+
}
|
|
142
|
+
return { planHash: plan.planHash, shared: protocol.shared, sharedArtifacts: protocol.sharedArtifacts, gatewayTopology: protocol.gatewayTopology };
|
|
143
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { campaignPlanSchema } from "./types.js";
|
|
3
|
+
const metric = z.number().finite().nonnegative().nullable();
|
|
4
|
+
const observationSchema = z.object({
|
|
5
|
+
cellId: z.string().min(1),
|
|
6
|
+
bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
7
|
+
runId: z.string().min(1),
|
|
8
|
+
firstPassed: z.boolean().nullable(),
|
|
9
|
+
finalPassed: z.boolean().nullable(),
|
|
10
|
+
requests: z.number().int().nonnegative().nullable(),
|
|
11
|
+
tokens: z.number().int().nonnegative().nullable(),
|
|
12
|
+
wallClockMs: metric,
|
|
13
|
+
}).strict().refine(row => row.firstPassed !== true || row.finalPassed === true, {
|
|
14
|
+
message: "first success requires final success",
|
|
15
|
+
});
|
|
16
|
+
/** For independent paired trials, Hoeffding bounds the mean of differences
|
|
17
|
+
* in [-1, 1]: P(|mean - E(mean)| >= e) <= 2 exp(-n e² / 2).
|
|
18
|
+
* This deliberately stays wide for small samples, including all ties.
|
|
19
|
+
* Each interval has marginal 95% coverage, not simultaneous coverage across
|
|
20
|
+
* metrics/cases, and targets this fixed plan rather than unseen task classes. */
|
|
21
|
+
function pairedUncertainty(pairs) {
|
|
22
|
+
const calculate = (field) => {
|
|
23
|
+
let observed = 0, totalDifference = 0;
|
|
24
|
+
for (const pair of pairs) {
|
|
25
|
+
const before = pair.baseline?.[field], after = pair.candidate?.[field];
|
|
26
|
+
if (before == null || after == null)
|
|
27
|
+
continue;
|
|
28
|
+
observed++;
|
|
29
|
+
totalDifference += Number(after) - Number(before);
|
|
30
|
+
}
|
|
31
|
+
const complete = observed === pairs.length && observed > 0;
|
|
32
|
+
const difference = complete ? totalDifference / observed : null;
|
|
33
|
+
const halfWidth = complete ? Math.sqrt(2 * Math.log(40) / observed) : null;
|
|
34
|
+
return {
|
|
35
|
+
method: "paired-hoeffding", confidenceLevel: 0.95,
|
|
36
|
+
assumption: "independent-paired-trials-fixed-plan",
|
|
37
|
+
planned: pairs.length, observed, difference,
|
|
38
|
+
interval: difference === null || halfWidth === null ? null : {
|
|
39
|
+
lower: Math.max(-1, difference - halfWidth), upper: Math.min(1, difference + halfWidth),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
return { first: calculate("firstPassed"), final: calculate("finalPassed") };
|
|
44
|
+
}
|
|
45
|
+
function distribution(values) {
|
|
46
|
+
if (!values.length || values.some(value => value === null)) {
|
|
47
|
+
return { values, total: null, median: null, maximum: null };
|
|
48
|
+
}
|
|
49
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
50
|
+
const middle = Math.floor(sorted.length / 2);
|
|
51
|
+
return {
|
|
52
|
+
values,
|
|
53
|
+
total: sorted.reduce((sum, value) => sum + value, 0),
|
|
54
|
+
median: sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2,
|
|
55
|
+
maximum: sorted.at(-1),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function summarize(rows) {
|
|
59
|
+
const success = (field) => {
|
|
60
|
+
const values = rows.map(row => row?.[field] ?? null);
|
|
61
|
+
const passed = values.filter(value => value === true).length;
|
|
62
|
+
const failed = values.filter(value => value === false).length;
|
|
63
|
+
const unknown = values.length - passed - failed;
|
|
64
|
+
return { planned: rows.length, passed, failed, unknown, rate: unknown || !rows.length ? null : passed / rows.length };
|
|
65
|
+
};
|
|
66
|
+
const first = success("firstPassed"), final = success("finalPassed");
|
|
67
|
+
const requests = distribution(rows.map(row => row?.requests ?? null));
|
|
68
|
+
const tokens = distribution(rows.map(row => row?.tokens ?? null));
|
|
69
|
+
const wallClockMs = distribution(rows.map(row => row?.wallClockMs ?? null));
|
|
70
|
+
return {
|
|
71
|
+
first, final, requests, tokens, wallClockMs,
|
|
72
|
+
// Failed attempts remain in the numerator. Unknown or zero correct
|
|
73
|
+
// deliveries cannot yield an apparent zero-cost delivery.
|
|
74
|
+
tokensPerCorrectDelivery: final.unknown || !final.passed || tokens.total === null ? null : tokens.total / final.passed,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Descriptive statistics only. The caller must verify source artifacts and
|
|
78
|
+
* preregistered identity/model/budget/oracle bindings before interpreting these
|
|
79
|
+
* observations. This function never authorizes promotion or returns a pass. */
|
|
80
|
+
export function summarizeFrontendTaskPairs(input) {
|
|
81
|
+
const plan = campaignPlanSchema.parse(input.plan);
|
|
82
|
+
if (input.baselineCandidateId === input.candidateId)
|
|
83
|
+
throw Error("distinct candidates required");
|
|
84
|
+
const cells = plan.cells.filter(cell => cell.executionRole === "candidate_run" &&
|
|
85
|
+
[input.baselineCandidateId, input.candidateId].includes(cell.candidateId));
|
|
86
|
+
const byId = new Map(cells.map(cell => [cell.cellId, cell]));
|
|
87
|
+
if (byId.size !== cells.length)
|
|
88
|
+
throw Error("duplicate planned cell");
|
|
89
|
+
const observations = new Map();
|
|
90
|
+
const runIds = new Set();
|
|
91
|
+
for (const raw of input.observations) {
|
|
92
|
+
const row = observationSchema.parse(raw), cell = byId.get(row.cellId);
|
|
93
|
+
if (!cell || cell.bundleHash !== row.bundleHash)
|
|
94
|
+
throw Error("observation does not match planned candidate cell");
|
|
95
|
+
if (observations.has(row.cellId) || runIds.has(row.runId))
|
|
96
|
+
throw Error("duplicate cell or run observation");
|
|
97
|
+
observations.set(row.cellId, row);
|
|
98
|
+
runIds.add(row.runId);
|
|
99
|
+
}
|
|
100
|
+
const groups = new Map();
|
|
101
|
+
for (const cell of cells) {
|
|
102
|
+
const key = JSON.stringify([cell.split, cell.taskRef, cell.seed]);
|
|
103
|
+
const pair = groups.get(key) ?? { taskRef: cell.taskRef, split: cell.split, seed: cell.seed };
|
|
104
|
+
const side = cell.candidateId === input.baselineCandidateId ? "baselineCellId" : "candidateCellId";
|
|
105
|
+
if (pair[side])
|
|
106
|
+
throw Error("duplicate planned candidate/task/seed/split");
|
|
107
|
+
pair[side] = cell.cellId;
|
|
108
|
+
groups.set(key, pair);
|
|
109
|
+
}
|
|
110
|
+
if (!groups.size || [...groups.values()].some(pair => !pair.baselineCellId || !pair.candidateCellId)) {
|
|
111
|
+
throw Error("plan must contain both arms for every pair");
|
|
112
|
+
}
|
|
113
|
+
const pairs = [...groups.values()].map(pair => ({
|
|
114
|
+
...pair,
|
|
115
|
+
baseline: observations.get(pair.baselineCellId) ?? null,
|
|
116
|
+
candidate: observations.get(pair.candidateCellId) ?? null,
|
|
117
|
+
}));
|
|
118
|
+
const perCase = [...new Set(pairs.map(pair => JSON.stringify([pair.split, pair.taskRef])))].map(key => {
|
|
119
|
+
const selected = pairs.filter(pair => JSON.stringify([pair.split, pair.taskRef]) === key);
|
|
120
|
+
const tally = (field) => {
|
|
121
|
+
const counts = { baselineWins: 0, candidateWins: 0, ties: 0, unknown: 0 };
|
|
122
|
+
for (const pair of selected) {
|
|
123
|
+
const before = pair.baseline?.[field], after = pair.candidate?.[field];
|
|
124
|
+
if (before == null || after == null)
|
|
125
|
+
counts.unknown++;
|
|
126
|
+
else if (before === after)
|
|
127
|
+
counts.ties++;
|
|
128
|
+
else if (after)
|
|
129
|
+
counts.candidateWins++;
|
|
130
|
+
else
|
|
131
|
+
counts.baselineWins++;
|
|
132
|
+
}
|
|
133
|
+
return counts;
|
|
134
|
+
};
|
|
135
|
+
return { taskRef: selected[0].taskRef, split: selected[0].split,
|
|
136
|
+
first: tally("firstPassed"), final: tally("finalPassed"),
|
|
137
|
+
pairedUncertainty: pairedUncertainty(selected),
|
|
138
|
+
baseline: summarize(selected.map(pair => pair.baseline)),
|
|
139
|
+
candidate: summarize(selected.map(pair => pair.candidate)) };
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
evidenceKind: "descriptive-task-pair-summary", perCase,
|
|
143
|
+
campaignId: plan.campaignId, planHash: plan.planHash,
|
|
144
|
+
baselineCandidateId: input.baselineCandidateId, candidateId: input.candidateId,
|
|
145
|
+
pairs,
|
|
146
|
+
pairedUncertainty: pairedUncertainty(pairs),
|
|
147
|
+
baseline: summarize(pairs.map(pair => pair.baseline)),
|
|
148
|
+
candidate: summarize(pairs.map(pair => pair.candidate)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFrontendCorpusBrowserAcceptance } from "./frontend-corpus-browser-acceptance.js";
|
|
2
|
+
import { readFrontendGatewayGraph } from "./frontend-gateway-evidence.js";
|
|
3
|
+
import { readFrontendSharedEvidence } from "./frontend-shared-evidence.js";
|
|
4
|
+
import { readFrontendBrowserAcceptance } from "./frontend-browser-acceptance.js";
|
|
5
|
+
import { computeCampaignPlanHash } from "./campaign-hash.js";
|
|
6
|
+
import { readCampaignPlan } from "../../infrastructure/evaluation/campaign-store.js";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
11
|
+
import { locateDagRun, readDagRunSpec, readDagRunState } from "../../workflows/dag/lifecycle.js";
|
|
12
|
+
import { readFrontendRootObservation } from "../../workflows/dag/frontend-root-observation.js";
|
|
13
|
+
import { verifyControllerIdentityForResume } from "../../workflows/dag/controller-identity.js";
|
|
14
|
+
import { assertFrontendPairExecution } from "./frontend-pair-registration.js";
|
|
15
|
+
import { summarizeFrontendTaskPairs } from "./frontend-paired-summary.js";
|
|
16
|
+
/** Collect run/cost facts. FINISHED is deliberately not converted to a passed
|
|
17
|
+
* product: independent acceptance and shared-identity admission are separate. */
|
|
18
|
+
export async function readFrontendTaskRunObservation(input) {
|
|
19
|
+
if (input.acceptance && input.corpusBrowser)
|
|
20
|
+
throw Error("conflicting browser acceptance evidence");
|
|
21
|
+
const root = await readFrontendRootObservation(input.repoRoot, input.runId);
|
|
22
|
+
if (!root.runIds.length)
|
|
23
|
+
throw Error("frontend observation root missing");
|
|
24
|
+
const snapshots = [];
|
|
25
|
+
let binding;
|
|
26
|
+
let sharedRegistration;
|
|
27
|
+
let registeredAcceptance;
|
|
28
|
+
let firstStatus;
|
|
29
|
+
let lastState;
|
|
30
|
+
for (const runId of root.runIds) {
|
|
31
|
+
const located = await locateDagRun(input.repoRoot, runId);
|
|
32
|
+
if (!located)
|
|
33
|
+
throw Error("frontend observation run missing");
|
|
34
|
+
const read = (name) => readFileBoundarySafe({ boundaryRoot: located.runDir,
|
|
35
|
+
targetPath: path.join(located.runDir, name), label: "frontend run observation" });
|
|
36
|
+
const snapshot = { runDir: located.runDir, state: await read("state.json"), spec: await read("run.json"), controller: await read("controller-identity.json") };
|
|
37
|
+
const state = await readDagRunState(located.runDir), spec = await readDagRunSpec(located.runDir);
|
|
38
|
+
if (await realpath(state.cwd) !== await realpath(input.repoRoot))
|
|
39
|
+
throw Error("frontend observation workspace mismatch");
|
|
40
|
+
firstStatus ??= state.status;
|
|
41
|
+
if (state.runId !== runId || !state.evaluation?.frontendPair || !spec.evaluation?.frontendPair ||
|
|
42
|
+
JSON.stringify(state.evaluation) !== JSON.stringify(spec.evaluation))
|
|
43
|
+
throw Error("frontend observation frozen evaluation mismatch");
|
|
44
|
+
if (binding && JSON.stringify(binding) !== JSON.stringify(state.evaluation))
|
|
45
|
+
throw Error("frontend observation recovery evaluation drift");
|
|
46
|
+
binding = state.evaluation;
|
|
47
|
+
const artifact = JSON.parse(snapshot.controller.toString("utf8"));
|
|
48
|
+
if (state.controllerIdentityRef?.path !== "controller-identity.json" || artifact.status !== "pinned" || !artifact.identity)
|
|
49
|
+
throw Error("frontend observation controller not pinned");
|
|
50
|
+
const pinned = await verifyControllerIdentityForResume({ runDir: located.runDir, state, currentIdentity: artifact.identity });
|
|
51
|
+
const registration = await assertFrontendPairExecution({ repoRoot: input.repoRoot, binding,
|
|
52
|
+
controllerFingerprint: pinned.identity.packageFingerprint.value, startedAt: state.startedAt });
|
|
53
|
+
sharedRegistration = registration;
|
|
54
|
+
registeredAcceptance = registration.shared.acceptance;
|
|
55
|
+
snapshots.push(snapshot);
|
|
56
|
+
lastState = state;
|
|
57
|
+
}
|
|
58
|
+
let acceptance;
|
|
59
|
+
if (input.acceptance) {
|
|
60
|
+
if (input.acceptance.contract.sha256 !== registeredAcceptance)
|
|
61
|
+
throw Error("browser acceptance contract not registered for run");
|
|
62
|
+
acceptance = await readFrontendBrowserAcceptance({ repoRoot: input.repoRoot, runDir: snapshots.at(-1).runDir, ...input.acceptance });
|
|
63
|
+
}
|
|
64
|
+
let corpusBrowserEvidence;
|
|
65
|
+
if (input.corpusBrowser) {
|
|
66
|
+
if (input.corpusBrowser.contract.sha256 !== registeredAcceptance)
|
|
67
|
+
throw Error("corpus browser contract not registered for run");
|
|
68
|
+
corpusBrowserEvidence = await readFrontendCorpusBrowserAcceptance({ repoRoot: input.repoRoot, runDir: snapshots.at(-1).runDir, ...input.corpusBrowser });
|
|
69
|
+
}
|
|
70
|
+
// A failed required browser behavior disproves delivery. Passing browser
|
|
71
|
+
// checks alone does not prove the remaining product quality gates.
|
|
72
|
+
const productVerdict = corpusBrowserEvidence?.browserPassed === false || acceptance?.passed === false ? false : null;
|
|
73
|
+
const sharedEvidence = sharedRegistration?.sharedArtifacts ? await readFrontendSharedEvidence({
|
|
74
|
+
repoRoot: input.repoRoot, runDirs: snapshots.map(snapshot => snapshot.runDir),
|
|
75
|
+
artifacts: sharedRegistration.sharedArtifacts, expected: sharedRegistration.shared,
|
|
76
|
+
}) : undefined;
|
|
77
|
+
let gatewayObservation;
|
|
78
|
+
if (input.gatewaySnapshots) {
|
|
79
|
+
if (!sharedRegistration?.gatewayTopology)
|
|
80
|
+
throw Error("frontend run gateway topology not registered");
|
|
81
|
+
const graph = await readFrontendGatewayGraph({ repoRoot: input.repoRoot,
|
|
82
|
+
topology: sharedRegistration.gatewayTopology, expectedPlanHash: sharedRegistration.planHash,
|
|
83
|
+
evidence: input.gatewaySnapshots });
|
|
84
|
+
const providerAttempts = graph.providerAttempts.filter(row => row.originCellId === binding.frontendPair.cellId);
|
|
85
|
+
if (providerAttempts.some(row => row.originTaskRef !== binding.taskRef))
|
|
86
|
+
throw Error("frontend gateway task binding mismatch");
|
|
87
|
+
gatewayObservation = {
|
|
88
|
+
evidenceKind: "registered-cell-gateway-observations",
|
|
89
|
+
cellId: binding.frontendPair.cellId, taskRef: binding.taskRef,
|
|
90
|
+
topologySha256: graph.topologySha256, snapshots: input.gatewaySnapshots,
|
|
91
|
+
observedProviderAttempts: providerAttempts.length,
|
|
92
|
+
reportedProviderTokens: graph.diagnostics.length || providerAttempts.some(row => row.totalTokens === null)
|
|
93
|
+
? null : providerAttempts.reduce((sum, row) => sum + row.totalTokens, 0),
|
|
94
|
+
providerAttempts,
|
|
95
|
+
// Graph-wide unresolved hops remain conservative even for another cell.
|
|
96
|
+
diagnostics: [...graph.diagnostics, "gateway-run-coverage-not-proven"],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
for (const snapshot of snapshots)
|
|
100
|
+
for (const [file, expected] of [["state.json", snapshot.state], ["run.json", snapshot.spec], ["controller-identity.json", snapshot.controller]]) {
|
|
101
|
+
const actual = await readFileBoundarySafe({ boundaryRoot: snapshot.runDir, targetPath: path.join(snapshot.runDir, file), label: "frontend observation recheck" });
|
|
102
|
+
if (!actual.equals(expected))
|
|
103
|
+
throw Error("frontend observation changed during collection");
|
|
104
|
+
}
|
|
105
|
+
const recheckedRoot = await readFrontendRootObservation(input.repoRoot, input.runId);
|
|
106
|
+
if (JSON.stringify(recheckedRoot) !== JSON.stringify(root))
|
|
107
|
+
throw Error("frontend root observations changed during collection");
|
|
108
|
+
const knownFailure = root.complete && ["failed", "partial_failed"].includes(lastState.status);
|
|
109
|
+
const consumed = root.cumulativeBudget?.consumed;
|
|
110
|
+
// Reservations precede dispatch and can survive a failed dispatch. The root
|
|
111
|
+
// timer starts at DAG allocation, after task/source preparation. Keep these
|
|
112
|
+
// useful facts, but do not relabel them as task-wide request/time metrics.
|
|
113
|
+
const observedBudget = {
|
|
114
|
+
reservedRequests: root.complete ? consumed?.providerRequests ?? null : null,
|
|
115
|
+
cumulativeTokens: root.complete && consumed?.missingTokenNodeIds.length === 0 ? consumed.tokens : null,
|
|
116
|
+
dagWallClockMs: root.dagWallClockMs,
|
|
117
|
+
};
|
|
118
|
+
const metricDiagnostics = ["task-provider-request-count-not-observed", "task-wallclock-not-observed"];
|
|
119
|
+
if (!root.sourceUsage?.complete)
|
|
120
|
+
metricDiagnostics.push("task-source-usage-not-observed");
|
|
121
|
+
const observation = {
|
|
122
|
+
cellId: binding.frontendPair.cellId, bundleHash: binding.bundleHash, runId: root.runIds[0],
|
|
123
|
+
firstPassed: root.complete && root.runIds.length === 1 ? (knownFailure ? false : productVerdict) : root.complete && ["failed", "partial_failed"].includes(firstStatus) ? false : null,
|
|
124
|
+
finalPassed: root.complete ? (knownFailure ? false : productVerdict) : null,
|
|
125
|
+
requests: null,
|
|
126
|
+
tokens: root.sourceUsage?.complete ? observedBudget.cumulativeTokens : null,
|
|
127
|
+
wallClockMs: null,
|
|
128
|
+
};
|
|
129
|
+
return { campaignId: binding.campaignId, observation, outcome: root.complete ? lastState.status : null,
|
|
130
|
+
diagnostics: [...root.diagnostics, ...metricDiagnostics, ...(gatewayObservation?.diagnostics ?? ["gateway-snapshots-not-supplied"]), ...(corpusBrowserEvidence?.diagnostics ?? acceptance?.diagnostics ?? ["independent-acceptance-pending"]), ...(sharedEvidence?.diagnostics ?? ["shared-identity-admission-pending"])],
|
|
131
|
+
acceptance, corpusBrowserEvidence, sharedEvidence, observedBudget, gatewayObservation,
|
|
132
|
+
runs: snapshots.map((snapshot, index) => ({ runId: root.runIds[index], stateSha256: createHash("sha256").update(snapshot.state).digest("hex"), runSha256: createHash("sha256").update(snapshot.spec).digest("hex") })) };
|
|
133
|
+
}
|
|
134
|
+
export async function collectFrontendTaskPairSummary(input) {
|
|
135
|
+
const stored = await readCampaignPlan(input.repoRoot, input.plan.campaignId);
|
|
136
|
+
if (!stored || input.plan.planHash !== stored.planHash || computeCampaignPlanHash(input.plan.campaignId, input.plan.cells) !== stored.planHash)
|
|
137
|
+
throw Error("frontend summary registered plan mismatch");
|
|
138
|
+
const collected = [];
|
|
139
|
+
for (const runId of input.runIds)
|
|
140
|
+
collected.push(await readFrontendTaskRunObservation({ repoRoot: input.repoRoot, runId, corpusBrowser: input.corpusBrowserByRun?.[runId], gatewaySnapshots: input.gatewaySnapshotsByRun?.[runId], acceptance: input.acceptanceByRun?.[runId] }));
|
|
141
|
+
if (collected.some(row => row.campaignId !== input.plan.campaignId))
|
|
142
|
+
throw Error("frontend summary campaign mismatch");
|
|
143
|
+
return { summary: summarizeFrontendTaskPairs({ ...input, observations: collected.map(row => row.observation) }), collected };
|
|
144
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
6
|
+
import { readDagRunSpec } from "../../workflows/dag/lifecycle.js";
|
|
7
|
+
import { readFrontendSessionBudgetReceipts } from "../../workflows/dag/frontend-session-budget.js";
|
|
8
|
+
import { verifyFrozenSourceUsage } from "../../task/source-prepare/source-provider-budget.js";
|
|
9
|
+
import { frontendSharedArtifactsSchema } from "./frontend-pair-registration.js";
|
|
10
|
+
export async function readFrontendSharedEvidence(input) {
|
|
11
|
+
if (!input.runDirs.length)
|
|
12
|
+
throw Error("shared evidence requires a root run");
|
|
13
|
+
const artifacts = frontendSharedArtifactsSchema.parse(input.artifacts);
|
|
14
|
+
const snapshots = [];
|
|
15
|
+
const read = async (root, name, expected) => {
|
|
16
|
+
const target = path.resolve(root, name);
|
|
17
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: target, label: "frontend shared evidence" });
|
|
18
|
+
if (expected && createHash("sha256").update(bytes).digest("hex") !== expected)
|
|
19
|
+
throw Error(`shared artifact hash mismatch: ${name}`);
|
|
20
|
+
snapshots.push({ root, target, bytes });
|
|
21
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
22
|
+
};
|
|
23
|
+
const values = {};
|
|
24
|
+
for (const key of ["source", "budget", "model", "initialTree"]) {
|
|
25
|
+
if (artifacts[key].sha256 !== input.expected[key])
|
|
26
|
+
throw Error(`shared artifact not registered: ${key}`);
|
|
27
|
+
values[key] = await read(input.repoRoot, artifacts[key].path, artifacts[key].sha256);
|
|
28
|
+
}
|
|
29
|
+
const model = z.object({ model: z.string().min(1), parameters: z.record(z.unknown()).optional() }).strict().parse(values.model);
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
if (model.parameters && Object.keys(model.parameters).length)
|
|
32
|
+
diagnostics.push("model-sampling-parameters-not-observed");
|
|
33
|
+
const expectedTree = z.object({ gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/), changedPaths: z.array(z.unknown()), stableGovernanceInputs: z.array(z.unknown()) }).strict().parse(values.initialTree);
|
|
34
|
+
const checkpoint = await read(input.runDirs[0], ".runtime/workspace-checkpoint-start.json");
|
|
35
|
+
const actualTree = { gitHeadSha: checkpoint.gitHeadSha, changedPaths: checkpoint.changedPaths, stableGovernanceInputs: checkpoint.stableGovernanceInputs ?? [] };
|
|
36
|
+
if (sha256OfCanonicalJson(actualTree) !== sha256OfCanonicalJson(expectedTree))
|
|
37
|
+
throw Error("shared initial tree mismatch");
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
const inspected = new Map();
|
|
40
|
+
const inspect = (receipt) => {
|
|
41
|
+
if (receipt.identity.executionKind !== "model")
|
|
42
|
+
return;
|
|
43
|
+
const key = JSON.stringify([receipt.identity.runId ?? receipt.identity.taskId, receipt.identity.sessionId]);
|
|
44
|
+
const contentHash = sha256OfCanonicalJson(receipt);
|
|
45
|
+
if (inspected.has(key)) {
|
|
46
|
+
if (inspected.get(key) !== contentHash)
|
|
47
|
+
throw Error("conflicting shared model receipt");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
inspected.set(key, contentHash);
|
|
51
|
+
if (receipt.completion !== "complete" || !receipt.attempts.length || !receipt.planned?.capabilities.model)
|
|
52
|
+
diagnostics.push("shared model observation incomplete");
|
|
53
|
+
if (receipt.planned?.capabilities.model && receipt.planned.capabilities.model !== model.model)
|
|
54
|
+
throw Error("shared planned model mismatch");
|
|
55
|
+
for (const attempt of receipt.attempts) {
|
|
56
|
+
if (attempt.model !== model.model)
|
|
57
|
+
throw Error("shared attempt model mismatch");
|
|
58
|
+
if (attempt.completion !== "complete")
|
|
59
|
+
diagnostics.push("shared model observation incomplete");
|
|
60
|
+
attempts++;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const sourceReceipts = new Set();
|
|
64
|
+
for (const runDir of input.runDirs) {
|
|
65
|
+
await read(runDir, "run.json");
|
|
66
|
+
const spec = await readDagRunSpec(runDir);
|
|
67
|
+
if (sha256OfCanonicalJson(spec.sourceBinding ?? null) !== sha256OfCanonicalJson(values.source))
|
|
68
|
+
throw Error("shared source mismatch");
|
|
69
|
+
if (sha256OfCanonicalJson(spec.budget ?? null) !== sha256OfCanonicalJson(values.budget))
|
|
70
|
+
throw Error("shared budget mismatch");
|
|
71
|
+
const state = await read(runDir, "state.json");
|
|
72
|
+
for (const task of spec.tasks) {
|
|
73
|
+
const receipts = await readFrontendSessionBudgetReceipts(runDir, task.id);
|
|
74
|
+
const node = state.nodes?.[task.id];
|
|
75
|
+
if (task.executor === "pi" && node && !["PENDING", "SKIPPED"].includes(node.status) && node.origin?.kind !== "imported" && !receipts.length)
|
|
76
|
+
diagnostics.push(`missing model receipts: ${task.id}`);
|
|
77
|
+
for (const receipt of receipts)
|
|
78
|
+
inspect(receipt);
|
|
79
|
+
}
|
|
80
|
+
if (spec.sourceProviderBudget) {
|
|
81
|
+
const verified = await verifyFrozenSourceUsage(input.repoRoot, spec.sourceProviderBudget);
|
|
82
|
+
diagnostics.push(...verified.diagnostics);
|
|
83
|
+
for (const source of spec.sourceProviderBudget.evidence?.receipts ?? []) {
|
|
84
|
+
if (sourceReceipts.has(source.sha256))
|
|
85
|
+
continue;
|
|
86
|
+
sourceReceipts.add(source.sha256);
|
|
87
|
+
inspect(JSON.parse(source.content));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!attempts)
|
|
92
|
+
diagnostics.push("shared model has no observed attempts");
|
|
93
|
+
// Sampling settings are not yet present in session receipts; matching model
|
|
94
|
+
// names alone cannot prove every parameter used by the SDK/provider.
|
|
95
|
+
if (!diagnostics.includes("model-sampling-parameters-not-observed"))
|
|
96
|
+
diagnostics.push("model-sampling-parameters-not-observed");
|
|
97
|
+
diagnostics.push("provider-resolved-model-not-observed");
|
|
98
|
+
for (const snapshot of snapshots) {
|
|
99
|
+
const latest = await readFileBoundarySafe({ boundaryRoot: snapshot.root, targetPath: snapshot.target, label: "shared evidence recheck" });
|
|
100
|
+
if (!latest.equals(snapshot.bytes))
|
|
101
|
+
throw Error("shared evidence changed during collection");
|
|
102
|
+
}
|
|
103
|
+
return { diagnostics: [...new Set(diagnostics)], model: model.model, observedAttempts: attempts,
|
|
104
|
+
verified: { source: true, budget: true, initialTree: true, modelReference: attempts > 0 && diagnostics.every(item => ["model-sampling-parameters-not-observed", "provider-resolved-model-not-observed"].includes(item)), samplingParameters: false } };
|
|
105
|
+
}
|
|
@@ -265,18 +265,51 @@ const corpusManifestObjectSchema = z
|
|
|
265
265
|
.optional(),
|
|
266
266
|
})
|
|
267
267
|
.strict();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
// The six-case exploratory frontend protocol is distinct from the heterogeneous
|
|
269
|
+
// dogfood corpus. All six are public execution tasks, not held-out claims.
|
|
270
|
+
const frontendCorpusCategories = [
|
|
271
|
+
"local-interaction", "form-validation", "component-reuse",
|
|
272
|
+
"table-filter-pagination", "multi-api-mock-boundary", "shared-context-domains",
|
|
273
|
+
];
|
|
274
|
+
const frontendCorpusFileSchema = z.object({
|
|
275
|
+
path: z.string().min(1).refine(value => !value.includes("\\") && !value.includes(":") &&
|
|
276
|
+
!value.startsWith("/") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."), "corpus artifact must be a normalized relative file path"),
|
|
277
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
278
|
+
}).strict();
|
|
279
|
+
const frontendCorpusFilesSchema = z.array(frontendCorpusFileSchema).min(1).superRefine((files, ctx) => {
|
|
280
|
+
if (new Set(files.map(file => file.path)).size !== files.length)
|
|
281
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "duplicate corpus artifact path" });
|
|
282
|
+
});
|
|
283
|
+
const frontendCorpusTaskSchema = corpusTaskSchema.extend({
|
|
284
|
+
class: z.literal("frontend"), split: z.literal("public"),
|
|
285
|
+
category: z.enum(frontendCorpusCategories),
|
|
286
|
+
requirements: frontendCorpusFilesSchema,
|
|
287
|
+
initialFiles: frontendCorpusFilesSchema,
|
|
288
|
+
oracleFiles: frontendCorpusFilesSchema,
|
|
289
|
+
}).superRefine((task, ctx) => {
|
|
290
|
+
if (task.seeds.length !== 3)
|
|
291
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontend corpus requires three planned repetitions" });
|
|
292
|
+
});
|
|
293
|
+
const frontendCorpusObjectSchema = z.object({
|
|
294
|
+
schemaVersion: z.literal(2), purpose: z.literal("frontend-task-pairs"),
|
|
295
|
+
corpusId: corpusIdSchema, createdAt: z.string().datetime(), description: z.string().optional(),
|
|
296
|
+
tasks: z.array(frontendCorpusTaskSchema).length(6),
|
|
297
|
+
corpusHash: z.string().regex(/^(sha256:)?[a-f0-9]{64}$/i).optional(),
|
|
298
|
+
}).strict();
|
|
299
|
+
function refineFrontendCorpus(manifest, ctx) {
|
|
300
|
+
if (new Set(manifest.tasks.map(task => task.category)).size !== frontendCorpusCategories.length)
|
|
301
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontend corpus requires all six categories exactly once" });
|
|
302
|
+
if (new Set(manifest.tasks.map(task => task.taskRef)).size !== manifest.tasks.length)
|
|
303
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "duplicate frontend corpus taskRef" });
|
|
304
|
+
}
|
|
305
|
+
export const corpusManifestInputSchema = z.union([
|
|
306
|
+
corpusManifestObjectSchema.superRefine(refineCorpusTasks),
|
|
307
|
+
frontendCorpusObjectSchema.superRefine(refineFrontendCorpus),
|
|
308
|
+
]);
|
|
309
|
+
export const corpusManifestSchema = z.union([
|
|
310
|
+
corpusManifestObjectSchema.extend({ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).superRefine(refineCorpusTasks),
|
|
311
|
+
frontendCorpusObjectSchema.extend({ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).superRefine(refineFrontendCorpus),
|
|
312
|
+
]);
|
|
280
313
|
// --- Private Verifier + Campaign (M4) ---
|
|
281
314
|
export const privateVerifierIdSchema = z
|
|
282
315
|
.string()
|