@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
2
|
+
import { capabilityBoundarySchema } from "../../task/config-types.js";
|
|
1
3
|
import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { readFileSync } from "node:fs";
|
|
@@ -195,6 +197,20 @@ export function isRecognizedFrontendVerifyCommandText(command) {
|
|
|
195
197
|
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
196
198
|
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
|
|
197
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Repo-level shell scripts whose name marks them as governance/static checks
|
|
202
|
+
* rather than test runners: `scripts/check-*.sh`, `scripts/ci.sh`,
|
|
203
|
+
* `scripts/ci-governance.sh` and friends. The behavior lane is test-observed
|
|
204
|
+
* at writer admission, so these must not be admitted there — they cannot emit
|
|
205
|
+
* a test report.
|
|
206
|
+
*
|
|
207
|
+
* A test word in the name still wins, because the test-semantics check runs
|
|
208
|
+
* earlier in `classifyFrontendVerifyCommandText`, so a real test script such as
|
|
209
|
+
* `scripts/run-tests.sh` or `scripts/check-tests.sh` is unaffected.
|
|
210
|
+
*/
|
|
211
|
+
const VERIFY_STATIC_SCRIPT_PATH_RE = /\bscripts[\\/]+(?:ci(?:-governance|-tests)?|check-[a-z0-9-]+)\.sh\b/i;
|
|
212
|
+
/** A shell script whose own name marks it as a test runner. */
|
|
213
|
+
const VERIFY_BEHAVIOR_SCRIPT_PATH_RE = /\bscripts[\\/]+[a-z0-9-]*(?:tests?|specs?|e2e)[a-z0-9-]*\.sh\b/i;
|
|
198
214
|
/**
|
|
199
215
|
* Classify a raw verification command text into the runtime-owned mode.
|
|
200
216
|
* Shared single implementation used by DAG generation (lane split) and
|
|
@@ -207,6 +223,14 @@ export function isRecognizedFrontendVerifyCommandText(command) {
|
|
|
207
223
|
* static scripts (`npm run check`, `check:types`) and leading binaries
|
|
208
224
|
* (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
|
|
209
225
|
* website run build`) are skipped when locating the script.
|
|
226
|
+
*
|
|
227
|
+
* Returns `undefined` for a command whose text carries no recognizable
|
|
228
|
+
* verification semantics. The previous fallback returned `"behavior"`, which
|
|
229
|
+
* silently pushed unknown commands into the strictest lane: the behavior lane
|
|
230
|
+
* is test-observed at writer admission, so a command that cannot emit a test
|
|
231
|
+
* report blocked admission deterministically with no way for the model to
|
|
232
|
+
* clear it. Callers must now decide explicitly instead of inheriting a lane
|
|
233
|
+
* from an unrecognized string.
|
|
210
234
|
*/
|
|
211
235
|
export function classifyFrontendVerifyCommandText(command) {
|
|
212
236
|
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
|
|
@@ -220,16 +244,24 @@ export function classifyFrontendVerifyCommandText(command) {
|
|
|
220
244
|
}
|
|
221
245
|
return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
|
|
222
246
|
? "static"
|
|
223
|
-
:
|
|
247
|
+
: undefined;
|
|
224
248
|
}
|
|
225
249
|
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
226
250
|
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
251
|
+
// A test word in the invoked script's own name wins over the static
|
|
252
|
+
// governance pattern: `scripts/check-tests.sh` runs tests even though it
|
|
253
|
+
// starts with `check-`, so it must stay in the test-observed lane.
|
|
254
|
+
if (VERIFY_BEHAVIOR_SCRIPT_PATH_RE.test(command)) {
|
|
255
|
+
return "behavior";
|
|
256
|
+
}
|
|
227
257
|
if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
228
258
|
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
229
|
-
|
|
259
|
+
VERIFY_STATIC_SCRIPT_PATH_RE.test(command)) {
|
|
230
260
|
return "static";
|
|
231
261
|
}
|
|
232
|
-
|
|
262
|
+
// Unrecognized: no lane is inferred. Callers must require an operator
|
|
263
|
+
// declaration rather than defaulting into the test-observed behavior lane.
|
|
264
|
+
return undefined;
|
|
233
265
|
}
|
|
234
266
|
export function slugFrontendVerifyCommandId(label, taken) {
|
|
235
267
|
const slug = label
|
|
@@ -591,23 +623,22 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
|
|
|
591
623
|
try {
|
|
592
624
|
raw = await readFile(specPath, "utf8");
|
|
593
625
|
}
|
|
594
|
-
catch {
|
|
595
|
-
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (error.code === "ENOENT")
|
|
628
|
+
return undefined;
|
|
629
|
+
throw error;
|
|
596
630
|
}
|
|
597
631
|
let spec;
|
|
598
632
|
try {
|
|
599
633
|
spec = JSON.parse(raw);
|
|
600
634
|
}
|
|
601
635
|
catch {
|
|
602
|
-
|
|
636
|
+
throw new Error("capability-boundary-source-invalid: run.json is malformed");
|
|
603
637
|
}
|
|
604
638
|
for (const task of spec.tasks ?? []) {
|
|
605
639
|
const boundary = task.shell?.frontendDesignPolicy?.capabilityBoundary;
|
|
606
|
-
if (boundary
|
|
607
|
-
(
|
|
608
|
-
(boundary.forbidden?.length ?? 0) > 0)) {
|
|
609
|
-
return boundary;
|
|
610
|
-
}
|
|
640
|
+
if (boundary !== undefined)
|
|
641
|
+
return capabilityBoundarySchema.parse(boundary);
|
|
611
642
|
}
|
|
612
643
|
return undefined;
|
|
613
644
|
}
|
|
@@ -617,44 +648,67 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
|
|
|
617
648
|
export function composeDependencyPolicyWithBoundary(policy, boundary) {
|
|
618
649
|
if (!boundary)
|
|
619
650
|
return policy;
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
651
|
+
const frozen = capabilityBoundarySchema.parse(boundary);
|
|
652
|
+
// Replace only our explicit complete block; a prose mention never grants
|
|
653
|
+
// ownership of the author's following constraints.
|
|
654
|
+
const begin = "<!-- frontend-capability-boundary:start -->";
|
|
655
|
+
const end = "<!-- frontend-capability-boundary:end -->";
|
|
656
|
+
const source = policy ?? "";
|
|
657
|
+
const start = source.indexOf(begin);
|
|
658
|
+
const finish = start < 0 ? -1 : source.indexOf(end, start + begin.length);
|
|
659
|
+
const prefix = (finish < 0 ? source : source.slice(0, start) + source.slice(finish + end.length)).trim();
|
|
660
|
+
const parts = [
|
|
661
|
+
frozen.allowed?.length ? `仅可使用 ${frozen.allowed.join("、")}` : undefined,
|
|
662
|
+
frozen.forbidden?.length ? `禁止 ${frozen.forbidden.join("、")}` : undefined,
|
|
663
|
+
frozen.requiredClauses?.length ? `必须满足:${frozen.requiredClauses.join(";")}` : undefined,
|
|
664
|
+
].filter(Boolean);
|
|
665
|
+
const sentence = `${CAPABILITY_BOUNDARY_MARKER}:${parts.join(";")}。`;
|
|
666
|
+
const block = `${begin}\n${sentence}\n${end}`;
|
|
667
|
+
return prefix ? `${prefix}\n${block}` : block;
|
|
631
668
|
}
|
|
632
669
|
/** Read the generation-frozen requirement→file ownership from run.json
|
|
633
|
-
* (frontendDesignPolicy spec)
|
|
634
|
-
export async function deriveRequirementOwnershipFromRun(runDir) {
|
|
670
|
+
* (frontendDesignPolicy spec), bound to the canonical source identity. */
|
|
671
|
+
export async function deriveRequirementOwnershipFromRun(runDir, expectedBinding) {
|
|
635
672
|
const specPath = path.join(runDir, "run.json");
|
|
636
673
|
let raw;
|
|
637
674
|
try {
|
|
638
675
|
raw = await readFile(specPath, "utf8");
|
|
639
676
|
}
|
|
640
|
-
catch {
|
|
641
|
-
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (error.code === "ENOENT" && (!expectedBinding || !isDagSourceBindingV2(expectedBinding)))
|
|
679
|
+
return [];
|
|
680
|
+
if (error.code === "ENOENT")
|
|
681
|
+
throw new Error("requirement-ownership-source-invalid: frozen run.json is missing");
|
|
682
|
+
throw error;
|
|
642
683
|
}
|
|
643
684
|
let spec;
|
|
644
685
|
try {
|
|
645
686
|
spec = JSON.parse(raw);
|
|
646
687
|
}
|
|
647
688
|
catch {
|
|
648
|
-
|
|
689
|
+
throw new Error("requirement-ownership-source-invalid: run.json is malformed");
|
|
649
690
|
}
|
|
650
691
|
const merged = new Map();
|
|
651
692
|
for (const task of spec.tasks ?? []) {
|
|
652
|
-
|
|
693
|
+
const raw = task.shell?.frontendDesignPolicy?.requirementOwnership;
|
|
694
|
+
if (raw === undefined)
|
|
695
|
+
continue;
|
|
696
|
+
const parsed = z.array(z.object({ requirementIds: z.array(z.string().min(1)).min(1), implementationFiles: z.array(z.string().min(1)).optional(), verificationFiles: z.array(z.string().min(1)).optional() }).strict()).safeParse(raw);
|
|
697
|
+
if (!parsed.success)
|
|
698
|
+
throw new Error("requirement-ownership-source-invalid: frozen requirementOwnership is malformed");
|
|
699
|
+
for (const entry of parsed.data) {
|
|
653
700
|
const key = [...(entry.requirementIds ?? [])].sort().join("|");
|
|
654
|
-
|
|
701
|
+
const previous = merged.get(key);
|
|
702
|
+
if (previous && serializeDeterministicJson(previous) !== serializeDeterministicJson(entry))
|
|
703
|
+
throw new Error("requirement-ownership-source-invalid: conflicting ownership declarations");
|
|
704
|
+
if (!previous)
|
|
655
705
|
merged.set(key, entry);
|
|
656
706
|
}
|
|
657
707
|
}
|
|
708
|
+
if (merged.size && expectedBinding) {
|
|
709
|
+
if (!spec.sourceBinding || serializeDeterministicJson(spec.sourceBinding) !== serializeDeterministicJson(expectedBinding))
|
|
710
|
+
throw new Error("requirement-ownership-source-invalid: frozen source binding mismatch or missing");
|
|
711
|
+
}
|
|
658
712
|
return [...merged.values()];
|
|
659
713
|
}
|
|
660
714
|
/** Deterministic requirement→file ownership check. A requirement's
|
|
@@ -700,27 +754,15 @@ function checkRequirementOwnership(canonical, ownership) {
|
|
|
700
754
|
continue;
|
|
701
755
|
if (implementationFiles.length > 0) {
|
|
702
756
|
const allTargets = requirement.implementationTargets ?? [];
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
// claims: a contract may require a mounting file to exist for a
|
|
713
|
-
// requirement implemented elsewhere (smoke r55 — a deliverable
|
|
714
|
-
// otherwise made plan finalize permanently unfixable).
|
|
715
|
-
const deliverables = new Set(requirement.deliverableTargets ?? []);
|
|
716
|
-
const ownedTargets = targets.filter((file) => !deliverables.has(file));
|
|
717
|
-
// Only files OUTSIDE the declared set violate. An empty owned set
|
|
718
|
-
// means every mapped file is a deliverable/test-shaped obligation —
|
|
719
|
-
// unplaced requirements are rejected upstream, not here (r56:
|
|
720
|
-
// "maps [X] expects [X]" still violated when deliverables covered
|
|
721
|
-
// the whole mapping).
|
|
722
|
-
const outside = ownedTargets.filter((file) => !implementationFiles.includes(file));
|
|
723
|
-
if (outside.length > 0) {
|
|
757
|
+
// A verification placement must have explicit verification
|
|
758
|
+
// ownership AND a bound verification target. File names alone
|
|
759
|
+
// never exempt implementation responsibility.
|
|
760
|
+
const verificationPlacements = new Set([...(targetsByRequirement.get(id) ?? [])].filter(file => verificationFiles.includes(file)));
|
|
761
|
+
const targets = allTargets.filter(file => !verificationPlacements.has(file) || implementationFiles.includes(file));
|
|
762
|
+
// implementationTargets retains actual placements. Existence-only
|
|
763
|
+
// deliverableTargets never subtract from this ownership claim.
|
|
764
|
+
const outside = targets.filter(file => !implementationFiles.includes(file));
|
|
765
|
+
if (outside.length > 0 || !targets.some(file => implementationFiles.includes(file))) {
|
|
724
766
|
findings.push({
|
|
725
767
|
code: "requirement-ownership-violated",
|
|
726
768
|
message: `requirement ${id} expects implementation within [${implementationFiles.join(", ")}] but plan maps [${targets.join(", ") || "none"}] — re-map the owning behavior group's module placement`,
|
|
@@ -759,7 +801,7 @@ const FRONTEND_CONTRACT_REVIEW_GROUPS = [
|
|
|
759
801
|
},
|
|
760
802
|
{
|
|
761
803
|
id: "data-design",
|
|
762
|
-
fields: ["mockApi", "designEvidence", "dependencyPolicy", "realIntegrationGap"],
|
|
804
|
+
fields: ["mockApi", "designEvidence", "capabilityBoundary", "dependencyPolicy", "realIntegrationGap"],
|
|
763
805
|
},
|
|
764
806
|
{
|
|
765
807
|
id: "verification",
|
|
@@ -1101,11 +1143,15 @@ export const frontendImplementationContractSchema = z
|
|
|
1101
1143
|
uiStates: z.array(z.string().min(1)),
|
|
1102
1144
|
// Display-only test scope; never read by control flow.
|
|
1103
1145
|
scope: frontendVerificationScopeSchema.optional(),
|
|
1146
|
+
// Planned evidence strength, never proof of observed behavior.
|
|
1147
|
+
evidenceLevel: z.enum(["unit", "mounted", "real-integration"]).optional(),
|
|
1148
|
+
boundary: z.string().min(1).optional(),
|
|
1104
1149
|
})
|
|
1105
1150
|
.strict()).min(1),
|
|
1106
1151
|
evidenceGaps: z.array(gap).optional(),
|
|
1107
1152
|
stylingStrategy: z.string().min(1).optional(),
|
|
1108
1153
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
1154
|
+
capabilityBoundary: capabilityBoundarySchema.optional(),
|
|
1109
1155
|
dependencyPolicy: z.string().min(1).optional(),
|
|
1110
1156
|
residualRisks: z.array(z.string().min(1)).optional(),
|
|
1111
1157
|
realIntegrationGap: z.string().min(1).optional(),
|
|
@@ -2032,7 +2078,7 @@ function assertFrontendContractPathsSafe(value) {
|
|
|
2032
2078
|
const record = asRecord(value);
|
|
2033
2079
|
if (!record)
|
|
2034
2080
|
return;
|
|
2035
|
-
const pathFields = ["files", "file", "implementationTargets", "fixture", "consumer"];
|
|
2081
|
+
const pathFields = ["files", "file", "implementationTargets", "deliverableTargets", "fixture", "consumer"];
|
|
2036
2082
|
for (const [key, child] of Object.entries(record)) {
|
|
2037
2083
|
if (pathFields.includes(key) && Array.isArray(child)) {
|
|
2038
2084
|
for (const item of child) {
|
|
@@ -2326,6 +2372,8 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
2326
2372
|
file,
|
|
2327
2373
|
symbol: asString(vt.symbol) || undefined,
|
|
2328
2374
|
...(scope ? { scope } : {}),
|
|
2375
|
+
...(vt.evidenceLevel !== undefined ? { evidenceLevel: vt.evidenceLevel } : {}),
|
|
2376
|
+
...(vt.boundary !== undefined ? { boundary: vt.boundary } : {}),
|
|
2329
2377
|
requirementIds: requirementIds.length > 0
|
|
2330
2378
|
? requirementIds
|
|
2331
2379
|
: [...canonicalBinding.requirementIds],
|
|
@@ -2755,7 +2803,10 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2755
2803
|
// field (r10/r11 design-review findings).
|
|
2756
2804
|
const referencedConcretePaths = [
|
|
2757
2805
|
...new Set([
|
|
2758
|
-
...result.data.requirements.flatMap(
|
|
2806
|
+
...result.data.requirements.flatMap(requirement => [
|
|
2807
|
+
...requirement.implementationTargets,
|
|
2808
|
+
...(requirement.deliverableTargets ?? []),
|
|
2809
|
+
]),
|
|
2759
2810
|
// Mock endpoint paths stay in the set: the zod refine validated
|
|
2760
2811
|
// them against the declared targets.files patterns, so the
|
|
2761
2812
|
// narrowing must keep covering them. Verification-target files
|
|
@@ -2765,6 +2816,9 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2765
2816
|
...(result.data.mockApi?.endpoints ?? []).flatMap((endpoint) => [endpoint.fixture, endpoint.consumer].filter((value) => typeof value === "string" && value.length > 0)),
|
|
2766
2817
|
].filter((value) => value.length > 0)),
|
|
2767
2818
|
].sort();
|
|
2819
|
+
const outsideDelivery = result.data.requirements.flatMap(requirement => requirement.deliverableTargets ?? []).filter(file => !parsedTargetFiles.some(pattern => pathMatchesPattern(file, pattern)));
|
|
2820
|
+
if (outsideDelivery.length)
|
|
2821
|
+
fail("blocked", `target-path-outside-write-set: deliverable paths [${outsideDelivery.join(", ")}] exceed frozen targets`, candidateJsonSha256);
|
|
2768
2822
|
if (referencedConcretePaths.length > 0) {
|
|
2769
2823
|
result.data.targets = {
|
|
2770
2824
|
...result.data.targets,
|
|
@@ -2793,7 +2847,26 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2793
2847
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
2794
2848
|
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
2795
2849
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
2796
|
-
|
|
2850
|
+
// Both finalize prechecks and the final materializer enter this function.
|
|
2851
|
+
// Check the submitted placement before normalization can invent a fallback.
|
|
2852
|
+
const ownership = await deriveRequirementOwnershipFromRun(input.runDir, input.sourceBinding);
|
|
2853
|
+
for (const entry of ownership) {
|
|
2854
|
+
if (!entry.implementationFiles?.length)
|
|
2855
|
+
continue;
|
|
2856
|
+
for (const id of entry.requirementIds) {
|
|
2857
|
+
const original = asRecord(parsed)?.requirements?.find(item => asRecord(item)?.id === id);
|
|
2858
|
+
if (!original || asStringArray(asRecord(original)?.implementationTargets).length === 0)
|
|
2859
|
+
fail("retryable-invalid", `requirement-ownership-placement-missing: ${id} requires an explicit placement from its Plan facts`, candidateJsonSha256);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
const ownershipFindings = checkRequirementOwnership(result.data, ownership);
|
|
2863
|
+
if (ownershipFindings.length)
|
|
2864
|
+
throw new PlanPolicyPrecheckFailure(ownershipFindings.map(f => `${f.code}: ${f.message}`).join("; "), ownershipFindings);
|
|
2865
|
+
const frozenBoundary = await deriveFrontendCapabilityBoundaryFromRun(input.runDir);
|
|
2866
|
+
delete result.data.capabilityBoundary;
|
|
2867
|
+
if (frozenBoundary)
|
|
2868
|
+
result.data.capabilityBoundary = frozenBoundary;
|
|
2869
|
+
result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, frozenBoundary);
|
|
2797
2870
|
return {
|
|
2798
2871
|
canonical: result.data,
|
|
2799
2872
|
candidateRawSha256,
|
|
@@ -2990,6 +3063,7 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
2990
3063
|
};
|
|
2991
3064
|
}
|
|
2992
3065
|
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
3066
|
+
"/capabilityBoundary",
|
|
2993
3067
|
"/schemaVersion",
|
|
2994
3068
|
"/sourceBinding",
|
|
2995
3069
|
"/riskLevel",
|
|
@@ -3036,7 +3110,7 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
3036
3110
|
if (!record)
|
|
3037
3111
|
return ["/"];
|
|
3038
3112
|
const violations = [];
|
|
3039
|
-
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
3113
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel", "capabilityBoundary"]) {
|
|
3040
3114
|
if (Object.hasOwn(record, key))
|
|
3041
3115
|
violations.push(`/${key}`);
|
|
3042
3116
|
}
|
|
@@ -3050,6 +3124,22 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
3050
3124
|
violations.push("/mockApi/productionDefaultOff");
|
|
3051
3125
|
return violations;
|
|
3052
3126
|
}
|
|
3127
|
+
/** Read-only verification references do not grant writer permissions. */
|
|
3128
|
+
export async function collectExistingFrontendVerificationPaths(workspaceRoot, contract) {
|
|
3129
|
+
if (!workspaceRoot)
|
|
3130
|
+
return [];
|
|
3131
|
+
const existing = [];
|
|
3132
|
+
for (const target of contract.verificationTargets) {
|
|
3133
|
+
if (target.mode === "static" || existing.includes(target.file))
|
|
3134
|
+
continue;
|
|
3135
|
+
try {
|
|
3136
|
+
await readFileBoundarySafe({ boundaryRoot: workspaceRoot, targetPath: path.resolve(workspaceRoot, target.file), label: "frontend existing verification" });
|
|
3137
|
+
existing.push(target.file);
|
|
3138
|
+
}
|
|
3139
|
+
catch { /* Missing or unsafe files cannot bypass the write-set check. */ }
|
|
3140
|
+
}
|
|
3141
|
+
return existing;
|
|
3142
|
+
}
|
|
3053
3143
|
function workspaceRootFromDagRunDir(runDir) {
|
|
3054
3144
|
const absolute = path.resolve(runDir);
|
|
3055
3145
|
const segments = absolute.split(path.sep);
|
|
@@ -3253,9 +3343,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
3253
3343
|
...checkRequiredDeliverables(analysis.canonical),
|
|
3254
3344
|
...checkUxNameCollisions(analysis.canonical),
|
|
3255
3345
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
3256
|
-
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
3346
|
+
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir), await collectExistingFrontendVerificationPaths(workspaceRootFromDagRunDir(input.runDir), analysis.canonical)),
|
|
3257
3347
|
...checkComponentChoiceSourceReferences(analysis.canonical, await deriveFrontendSourceBindingPathsFromRun(input.runDir)),
|
|
3258
|
-
...checkRequirementOwnership(analysis.canonical, await deriveRequirementOwnershipFromRun(input.runDir)),
|
|
3259
3348
|
...checkInteractionIdVocabulary(analysis.canonical, await deriveFrontendInteractionIdsFromRun(input.runDir)),
|
|
3260
3349
|
...checkMockApiStrategyAllowed(analysis.canonical, await deriveFrontendAllowedMockStrategiesFromRun(input.runDir)),
|
|
3261
3350
|
];
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
+
import { FRONTEND_PLAN_CANARY_SCENARIOS, readFrontendCanaryEvidence, compareFrontendPairIdentity, readFrontendRootObservation } from "./frontend-root-observation.js";
|
|
1
2
|
import { readFrontendPlanCanaryRunMetrics, } from "./frontend-session-budget.js";
|
|
2
|
-
export
|
|
3
|
-
"missing-idle-loading-empty",
|
|
4
|
-
"loading-not-mounted",
|
|
5
|
-
"test-layer-does-not-prove-wiring",
|
|
6
|
-
"mock-claimed-as-real-integration",
|
|
7
|
-
];
|
|
3
|
+
export { FRONTEND_PLAN_CANARY_SCENARIOS } from "./frontend-root-observation.js";
|
|
8
4
|
const reduction = (before, after) => before !== null && after !== null && before > 0 ? ((before - after) / before) * 100 : null;
|
|
9
5
|
/** Compare two same-input runs. Missing observations fail the canary rather
|
|
10
6
|
* than being interpreted as a zero-cost candidate. */
|
|
11
7
|
export function evaluateFrontendPlanCanary(input) {
|
|
12
|
-
const required = { planSessionsPct: 30, planWallClockPct: 30,
|
|
8
|
+
const required = { planSessionsPct: 30, planWallClockPct: 30, factReplacementPct: 50, ...input.requiredReductions };
|
|
13
9
|
const reductions = {
|
|
14
10
|
planSessionsPct: reduction(input.baseline.planSessions, input.candidate.planSessions),
|
|
15
11
|
planWallClockPct: reduction(input.baseline.planWallClockMs, input.candidate.planWallClockMs),
|
|
16
|
-
|
|
12
|
+
factReplacementPct: reduction(input.baseline.factReplacements, input.candidate.factReplacements),
|
|
17
13
|
};
|
|
18
14
|
const failedChecks = [];
|
|
19
15
|
if (reductions.planSessionsPct === null || reductions.planSessionsPct < required.planSessionsPct)
|
|
@@ -23,31 +19,85 @@ export function evaluateFrontendPlanCanary(input) {
|
|
|
23
19
|
// A zero retry baseline is already optimal: a zero->zero candidate is not a
|
|
24
20
|
// regression and must not block, while zero->positive or missing evidence
|
|
25
21
|
// still fails closed.
|
|
26
|
-
const retriesAlreadyOptimal = input.baseline.
|
|
22
|
+
const retriesAlreadyOptimal = input.baseline.factReplacements === 0 && input.candidate.factReplacements === 0;
|
|
27
23
|
if (!retriesAlreadyOptimal &&
|
|
28
|
-
(reductions.
|
|
29
|
-
failedChecks.push("
|
|
24
|
+
(reductions.factReplacementPct === null || reductions.factReplacementPct < required.factReplacementPct)) {
|
|
25
|
+
failedChecks.push("fact-replacement-reduction");
|
|
30
26
|
}
|
|
31
27
|
for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
|
|
32
28
|
const result = input.scenarios[scenario];
|
|
33
29
|
if (result !== "blocked" && result !== "reported")
|
|
34
30
|
failedChecks.push(`regression:${scenario}`);
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
for (const metric of ["planModelRequests", "planTokens", "toolRejections", "transportRetries", "correctionSessions", "finalizeRejections"]) {
|
|
33
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
34
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || after < 0 || after > before)
|
|
35
|
+
failedChecks.push(`plan-cost:${metric}`);
|
|
36
|
+
}
|
|
37
|
+
const planPass = failedChecks.length === 0;
|
|
38
|
+
for (const metric of ["rootTokens", "rootReservedRequests"]) {
|
|
39
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
40
|
+
const allowance = metric === "rootTokens" ? 1.1 : 1;
|
|
41
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || before < 0 || after < 0 || after > before * allowance)
|
|
42
|
+
failedChecks.push(`root-cost:${metric}`);
|
|
43
|
+
}
|
|
44
|
+
for (const metric of ["dagWallClockMs", "designReviewRejectRate", "finalReviewDefectRate", "recoveryCount"]) {
|
|
45
|
+
const before = input.baseline[metric], after = input.candidate[metric];
|
|
46
|
+
if (before == null || after == null || !Number.isFinite(before) || !Number.isFinite(after) || after < 0 || after > before)
|
|
47
|
+
failedChecks.push(`quality:${metric}`);
|
|
48
|
+
}
|
|
49
|
+
return { pass: failedChecks.length === 0, planPass, reductions, failedChecks };
|
|
37
50
|
}
|
|
38
51
|
/** Read two frozen run directories and evaluate the same-input canary. Plan
|
|
39
|
-
* cost metrics come from persisted session receipts
|
|
40
|
-
*
|
|
52
|
+
* cost metrics come from persisted session receipts and optionally explicit root
|
|
53
|
+
* lineage. Free numeric supplements cannot establish quality evidence.
|
|
41
54
|
* Missing observations surface as `null` and fail the evaluation, never as a
|
|
42
55
|
* manufactured zero-cost improvement. */
|
|
43
56
|
export async function compareFrontendPlanCanaryRuns(input) {
|
|
44
57
|
const baseline = await readFrontendPlanCanaryRunMetrics(input.baselineRunDir);
|
|
45
58
|
const candidate = await readFrontendPlanCanaryRunMetrics(input.candidateRunDir);
|
|
59
|
+
const lineageChecks = [];
|
|
60
|
+
const regressionEvidence = [];
|
|
61
|
+
const identities = {};
|
|
62
|
+
if (input.rootRuns) {
|
|
63
|
+
for (const [name, metrics, runId] of [["baseline", baseline, input.rootRuns.baselineRunId], ["candidate", candidate, input.rootRuns.candidateRunId]]) {
|
|
64
|
+
const root = await readFrontendRootObservation(input.rootRuns.workspaceRoot, runId);
|
|
65
|
+
const consumed = root.cumulativeBudget?.consumed;
|
|
66
|
+
Object.assign(metrics, root.plan, { dagWallClockMs: root.dagWallClockMs, recoveryCount: root.recoveryCount,
|
|
67
|
+
rootTokens: root.complete && consumed?.missingTokenNodeIds?.length === 0 ? consumed.tokens : null,
|
|
68
|
+
rootReservedRequests: root.complete ? consumed?.providerRequests ?? null : null });
|
|
69
|
+
if (!root.complete)
|
|
70
|
+
lineageChecks.push(`lineage:${name}`);
|
|
71
|
+
if (root.complete && input.evidence?.[name]) {
|
|
72
|
+
try {
|
|
73
|
+
const evidence = await readFrontendCanaryEvidence({ workspaceRoot: input.rootRuns.workspaceRoot, runDir: name === "baseline" ? input.baselineRunDir : input.candidateRunDir, runIds: root.runIds, manifest: input.evidence[name] });
|
|
74
|
+
identities[name] = evidence.identity;
|
|
75
|
+
regressionEvidence.push(evidence.regressions);
|
|
76
|
+
Object.assign(metrics, { designReviewRejectRate: evidence.designReviewRejectRate, finalReviewDefectRate: evidence.finalReviewDefectRate });
|
|
77
|
+
if (!evidence.acceptancePassed)
|
|
78
|
+
lineageChecks.push(`acceptance:${name}`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
lineageChecks.push(`evidence:${name}:${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
lineageChecks.push(`missing-evidence:${name}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
lineageChecks.push("missing-root-lineage");
|
|
90
|
+
const scenarios = {};
|
|
91
|
+
for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
|
|
92
|
+
scenarios[scenario] = regressionEvidence.length === 2 && regressionEvidence.every(evidence => evidence[scenario] === "blocked" || evidence[scenario] === "reported") ? "reported" : "not-observed";
|
|
93
|
+
}
|
|
46
94
|
const evaluation = evaluateFrontendPlanCanary({
|
|
47
95
|
baseline,
|
|
48
96
|
candidate,
|
|
49
|
-
scenarios
|
|
97
|
+
scenarios,
|
|
50
98
|
requiredReductions: input.requiredReductions,
|
|
51
99
|
});
|
|
52
|
-
|
|
100
|
+
const identityChecks = compareFrontendPairIdentity(identities.baseline ?? {}, identities.candidate ?? {});
|
|
101
|
+
const failedChecks = [...evaluation.failedChecks, ...identityChecks, ...lineageChecks];
|
|
102
|
+
return { ...evaluation, pass: failedChecks.length === 0, failedChecks, baseline, candidate };
|
|
53
103
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { repairAssertionSchema } from "./frontend-repair-assertions.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
3
4
|
import { deterministicSha256 } from "./frontend-implementation-contract.js";
|
|
@@ -94,7 +95,7 @@ export function projectFrontendSafetyFacts(input) {
|
|
|
94
95
|
const ids = sorted(group?.requirementIds ?? []);
|
|
95
96
|
for (const id of ids)
|
|
96
97
|
verificationByRequirement[id]?.push(focus.id);
|
|
97
|
-
return { id: focus.id, file: focus.file, commandId: focus.commandId, requirementIds: ids };
|
|
98
|
+
return { id: focus.id, file: focus.file, commandId: focus.commandId, requirementIds: ids, evidenceLevel: focus.evidenceLevel, ...(focus.boundary ? { boundary: focus.boundary } : {}) };
|
|
98
99
|
});
|
|
99
100
|
for (const id of requirementIds)
|
|
100
101
|
verificationByRequirement[id] = sorted(verificationByRequirement[id]);
|
|
@@ -443,6 +444,8 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
443
444
|
: declaredStateNamesByGroup.get(focus.behaviorGroupId) ?? [];
|
|
444
445
|
return {
|
|
445
446
|
id: focus.id,
|
|
447
|
+
evidenceLevel: focus.evidenceLevel,
|
|
448
|
+
...(focus.boundary ? { boundary: focus.boundary } : {}),
|
|
446
449
|
commandId: focus.commandId,
|
|
447
450
|
file: focus.file,
|
|
448
451
|
requirementIds,
|
|
@@ -450,7 +453,7 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
450
453
|
};
|
|
451
454
|
});
|
|
452
455
|
const verificationTargetIds = new Set(verificationTargets.map(target => target.id));
|
|
453
|
-
// ---- requirements:
|
|
456
|
+
// ---- requirements: preserve group placement separately from contract deliverable
|
|
454
457
|
// obligations. Every requirement must keep a placement and at least one
|
|
455
458
|
// verification target (both are guaranteed post-finalize because the
|
|
456
459
|
// decision safety projection blocks unplaced / unverified requirements).
|
|
@@ -458,7 +461,7 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
458
461
|
const groupId = groupByRequirement.get(requirement.id);
|
|
459
462
|
const placed = sorted(placementPathsById.get(groupId ?? "") ?? []);
|
|
460
463
|
const deliverable = sorted(requiredByRequirement.get(requirement.id) ?? []);
|
|
461
|
-
const implementationTargets =
|
|
464
|
+
const implementationTargets = placed;
|
|
462
465
|
const derivedVerificationTargetIds = sorted([...(vtIdsByGroup.get(groupId ?? "") ?? [])].filter(id => verificationTargetIds.has(id)));
|
|
463
466
|
return {
|
|
464
467
|
id: requirement.id,
|
|
@@ -613,12 +616,6 @@ export function buildFrontendPlanRelationshipPatch(input) {
|
|
|
613
616
|
...(stylingStrategy ? { stylingStrategy } : {}),
|
|
614
617
|
};
|
|
615
618
|
}
|
|
616
|
-
const repairAssertionSchema = z.discriminatedUnion("check", [
|
|
617
|
-
z.object({ check: z.literal("file-absent"), token: z.string().min(1), source: z.string() }).strict(),
|
|
618
|
-
z.object({ check: z.literal("state-present"), token: z.string().min(1), source: z.string() }).strict(),
|
|
619
|
-
z.object({ check: z.literal("vt-changed"), token: z.string().min(1), source: z.string(),
|
|
620
|
-
expectedRequirementIds: z.array(z.string().min(1)).min(1) }).strict(),
|
|
621
|
-
]);
|
|
622
619
|
/** No heuristic fallback: absent structured expectations mean that semantic
|
|
623
620
|
* closure must be decided by the subsequent design review. Invalid explicit
|
|
624
621
|
* assertions throw rather than silently disabling a requested check. */
|
|
@@ -742,26 +739,21 @@ export function collapseDecisionEntriesByKind(kind, collected) {
|
|
|
742
739
|
};
|
|
743
740
|
const byKey = new Map();
|
|
744
741
|
const keyless = [];
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
const retractedKeys = new Set();
|
|
748
|
-
for (const entry of collected) {
|
|
749
|
-
if (entry.retracted === true && typeof entry.interaction === "string") {
|
|
750
|
-
retractedKeys.add(entry.interaction);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
742
|
+
// Chronological last-wins semantics also permit a later explicit correction
|
|
743
|
+
// after a tombstone. Never return the original tombstones as active rows.
|
|
753
744
|
for (const entry of collected) {
|
|
754
|
-
if (entry.retracted === true)
|
|
755
|
-
continue;
|
|
756
745
|
const key = keyOf(entry);
|
|
757
|
-
if (
|
|
746
|
+
if (entry.retracted === true) {
|
|
747
|
+
if (key)
|
|
748
|
+
byKey.delete(key);
|
|
758
749
|
continue;
|
|
750
|
+
}
|
|
759
751
|
if (key)
|
|
760
752
|
byKey.set(key, entry);
|
|
761
753
|
else
|
|
762
754
|
keyless.push(entry);
|
|
763
755
|
}
|
|
764
|
-
return
|
|
756
|
+
return [...byKey.values(), ...keyless];
|
|
765
757
|
}
|
|
766
758
|
/** Compile committed decision facts into a FrontendPlanDecisionContract. Each
|
|
767
759
|
* fact's `entry` is already schema-shaped (the tool validates it); this collects
|