@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.11
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/sessions/index.js +6 -0
- package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
- package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
- package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
- package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
- package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -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 +733 -5782
- 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/checkpoint.js +686 -0
- 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/hybrid/sources.js +1812 -0
- package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
- package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
- package/dist/workflows/dag/hybrid/templates/index.js +8 -0
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
- package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
- package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
- package/dist/workflows/dag/hybrid/types.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +57 -7106
- 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 +38 -1159
- package/dist/workflows/dag/terminal-status.js +502 -0
- 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/dag-execution.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,92 @@
|
|
|
1
|
+
import { assemblePlanPatchFromCommittedFacts } from "./frontend-committed-facts.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4
|
+
const identity = (params) => {
|
|
5
|
+
const p = object(params), entry = object(p.entry ?? p.choice);
|
|
6
|
+
return String(p.id ?? p.scopeId ?? p.requirementId ?? entry.id ?? entry.name ?? entry.purpose ?? entry.state ?? entry.interaction ?? entry.boundary ?? "singleton");
|
|
7
|
+
};
|
|
8
|
+
/** Counts unresolved diagnostic identities, not tool activity. A changed
|
|
9
|
+
* rationale/read/heartbeat cannot resolve the same compiler diagnostic. */
|
|
10
|
+
export function createFrontendPlanProgressGuard() {
|
|
11
|
+
let stop;
|
|
12
|
+
let terminal = false;
|
|
13
|
+
let failures = [];
|
|
14
|
+
const states = [];
|
|
15
|
+
const warned = new Set();
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
return {
|
|
18
|
+
readStop: () => stop,
|
|
19
|
+
acceptTerminal: () => { if (!stop) {
|
|
20
|
+
terminal = true;
|
|
21
|
+
failures = [];
|
|
22
|
+
} },
|
|
23
|
+
subscribe(listener) { listeners.add(listener); if (stop)
|
|
24
|
+
listener(); return () => { listeners.delete(listener); }; },
|
|
25
|
+
observe(tool, params, details, committed, effectiveDigest) {
|
|
26
|
+
if (stop)
|
|
27
|
+
return stop;
|
|
28
|
+
if (terminal)
|
|
29
|
+
return undefined;
|
|
30
|
+
const key = `${tool}:${identity(params)}`;
|
|
31
|
+
if (details?.ok === true) {
|
|
32
|
+
// Only a successful durable correction of the rejected identity (or
|
|
33
|
+
// a successful compile) resolves it. Unrelated writes do not reset it.
|
|
34
|
+
if (tool.startsWith("finalize_") && committed) {
|
|
35
|
+
terminal = true;
|
|
36
|
+
failures = [];
|
|
37
|
+
}
|
|
38
|
+
else if (committed)
|
|
39
|
+
failures = failures.filter(f => f.key !== key);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const stage = typeof details?.validationStage === "string" ? details.validationStage : undefined;
|
|
43
|
+
const completed = Array.isArray(details?.completedValidationStages) ? details.completedValidationStages : [];
|
|
44
|
+
failures = failures.filter(f => !f.stage || !completed.includes(f.stage));
|
|
45
|
+
const error = String(details?.error ?? details?.code ?? "unknown tool failure").replace(/\s+/g, " ").trim();
|
|
46
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([key, details?.code ?? null, error])).digest("hex");
|
|
47
|
+
failures.push({ fingerprint, key, stage });
|
|
48
|
+
const state = `${stage ?? key}:${fingerprint}:${effectiveDigest ?? "unknown"}`;
|
|
49
|
+
states.push(state);
|
|
50
|
+
if (states.length > 8)
|
|
51
|
+
states.shift();
|
|
52
|
+
failures = failures.slice(-12);
|
|
53
|
+
const tail = failures.slice(-6);
|
|
54
|
+
const repeated = failures.filter(f => f.fingerprint === fingerprint).length;
|
|
55
|
+
if (repeated >= 6 || (tail.length === 6 && new Set(tail.map(f => f.fingerprint)).size <= 2) || (states.length === 8 && new Set(states).size <= 2)) {
|
|
56
|
+
stop = { code: "FRONTEND_PLAN_NO_PROGRESS", fingerprint, error: `Unresolved repeated Plan diagnostic: ${error}` };
|
|
57
|
+
for (const listener of listeners)
|
|
58
|
+
listener();
|
|
59
|
+
return stop;
|
|
60
|
+
}
|
|
61
|
+
if (repeated >= 3 && !warned.has(fingerprint)) {
|
|
62
|
+
warned.add(fingerprint);
|
|
63
|
+
return { code: "FRONTEND_PLAN_REPAIR_HINT", fingerprint, error: `The same diagnostic remains unresolved. Correct the named fact before repeating this call: ${error}` };
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Effective facts for the existing node retry ladder. Replacement history and
|
|
70
|
+
* explanation-only edits must not look like a new implementation decision. */
|
|
71
|
+
export function frontendPlanSemanticDigest(records) {
|
|
72
|
+
const current = new Map();
|
|
73
|
+
const strip = (value) => Array.isArray(value) ? value.map(strip) : value && typeof value === "object"
|
|
74
|
+
? Object.fromEntries(Object.entries(value).filter(([key]) => !["rationale", "replaces"].includes(key)).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, strip(item)])) : value;
|
|
75
|
+
const ordinary = records.some(r => ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "mock-api"].includes(String(object(r.fact).kind)));
|
|
76
|
+
if (ordinary) {
|
|
77
|
+
try {
|
|
78
|
+
const patch = assemblePlanPatchFromCommittedFacts(records.map(r => ({ ...r, fact: { ...object(r.fact), origin: "plan" } })));
|
|
79
|
+
if (patch)
|
|
80
|
+
return createHash("sha256").update(JSON.stringify(strip(patch))).digest("hex");
|
|
81
|
+
}
|
|
82
|
+
catch { /* An incomplete ledger still needs a stable diagnostic state. */ }
|
|
83
|
+
}
|
|
84
|
+
for (const record of records) {
|
|
85
|
+
if (record.phase !== "committed")
|
|
86
|
+
continue;
|
|
87
|
+
const fact = object(record.fact);
|
|
88
|
+
const key = `${fact.kind}:${identity(fact)}`;
|
|
89
|
+
current.set(key, strip(fact));
|
|
90
|
+
}
|
|
91
|
+
return createHash("sha256").update(JSON.stringify([...current].sort(([a], [b]) => a.localeCompare(b)))).digest("hex");
|
|
92
|
+
}
|
|
@@ -61,11 +61,14 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
61
61
|
// verification commands / trace. Resetting only verify + its descendants
|
|
62
62
|
// would re-run the same failing checks against the same implementation, so
|
|
63
63
|
// reset from implement: the recovery re-implements and re-verifies.
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: input.
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const ownerRoots = { contract: "frontend-contract-pi", scout: "frontend-scout-pi", plan: "frontend-plan-pi", implementation: "frontend-implement-pi", "verification-config": "frontend-contract-pi" };
|
|
65
|
+
if (input.failureOwner && !ownerRoots[input.failureOwner])
|
|
66
|
+
throw new Error(`FRONTEND_RECOVERY_OWNER_UNSUPPORTED: ${input.failureOwner}; resolve the owning input before retrying`);
|
|
67
|
+
const resetRoot = input.failureOwner ? ownerRoots[input.failureOwner] :
|
|
68
|
+
input.failureSource === "frontend-verify-shell" || input.failureSource === "frontend-review-pi" ? "frontend-implement-pi" :
|
|
69
|
+
input.failureSource === "frontend-design-review-pi" ? "frontend-plan-pi" : input.failureSource;
|
|
70
|
+
if (!input.spec.tasks.some(task => task.id === resetRoot))
|
|
71
|
+
throw new Error(`FRONTEND_RECOVERY_PRODUCER_UNAVAILABLE: ${resetRoot}; this topology has no authorized producer, reproject the task contract instead of importing the same candidate`);
|
|
69
72
|
const resetNodeIds = computeResetClosure(input.spec, resetRoot);
|
|
70
73
|
const resetSet = new Set(resetNodeIds);
|
|
71
74
|
const importedNodeIds = input.spec.tasks
|
|
@@ -77,6 +80,8 @@ function computeFrontendRecoveryPlanForSource(input) {
|
|
|
77
80
|
parentRunId: input.parentRunId,
|
|
78
81
|
requestId: input.requestId,
|
|
79
82
|
failureSource: input.failureSource,
|
|
83
|
+
...(input.failureOwner ? { failureOwner: input.failureOwner } : {}),
|
|
84
|
+
resetRootNodeId: resetRoot,
|
|
80
85
|
resetNodeIds,
|
|
81
86
|
importedNodeIds,
|
|
82
87
|
};
|
|
@@ -298,6 +298,34 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
|
|
|
298
298
|
}
|
|
299
299
|
async function materializeChildStaging(input) {
|
|
300
300
|
const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, recoveryPlan, requestId, childRunId, stagingDir, createdAt, } = input;
|
|
301
|
+
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
302
|
+
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
303
|
+
// upstream is imported as verified parent facts.
|
|
304
|
+
let plan;
|
|
305
|
+
if (recoveryPlan) {
|
|
306
|
+
plan = recoveryPlan;
|
|
307
|
+
}
|
|
308
|
+
else if (recovery.failureSource) {
|
|
309
|
+
plan = computeFrontendRecoveryPlanForSource({
|
|
310
|
+
spec: parentSpec,
|
|
311
|
+
parentRunId,
|
|
312
|
+
requestId,
|
|
313
|
+
failureSource: recovery.failureSource,
|
|
314
|
+
failureOwner: recovery.failureOwner,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
|
|
319
|
+
if (!admissionRead.ok) {
|
|
320
|
+
throw new Error(admissionRead.reason);
|
|
321
|
+
}
|
|
322
|
+
plan = computeFrontendRecoveryPlan({
|
|
323
|
+
spec: parentSpec,
|
|
324
|
+
parentRunId,
|
|
325
|
+
requestId,
|
|
326
|
+
result: admissionRead.result,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
301
329
|
await prepareRunDir(stagingDir);
|
|
302
330
|
// Design-review rejection must be visible to the reset planner. Recovery
|
|
303
331
|
// children otherwise copy the old spec and only rerun design review, which
|
|
@@ -305,6 +333,8 @@ async function materializeChildStaging(input) {
|
|
|
305
333
|
// findings into the child spec as ordinary rerun feedback.
|
|
306
334
|
let childSpec = parentSpec;
|
|
307
335
|
if (recovery.failureSource === "frontend-plan-pi" ||
|
|
336
|
+
recovery.failureSource === "frontend-design-review-pi" ||
|
|
337
|
+
plan.resetRootNodeId === "frontend-contract-pi" ||
|
|
308
338
|
recovery.failureSource === "frontend-review-pi") {
|
|
309
339
|
const designRecovery = parentState.terminalReason === "design-request-changes";
|
|
310
340
|
const reviewRecovery = recovery.failureSource === "frontend-review-pi" ||
|
|
@@ -331,12 +361,13 @@ async function materializeChildStaging(input) {
|
|
|
331
361
|
}
|
|
332
362
|
}
|
|
333
363
|
catch (error) {
|
|
334
|
-
|
|
335
|
-
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
336
|
-
}
|
|
364
|
+
throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
337
365
|
}
|
|
338
366
|
if (feedback) {
|
|
339
367
|
childSpec = { ...parentSpec, rerunFeedback: feedback };
|
|
368
|
+
if (plan.resetRootNodeId === "frontend-contract-pi") {
|
|
369
|
+
childSpec = { ...childSpec, tasks: childSpec.tasks.map(task => task.id !== plan.resetRootNodeId ? task : { ...task, subtask_prompt: `${task.subtask_prompt}\n\n<contract_recovery_diagnostic>\nParent run: ${parentRunId}\nSource node: ${feedback.sourceNodeId}\nRe-extract only from the unchanged frozen task source. These parent findings are diagnostic hypotheses, not new requirements or authorization. Do not import the parent's Plan as Contract facts. Report unresolved source ambiguity rather than inventing a decision.\n${feedback.feedbackText}\n</contract_recovery_diagnostic>` }) };
|
|
370
|
+
}
|
|
340
371
|
}
|
|
341
372
|
else if (designRecovery || reviewRecovery) {
|
|
342
373
|
// A design/review rejection cannot be safely retried without its typed
|
|
@@ -346,33 +377,6 @@ async function materializeChildStaging(input) {
|
|
|
346
377
|
}
|
|
347
378
|
}
|
|
348
379
|
await writeJsonVerified(stagingDir, "run.json", childSpec);
|
|
349
|
-
// Phase 5: writer transient partial-write recovery resets the writer subtree
|
|
350
|
-
// (frontend-implement-pi) instead of the candidate producer; everything
|
|
351
|
-
// upstream is imported as verified parent facts.
|
|
352
|
-
let plan;
|
|
353
|
-
if (recoveryPlan) {
|
|
354
|
-
plan = recoveryPlan;
|
|
355
|
-
}
|
|
356
|
-
else if (recovery.failureSource) {
|
|
357
|
-
plan = computeFrontendRecoveryPlanForSource({
|
|
358
|
-
spec: parentSpec,
|
|
359
|
-
parentRunId,
|
|
360
|
-
requestId,
|
|
361
|
-
failureSource: recovery.failureSource,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
const admissionRead = await readFrontendWriterAdmissionResult(parentRunDir);
|
|
366
|
-
if (!admissionRead.ok) {
|
|
367
|
-
throw new Error(admissionRead.reason);
|
|
368
|
-
}
|
|
369
|
-
plan = computeFrontendRecoveryPlan({
|
|
370
|
-
spec: parentSpec,
|
|
371
|
-
parentRunId,
|
|
372
|
-
requestId,
|
|
373
|
-
result: admissionRead.result,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
380
|
const { ranks } = topoSortToRanks(childSpec);
|
|
377
381
|
const resetSet = new Set(plan.resetNodeIds);
|
|
378
382
|
const importedSet = new Set(plan.importedNodeIds);
|
|
@@ -408,16 +412,16 @@ async function materializeChildStaging(input) {
|
|
|
408
412
|
}
|
|
409
413
|
// Canonical contract + writer-admission identity facts are copied alongside
|
|
410
414
|
// the per-node facts so reset nodes can re-read them (AC-4).
|
|
411
|
-
const canonicalContract = await copyArtifactVerified({
|
|
415
|
+
const canonicalContract = plan.importedNodeIds.some(id => ["frontend-design-policy-shell", "frontend-contract-import-shell"].includes(id)) ? await copyArtifactVerified({
|
|
412
416
|
parentRunDir,
|
|
413
417
|
newRunDir: stagingDir,
|
|
414
418
|
relativePath: resolveCanonicalContractRelPath(childSpec),
|
|
415
|
-
});
|
|
416
|
-
const writerAdmissionResult = await copyArtifactVerified({
|
|
419
|
+
}) : undefined;
|
|
420
|
+
const writerAdmissionResult = plan.importedNodeIds.includes("frontend-writer-admission-shell") ? await copyArtifactVerified({
|
|
417
421
|
parentRunDir,
|
|
418
422
|
newRunDir: stagingDir,
|
|
419
423
|
relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
|
|
420
|
-
});
|
|
424
|
+
}) : undefined;
|
|
421
425
|
// Imported nodes keep their parent FINISHED record, so the run-owned files
|
|
422
426
|
// their bodies wrote outside the node directory travel with them (smoke r31:
|
|
423
427
|
// a review-triggered recovery reached review-context with no worktree
|
|
@@ -470,6 +474,7 @@ async function materializeChildStaging(input) {
|
|
|
470
474
|
}
|
|
471
475
|
}
|
|
472
476
|
const childState = {
|
|
477
|
+
...(parentSpec.sourceProviderBudget ? { sourceProviderBudget: structuredClone(parentSpec.sourceProviderBudget) } : {}),
|
|
473
478
|
...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
|
|
474
479
|
...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
|
|
475
480
|
version: 1,
|
|
@@ -506,6 +511,8 @@ async function materializeChildStaging(input) {
|
|
|
506
511
|
...(recovery.failureSource
|
|
507
512
|
? { failureSource: recovery.failureSource }
|
|
508
513
|
: {}),
|
|
514
|
+
...(plan.failureOwner ? { failureOwner: plan.failureOwner } : {}),
|
|
515
|
+
...(plan.resetRootNodeId ? { resetRootNodeId: plan.resetRootNodeId } : {}),
|
|
509
516
|
revision: 0,
|
|
510
517
|
},
|
|
511
518
|
...(parentState.evaluation
|
|
@@ -518,14 +525,8 @@ async function materializeChildStaging(input) {
|
|
|
518
525
|
parentRunId,
|
|
519
526
|
requestId,
|
|
520
527
|
createdAt,
|
|
521
|
-
canonicalContract: {
|
|
522
|
-
|
|
523
|
-
sha256: canonicalContract.sha256,
|
|
524
|
-
},
|
|
525
|
-
writerAdmissionResult: {
|
|
526
|
-
relativePath: writerAdmissionResult.destinationRelativePath,
|
|
527
|
-
sha256: writerAdmissionResult.sha256,
|
|
528
|
-
},
|
|
528
|
+
...(canonicalContract ? { canonicalContract: { relativePath: canonicalContract.destinationRelativePath, sha256: canonicalContract.sha256 } } : {}),
|
|
529
|
+
...(writerAdmissionResult ? { writerAdmissionResult: { relativePath: writerAdmissionResult.destinationRelativePath, sha256: writerAdmissionResult.sha256 } } : {}),
|
|
529
530
|
importedRunArtifacts,
|
|
530
531
|
importedFacts,
|
|
531
532
|
};
|
|
@@ -590,7 +591,8 @@ export async function stageRecoveryChild(options) {
|
|
|
590
591
|
recovery.recoveryRootRunId !== intent.recoveryRootRunId ||
|
|
591
592
|
recovery.attemptIndex !== intent.attemptIndex ||
|
|
592
593
|
recovery.continuationCount !== intent.continuationCount ||
|
|
593
|
-
recovery.failureSource !== intent.failureSource
|
|
594
|
+
recovery.failureSource !== intent.failureSource ||
|
|
595
|
+
recovery.failureOwner !== intent.failureOwner || recovery.resetRootNodeId !== intent.resetRootNodeId) {
|
|
594
596
|
throw new Error("frontend recovery intent replay binding mismatch");
|
|
595
597
|
}
|
|
596
598
|
}
|
|
@@ -620,7 +622,8 @@ export async function stageRecoveryChild(options) {
|
|
|
620
622
|
if (options.recoveryPlan) {
|
|
621
623
|
if (options.recoveryPlan.parentRunId !== parentRunId ||
|
|
622
624
|
options.recoveryPlan.requestId !== requestId ||
|
|
623
|
-
options.recoveryPlan.failureSource !== recovery.failureSource
|
|
625
|
+
options.recoveryPlan.failureSource !== recovery.failureSource ||
|
|
626
|
+
options.recoveryPlan.failureOwner !== recovery.failureOwner || options.recoveryPlan.resetRootNodeId !== recovery.resetRootNodeId) {
|
|
624
627
|
throw new Error("frontend recovery plan binding mismatch");
|
|
625
628
|
}
|
|
626
629
|
const expectedPlan = computeFrontendRecoveryPlanForSource({
|
|
@@ -628,9 +631,11 @@ export async function stageRecoveryChild(options) {
|
|
|
628
631
|
parentRunId,
|
|
629
632
|
requestId,
|
|
630
633
|
failureSource: recovery.failureSource,
|
|
634
|
+
failureOwner: recovery.failureOwner,
|
|
631
635
|
});
|
|
632
|
-
if (
|
|
633
|
-
JSON.stringify(
|
|
636
|
+
if (expectedPlan.resetRootNodeId !== options.recoveryPlan.resetRootNodeId ||
|
|
637
|
+
JSON.stringify(expectedPlan.resetNodeIds) !==
|
|
638
|
+
JSON.stringify(options.recoveryPlan.resetNodeIds) ||
|
|
634
639
|
JSON.stringify(expectedPlan.importedNodeIds) !==
|
|
635
640
|
JSON.stringify(options.recoveryPlan.importedNodeIds)) {
|
|
636
641
|
throw new Error("frontend recovery plan drifted from parent DAG");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const repairAssertionSchema = z.discriminatedUnion("check", [
|
|
3
|
+
z.object({ check: z.literal("file-absent"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
|
|
4
|
+
z.object({ check: z.literal("state-present"), token: z.string().min(1).max(512), source: z.string().max(4096) }).strict(),
|
|
5
|
+
z.object({ check: z.literal("vt-changed"), token: z.string().min(1).max(512), source: z.string().max(4096), expectedRequirementIds: z.array(z.string().min(1)).min(1).max(128) }).strict(),
|
|
6
|
+
]);
|
|
7
|
+
/** Invalid optional assertions never erase a blocking finding or its verdict. */
|
|
8
|
+
export function sanitizeRepairAssertions(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return { assertions: [], diagnostics: [] };
|
|
11
|
+
if (!Array.isArray(value))
|
|
12
|
+
return { assertions: [], diagnostics: ["repairAssertions must be an array; semantic review remains required"] };
|
|
13
|
+
const assertions = [], diagnostics = [];
|
|
14
|
+
for (const [index, raw] of value.slice(0, 32).entries()) {
|
|
15
|
+
const parsed = repairAssertionSchema.safeParse(raw);
|
|
16
|
+
if (parsed.success)
|
|
17
|
+
assertions.push(parsed.data);
|
|
18
|
+
else
|
|
19
|
+
diagnostics.push(`repairAssertions[${index}] invalid; semantic review remains required`);
|
|
20
|
+
}
|
|
21
|
+
if (value.length > 32)
|
|
22
|
+
diagnostics.push("repairAssertions exceeds 32 items; excess items require semantic review");
|
|
23
|
+
return { assertions, diagnostics };
|
|
24
|
+
}
|
|
25
|
+
/** Review annotations cannot authorize deletion or invent requirements/states. */
|
|
26
|
+
export function authorizeRepairAssertions(assertions, input) {
|
|
27
|
+
const files = new Set(input.parent.targets?.files ?? []);
|
|
28
|
+
const required = new Set([...(input.parent.targets?.requiredDeliverables ?? []), ...(input.requiredDeliverables ?? [])]);
|
|
29
|
+
const states = new Set(input.stateIds);
|
|
30
|
+
const targets = new Set(input.parent.verificationTargets?.map(target => String(target.id)) ?? []);
|
|
31
|
+
return assertions.flatMap(assertion => {
|
|
32
|
+
const permitted = assertion.check === "file-absent" ? files.has(assertion.token) && !required.has(assertion.token) :
|
|
33
|
+
assertion.check === "state-present" ? states.has(assertion.token) : targets.has(assertion.token) && assertion.expectedRequirementIds.every(id => input.requirementIds.includes(id));
|
|
34
|
+
return permitted ? [] : [`${assertion.check}: ${assertion.token} is outside frozen repair authority or contradicts a required delivery`];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
3
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
5
6
|
import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
|
|
@@ -244,6 +245,31 @@ function assertReviewEvidence(input) {
|
|
|
244
245
|
throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
|
|
245
246
|
}
|
|
246
247
|
}
|
|
248
|
+
/** Snapshot only exact behavior-test paths already named by the frozen contract. */
|
|
249
|
+
export async function captureFrontendReviewTestSources(input) {
|
|
250
|
+
const sources = [];
|
|
251
|
+
for (const [index, file] of [...new Set(input.files)].entries()) {
|
|
252
|
+
const targetPath = path.resolve(input.workspaceRoot, file);
|
|
253
|
+
const exists = await lstat(targetPath).catch((error) => { if (error.code === "ENOENT")
|
|
254
|
+
return undefined; throw error; });
|
|
255
|
+
if (!exists) {
|
|
256
|
+
sources.push({ file, status: "unavailable", scopes: [] });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: input.workspaceRoot, targetPath, label: "frontend review test source" });
|
|
260
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
261
|
+
const text = bytes.toString("utf8");
|
|
262
|
+
const scopes = [];
|
|
263
|
+
// Small text slices bound each model input; all slices are retained.
|
|
264
|
+
for (let offset = 0; offset < Math.max(1, text.length); offset += 2000) {
|
|
265
|
+
const relative = `contracts/test-source-${index}-${offset / 2000}.json`;
|
|
266
|
+
await writeDagRunJsonArtifact(input.runDir, relative, { schemaVersion: 1, file, sourceSha256: sha256, offset, totalCharacters: text.length, text: text.slice(offset, offset + 2000) });
|
|
267
|
+
scopes.push({ path: relative, sha256: createHash("sha256").update(await readFile(path.join(input.runDir, relative))).digest("hex") });
|
|
268
|
+
}
|
|
269
|
+
sources.push({ file, status: "captured", sha256, scopes });
|
|
270
|
+
}
|
|
271
|
+
return sources;
|
|
272
|
+
}
|
|
247
273
|
export async function runFrontendReviewContextGate(input) {
|
|
248
274
|
const contractRelativePath = "contracts/frontend-implementation-contract.json";
|
|
249
275
|
let contractRaw;
|
|
@@ -284,6 +310,7 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
284
310
|
const verifyFailure = await readOptionalVerifyFailure(input.runDir);
|
|
285
311
|
const lintAssessment = await readOptionalLintAssessment(input.runDir);
|
|
286
312
|
const verificationEvidence = await readVerificationEvidence(input.runDir);
|
|
313
|
+
const testSources = await captureFrontendReviewTestSources({ ...input, files: parsedContract.data.verificationTargets.filter(target => target.mode === "behavior").map(target => target.file) });
|
|
287
314
|
const cumulativeDiff = await captureCumulativeDiffContext({
|
|
288
315
|
runDir: input.runDir,
|
|
289
316
|
workspaceRoot: input.workspaceRoot,
|
|
@@ -320,6 +347,7 @@ export async function runFrontendReviewContextGate(input) {
|
|
|
320
347
|
},
|
|
321
348
|
verificationTrace,
|
|
322
349
|
verificationEvidence,
|
|
350
|
+
testSources,
|
|
323
351
|
// Protocol-legal lint vocabulary; `lintConfigured: false` tells the
|
|
324
352
|
// reviewer that this task declares no lint surface at all, so
|
|
325
353
|
// `unavailable` reads as "not applicable", not "missing evidence".
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseFrontendTestExecutionReport } from "./frontend-test-execution-evidence.js";
|
|
2
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
1
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
4
|
import { readFile, realpath, lstat } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
@@ -11,9 +13,10 @@ const isArtifactReference = (value) => isRecord(value)
|
|
|
11
13
|
&& typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256);
|
|
12
14
|
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
13
15
|
/** The section index is navigation; reconstruct coverage against the canonical source before using it. */
|
|
14
|
-
export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
|
|
16
|
+
export async function loadFrontendReviewScopes(runDir, phase, targetBytes, workspaceRoot) {
|
|
15
17
|
const root = await realpath(runDir);
|
|
16
18
|
const files = new Map();
|
|
19
|
+
const sourceChecks = [];
|
|
17
20
|
async function read(relative, expected) {
|
|
18
21
|
const absolute = path.resolve(root, relative);
|
|
19
22
|
if (!absolute.startsWith(root + path.sep) || (await lstat(absolute)).isSymbolicLink() || !(await realpath(absolute)).startsWith(root + path.sep))
|
|
@@ -103,11 +106,88 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
|
|
|
103
106
|
if (diffIndex.patchSha256 !== payload.diff.patchSha256)
|
|
104
107
|
throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review diff binding");
|
|
105
108
|
scopes.push(context);
|
|
109
|
+
const reportCoverage = new Map();
|
|
110
|
+
const reportedTests = [];
|
|
111
|
+
for (const ref of payload.verificationTrace?.testExecutionEvidence ?? []) {
|
|
112
|
+
if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256))
|
|
113
|
+
throw invalidScope("invalid test execution reference");
|
|
114
|
+
const excerpt = await read(ref.path, ref.sha256);
|
|
115
|
+
const data = JSON.parse(excerpt.text);
|
|
116
|
+
if (data.reportSha256 !== ref.reportSha256)
|
|
117
|
+
throw invalidScope("test execution report binding mismatch");
|
|
118
|
+
const coverage = reportCoverage.get(ref.reportPath) ?? { count: 0, total: data.total };
|
|
119
|
+
if (!Number.isInteger(data.total) || data.total < 1 || data.total !== coverage.total || data.offset !== coverage.count || !Array.isArray(data.tests) || !data.tests.length || data.tests.length > 24)
|
|
120
|
+
throw invalidScope("test report excerpt coverage invalid");
|
|
121
|
+
coverage.count += data.tests.length;
|
|
122
|
+
reportCoverage.set(ref.reportPath, coverage);
|
|
123
|
+
const report = await read(ref.reportPath, ref.reportSha256);
|
|
124
|
+
if (workspaceRoot) {
|
|
125
|
+
if (typeof data.cwd !== "string" || !Number.isInteger(data.offset) || data.offset < 0)
|
|
126
|
+
throw invalidScope("invalid test report excerpt coordinates");
|
|
127
|
+
const rootPath = await realpath(workspaceRoot);
|
|
128
|
+
const cwdPath = await realpath(path.resolve(workspaceRoot, data.cwd));
|
|
129
|
+
if (cwdPath !== rootPath && !cwdPath.startsWith(rootPath + path.sep))
|
|
130
|
+
throw invalidScope("test report cwd escaped workspace");
|
|
131
|
+
const actual = parseFrontendTestExecutionReport(JSON.parse(report.text), cwdPath, rootPath);
|
|
132
|
+
const expected = actual.slice(data.offset, data.offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) }));
|
|
133
|
+
if (data.total !== actual.length || !expected.length || sha256OfCanonicalJson(expected) !== sha256OfCanonicalJson(data.tests))
|
|
134
|
+
throw invalidScope("test report excerpt differs from actual runner report");
|
|
135
|
+
}
|
|
136
|
+
if (data.tests.some((test) => !isRecord(test) || typeof test.file !== "string" || typeof test.status !== "string"))
|
|
137
|
+
throw invalidScope("invalid test report records");
|
|
138
|
+
reportedTests.push(...data.tests);
|
|
139
|
+
scopes.push(excerpt);
|
|
140
|
+
}
|
|
141
|
+
if ([...reportCoverage.values()].some(coverage => coverage.count !== coverage.total))
|
|
142
|
+
throw invalidScope("test report excerpts incomplete");
|
|
143
|
+
const allowedTests = new Set((canonical.verificationTargets ?? []).filter((target) => target.mode === "behavior").map((target) => target.file));
|
|
144
|
+
// A trace may claim execution even when its entire report reference list
|
|
145
|
+
// was omitted. Require the actual, hash-checked reports for every behavior
|
|
146
|
+
// file before permitting review of the corresponding implementation.
|
|
147
|
+
for (const file of allowedTests) {
|
|
148
|
+
const normalized = path.posix.normalize(String(file).replace(/\\/g, "/"));
|
|
149
|
+
const matching = reportedTests.filter(test => path.posix.normalize(test.file) === normalized);
|
|
150
|
+
if (!workspaceRoot || !matching.length || matching.some(test => test.status !== "passed"))
|
|
151
|
+
throw invalidScope(`behavior test report coverage incomplete or not passed: ${String(file)}`);
|
|
152
|
+
}
|
|
153
|
+
if (allowedTests.size && (!Array.isArray(payload.testSources) || payload.testSources.length !== allowedTests.size || new Set(payload.testSources.map((source) => source.file)).size !== allowedTests.size))
|
|
154
|
+
throw invalidScope("contract test source coverage incomplete");
|
|
155
|
+
for (const source of payload.testSources ?? []) {
|
|
156
|
+
if (!allowedTests.has(source.file))
|
|
157
|
+
throw invalidScope("test source is outside frozen contract");
|
|
158
|
+
if (source.status === "unavailable")
|
|
159
|
+
continue;
|
|
160
|
+
if (source.status !== "captured" || !/^[a-f0-9]{64}$/.test(source.sha256) || !Array.isArray(source.scopes) || !source.scopes.length)
|
|
161
|
+
throw invalidScope("invalid test source reference");
|
|
162
|
+
let content = "";
|
|
163
|
+
for (const ref of source.scopes) {
|
|
164
|
+
if (!isArtifactReference(ref))
|
|
165
|
+
throw invalidScope("invalid test source slice");
|
|
166
|
+
const excerpt = await read(ref.path, ref.sha256);
|
|
167
|
+
const data = JSON.parse(excerpt.text);
|
|
168
|
+
if (data.file !== source.file || data.sourceSha256 !== source.sha256 || data.offset !== content.length || typeof data.text !== "string")
|
|
169
|
+
throw invalidScope("test source slice binding mismatch");
|
|
170
|
+
content += data.text;
|
|
171
|
+
scopes.push(excerpt);
|
|
172
|
+
}
|
|
173
|
+
if (hash(content) !== source.sha256)
|
|
174
|
+
throw invalidScope("test source snapshot incomplete");
|
|
175
|
+
if (workspaceRoot) {
|
|
176
|
+
const check = async () => {
|
|
177
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: workspaceRoot, targetPath: path.resolve(workspaceRoot, source.file), label: "frontend review test source" });
|
|
178
|
+
if (createHash("sha256").update(bytes).digest("hex") !== source.sha256)
|
|
179
|
+
throw Error(`FRONTEND_REVIEW_SCOPE_DRIFT: ${source.file}`);
|
|
180
|
+
};
|
|
181
|
+
await check();
|
|
182
|
+
sourceChecks.push(check);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
106
185
|
for (const hunk of diffIndex.hunks)
|
|
107
186
|
scopes.push(await read(hunk.patchPath, hunk.sha256));
|
|
108
187
|
}
|
|
109
188
|
return { scopes, digest: sha256OfCanonicalJson([...files]), validate: async () => { for (const [relative, expected] of files)
|
|
110
|
-
await read(relative, expected);
|
|
189
|
+
await read(relative, expected); for (const check of sourceChecks)
|
|
190
|
+
await check(); } };
|
|
111
191
|
}
|
|
112
192
|
/** Checkpoints attest explicit model completion of already supplied, hash-bound scopes. */
|
|
113
193
|
export function createFrontendReviewScopeProtocol(input) {
|
|
@@ -118,6 +198,16 @@ export function createFrontendReviewScopeProtocol(input) {
|
|
|
118
198
|
return {
|
|
119
199
|
setActiveScope(ids) { active = new Set(ids); },
|
|
120
200
|
completedScopeIds,
|
|
201
|
+
findingScopeIds(previous) {
|
|
202
|
+
if (!input.inventory)
|
|
203
|
+
return undefined;
|
|
204
|
+
const ids = previous ?? [...active];
|
|
205
|
+
if (!ids.length || ids.some(id => !active.has(id) || !input.inventory.scopes.some(scope => scope.id === id)))
|
|
206
|
+
throw Object.assign(Error("Findings require their original active review scope"), { code: "FRONTEND_INPUT_SCOPE_VIOLATION" });
|
|
207
|
+
if (ids.some(id => completedScopeIds().has(id)))
|
|
208
|
+
throw Object.assign(Error("A completed review scope cannot be changed; restart its owning review"), { code: "FRONTEND_REVIEW_SCOPE_COMPLETED" });
|
|
209
|
+
return [...ids];
|
|
210
|
+
},
|
|
121
211
|
committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
|
|
122
212
|
assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
|
|
123
213
|
throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
|
|
@@ -55,7 +55,12 @@ function isExplicitSharedExclusion(clause, pattern) {
|
|
|
55
55
|
const prefix = clause.slice(0, match.index).trim();
|
|
56
56
|
const suffix = clause.slice(match.index + match[0].length).trim();
|
|
57
57
|
const actionInSignal = ["new-route", "new-dependency"].includes(pattern.id);
|
|
58
|
-
|
|
58
|
+
// A coordinated toolchain object remains under the same explicit negation.
|
|
59
|
+
// Match the entire suffix: qualifications ("unless..." / "除非...") and
|
|
60
|
+
// consequences must still retain the signal.
|
|
61
|
+
const excludedToolchainObject = pattern.id === "new-dependency" &&
|
|
62
|
+
/^(?:或|和|及|与)\s*(?:构建|打包)(?:系统|工具|流程)$/.test(suffix);
|
|
63
|
+
if (suffix && !excludedToolchainObject && !(actionInSignal && /^no$/i.test(prefix) && /^is\s+(?:required|needed)$/i.test(suffix)))
|
|
59
64
|
return false;
|
|
60
65
|
if (/^(?:不涉及|不(?:修改|变更|调整|改变|新增|添加)|无需(?:修改|变更|调整|改变|新增|添加)|no\s+changes?\s+to|(?:do\s+not|don't)\s+(?:change|modify))$/i.test(prefix))
|
|
61
66
|
return true;
|
|
@@ -169,13 +174,13 @@ export function classifyFrontendRisk(input) {
|
|
|
169
174
|
profile.includes("supervised") ||
|
|
170
175
|
/\bsupervised\b/i.test(blob);
|
|
171
176
|
for (const pattern of HIGH_RISK_PATTERNS) {
|
|
172
|
-
if (
|
|
177
|
+
if (hasAffirmativeSharedSignal(blob, pattern))
|
|
173
178
|
signals.push(pattern.id);
|
|
174
179
|
}
|
|
175
180
|
const paths = input.allowedPaths ?? [];
|
|
176
181
|
const spread = pathSpreadScore(paths);
|
|
177
182
|
if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
|
|
178
|
-
if (
|
|
183
|
+
if (hasAffirmativeSharedSignal(blob, { id: "new-dependency", re: DEPENDENCY_INSTALL_LANGUAGE })) {
|
|
179
184
|
signals.push("new-dependency");
|
|
180
185
|
}
|
|
181
186
|
else {
|