@tea-agent/loop-agent 0.39.0-next.8 → 0.39.0
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/AGENTS.md +4 -1
- package/CHANGELOG.md +264 -67
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +124 -18
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +554 -33
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +138 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +28 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +480 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +10 -10
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operator-actions.js +195 -4
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
- package/dist/workflows/dag/frontend-plan-render.js +1 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +894 -205
- package/dist/workflows/dag/node-execution.js +183 -15
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +109 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +143 -1
- package/dist/workflows/dag/validate.js +64 -20
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +6 -4
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
- package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
|
@@ -5,7 +5,28 @@ import { z } from "zod";
|
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
|
|
7
7
|
import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
|
|
8
|
+
import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
|
|
9
|
+
import { defaultFrontendTestLayout, frontendTestEvidencePrefix, frontendTestManifestPath, } from "./frontend-test-layout.js";
|
|
8
10
|
export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
|
|
11
|
+
/**
|
|
12
|
+
* Truncated acceptance-id shapes observed in generator output: a bare
|
|
13
|
+
* numeric segment ("AC-002" dropped its AC-FE-<FEATURE> prefix) or an
|
|
14
|
+
* AC-FE-<FEATURE> literal without the trailing -NNN sequence number
|
|
15
|
+
* ("AC-FE-CHAT"). Generic short ids like "AC-1"/"AC-FE-001" stay valid;
|
|
16
|
+
* declared-source cross-checks (unknown-ac) remain the primary guard when a
|
|
17
|
+
* sourceBinding exists.
|
|
18
|
+
*/
|
|
19
|
+
export function isTruncatedFrontendAcId(acId) {
|
|
20
|
+
return /^AC-FE-[A-Z]+$/i.test(acId.trim());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bare numeric ids ("AC-002") only indicate truncation when no declared AC
|
|
24
|
+
* list exists to adjudicate them: with a sourceBinding they are ordinary
|
|
25
|
+
* unknown-ac findings ("AC-999" may be a legitimately declared id style).
|
|
26
|
+
*/
|
|
27
|
+
export function isTruncatedWhenUndeclaredFrontendAcId(acId) {
|
|
28
|
+
return /^AC-\d{3}$/i.test(acId.trim()) || isTruncatedFrontendAcId(acId);
|
|
29
|
+
}
|
|
9
30
|
const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
|
|
10
31
|
!path.win32.isAbsolute(value) &&
|
|
11
32
|
!value.includes("\\") &&
|
|
@@ -56,6 +77,8 @@ export const frontendTestResultContractSchema = z.object({
|
|
|
56
77
|
passed: z.number().int().min(0),
|
|
57
78
|
failed: z.number().int().min(0),
|
|
58
79
|
blocked: z.number().int().min(0),
|
|
80
|
+
/** Stability metric: passed on attempt 0 (no rerun/self-heal retry). */
|
|
81
|
+
firstTryPassed: z.number().int().min(0).optional(),
|
|
59
82
|
}).strict(),
|
|
60
83
|
acceptanceCoverage: z.object({
|
|
61
84
|
covered: z.array(z.string().min(1)),
|
|
@@ -102,14 +125,22 @@ function isSafeEvidenceShellPath(value) {
|
|
|
102
125
|
* backslash interpretation for the former long `node -e` command.
|
|
103
126
|
*/
|
|
104
127
|
export async function validateFrontendCaseEvidence(input) {
|
|
105
|
-
const
|
|
128
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
129
|
+
const manifestRel = frontendTestManifestPath(layout);
|
|
130
|
+
const manifestPath = path.join(input.workspaceRoot, manifestRel);
|
|
106
131
|
try {
|
|
107
132
|
await stat(manifestPath);
|
|
108
133
|
}
|
|
109
134
|
catch {
|
|
110
|
-
throw new Error(
|
|
135
|
+
throw new Error(`missing ${manifestRel}`);
|
|
136
|
+
}
|
|
137
|
+
let manifest;
|
|
138
|
+
try {
|
|
139
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error("invalid frontend case manifest: malformed JSON");
|
|
111
143
|
}
|
|
112
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
113
144
|
if (!Array.isArray(manifest.cases))
|
|
114
145
|
throw new Error("invalid frontend case manifest");
|
|
115
146
|
const statuses = new Set(["passed", "failed", "blocked"]);
|
|
@@ -119,7 +150,7 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
119
150
|
const item = rawCase;
|
|
120
151
|
const id = typeof item?.caseId === "string" ? item.caseId : "?";
|
|
121
152
|
const dir = item?.evidenceDir;
|
|
122
|
-
const prefix =
|
|
153
|
+
const prefix = frontendTestEvidencePrefix(layout, id);
|
|
123
154
|
if (!item ||
|
|
124
155
|
typeof item.caseId !== "string" ||
|
|
125
156
|
typeof dir !== "string" ||
|
|
@@ -163,12 +194,15 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
163
194
|
continue;
|
|
164
195
|
}
|
|
165
196
|
const currentPrefix = `${prefix}/`;
|
|
166
|
-
if (evidencePath.startsWith(
|
|
197
|
+
if ((evidencePath.startsWith(`${layout.evidenceDir}/`) ||
|
|
198
|
+
evidencePath.startsWith("testcase/frontend/evidence/")) &&
|
|
199
|
+
!evidencePath.startsWith(currentPrefix)) {
|
|
167
200
|
hardFail = true;
|
|
168
201
|
issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
|
|
169
202
|
continue;
|
|
170
203
|
}
|
|
171
|
-
const target = evidencePath.startsWith(
|
|
204
|
+
const target = evidencePath.startsWith(`${layout.testRoot}/`) ||
|
|
205
|
+
evidencePath.startsWith("testcase/")
|
|
172
206
|
? path.join(input.workspaceRoot, evidencePath)
|
|
173
207
|
: path.join(input.workspaceRoot, dir, evidencePath);
|
|
174
208
|
if (!(await isNonEmptyFile(target))) {
|
|
@@ -194,11 +228,17 @@ export function evaluatePassedCaseBrowserReceipts(summary, caseId) {
|
|
|
194
228
|
}
|
|
195
229
|
return { ok: true };
|
|
196
230
|
}
|
|
197
|
-
async function
|
|
231
|
+
async function loadCaseBrowserReceiptsWithSummary(input) {
|
|
198
232
|
try {
|
|
199
233
|
const entries = await readdir(input.runDir, { withFileTypes: true });
|
|
200
234
|
// A passed chain must belong to exactly one browser child stream. Never
|
|
201
|
-
// stitch receipts across DAG nodes or accept ambiguous duplicate
|
|
235
|
+
// stitch receipts across DAG nodes or accept ambiguous duplicate
|
|
236
|
+
// authority — with one exception: bounded rerun creates a second,
|
|
237
|
+
// later stream for the same case (the rerun overwrites the disk
|
|
238
|
+
// case-result, so the final attempt is the authoritative one). When
|
|
239
|
+
// multiple streams authorize, prefer the highest rerun round
|
|
240
|
+
// (rerun-frontend-case-rN-<idx> directories); ties within the same
|
|
241
|
+
// round remain ambiguous and fail closed.
|
|
202
242
|
const authorizingStreams = [];
|
|
203
243
|
for (const entry of entries) {
|
|
204
244
|
if (!entry.isDirectory())
|
|
@@ -206,48 +246,123 @@ async function loadCaseBrowserReceiptSummary(input) {
|
|
|
206
246
|
const nodeReceipts = (await readPlaywrightCliReceipts(input.runDir, entry.name))
|
|
207
247
|
.filter((item) => item.caseId === input.caseId);
|
|
208
248
|
const summary = summarizePlaywrightCliReceipts(nodeReceipts);
|
|
209
|
-
if (summary.hasOrderedPassedReceiptChain)
|
|
210
|
-
|
|
249
|
+
if (summary.hasOrderedPassedReceiptChain) {
|
|
250
|
+
const roundMatch = /rerun-frontend-case-r(\d+)-/i.exec(entry.name);
|
|
251
|
+
authorizingStreams.push({
|
|
252
|
+
receipts: nodeReceipts,
|
|
253
|
+
summary,
|
|
254
|
+
round: roundMatch ? Number(roundMatch[1]) : 0,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (authorizingStreams.length === 0) {
|
|
259
|
+
return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
211
260
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
261
|
+
const maxRound = Math.max(...authorizingStreams.map((s) => s.round));
|
|
262
|
+
const finalists = authorizingStreams.filter((s) => s.round === maxRound);
|
|
263
|
+
return finalists.length === 1
|
|
264
|
+
? finalists[0]
|
|
265
|
+
: { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
215
266
|
}
|
|
216
267
|
catch {
|
|
217
|
-
return summarizePlaywrightCliReceipts([]);
|
|
268
|
+
return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
218
269
|
}
|
|
219
270
|
}
|
|
220
|
-
|
|
221
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Extract `find "literal" >=N` / `find "literal"` declarations from a case
|
|
273
|
+
* document. Only quoted literals are binding assertions — the count suffix
|
|
274
|
+
* (`>=N`, `==N`) is the expected minimum/exact match count from the ledger.
|
|
275
|
+
* Placeholder literals ("...", "…", "<字面>", "eX") appear in prose discussing
|
|
276
|
+
* the syntax itself; they are documentation, never declarations.
|
|
277
|
+
*/
|
|
278
|
+
export function extractDeclaredFindAssertions(caseMarkdown) {
|
|
279
|
+
const source = stripFencedCodeBlocks(caseMarkdown);
|
|
280
|
+
const out = [];
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const pattern = /find\s+"((?:[^"\\]|\\.)+)"\s*(>=|==)\s*(\d+)/g;
|
|
283
|
+
const isPlaceholder = (literal) => /^(?:\.{2,}|…+|<[^>]*>|e\d+)$/.test(literal.trim());
|
|
284
|
+
let match;
|
|
285
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
286
|
+
const literal = match[1].replace(/\\(.)/g, "$1");
|
|
287
|
+
if (isPlaceholder(literal))
|
|
288
|
+
continue;
|
|
289
|
+
const operator = match[2];
|
|
290
|
+
const expected = Number(match[3]);
|
|
291
|
+
const key = `${operator}:${literal}`;
|
|
292
|
+
if (seen.has(key))
|
|
293
|
+
continue;
|
|
294
|
+
seen.add(key);
|
|
295
|
+
out.push({ literal, expected, operator });
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
222
298
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
299
|
+
function stripFencedCodeBlocks(markdown) {
|
|
300
|
+
return markdown.replace(/```[\s\S]*?```/g, "");
|
|
301
|
+
}
|
|
302
|
+
async function readCaseMarkdownForAudit(workspaceRoot, casePath) {
|
|
303
|
+
try {
|
|
304
|
+
return await readFile(path.join(workspaceRoot, casePath), "utf8");
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
228
307
|
return "";
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Controller-side assertion audit: every find-family declaration in the case
|
|
312
|
+
* document must be backed by a successful find receipt whose parsed match
|
|
313
|
+
* count satisfies the declared operator. This moves pass authority from
|
|
314
|
+
* "the model says it asserted" to "receipts prove it asserted".
|
|
315
|
+
*/
|
|
316
|
+
export function auditDeclaredFindAssertions(declared, receipts) {
|
|
317
|
+
if (declared.length === 0)
|
|
318
|
+
return { ok: true };
|
|
319
|
+
const successfulFinds = receipts.filter((receipt) => (receipt.command === "find" || receipt.command === "find-unique") &&
|
|
320
|
+
receipt.exitCode === 0 &&
|
|
321
|
+
!receipt.timedOut);
|
|
322
|
+
const missing = [];
|
|
323
|
+
for (const decl of declared) {
|
|
324
|
+
const backing = successfulFinds.some((receipt) => receipt.argsRedacted.some((arg) => arg === decl.literal || arg === `"${decl.literal}"`) &&
|
|
325
|
+
typeof receipt.matchCount === "number" &&
|
|
326
|
+
(decl.operator === ">="
|
|
327
|
+
? receipt.matchCount >= decl.expected
|
|
328
|
+
: receipt.matchCount === decl.expected));
|
|
329
|
+
if (!backing) {
|
|
330
|
+
missing.push(`${decl.operator}${decl.expected} "${decl.literal.slice(0, 40)}"`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (missing.length > 0) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
reason: "assertion-not-receipt-backed",
|
|
337
|
+
missing,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { ok: true };
|
|
232
341
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
return items;
|
|
342
|
+
function sha256(content) {
|
|
343
|
+
return createHash("sha256").update(content).digest("hex");
|
|
236
344
|
}
|
|
237
345
|
/**
|
|
238
346
|
* Read planned case facts from the case Markdown. Recognizes both English and
|
|
239
|
-
* Chinese headings
|
|
240
|
-
* test steps), alongside the legacy `操作步骤`/`Steps`
|
|
347
|
+
* Chinese headings at `##` or `###`, including `测试点` (test points) and
|
|
348
|
+
* `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
|
|
349
|
+
* headings. Numbered and bulleted lists are extracted; a missing 测试目的
|
|
350
|
+
* falls back to the case H1 title.
|
|
241
351
|
*/
|
|
242
|
-
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
243
|
-
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
352
|
+
async function readFrontendCaseContent(workspaceRoot, casePath, layout) {
|
|
353
|
+
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
354
|
+
!(casePath.startsWith(`${layout.casesDir}/`) ||
|
|
355
|
+
casePath.startsWith("testcase/frontend/cases/"))) {
|
|
244
356
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
245
357
|
}
|
|
246
358
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
359
|
+
const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
|
|
360
|
+
markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
|
|
361
|
+
"未提供测试目的";
|
|
247
362
|
return {
|
|
248
363
|
caseContent: {
|
|
249
|
-
purpose
|
|
250
|
-
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
|
|
364
|
+
purpose,
|
|
365
|
+
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
|
|
251
366
|
steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
|
|
252
367
|
expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
|
|
253
368
|
},
|
|
@@ -290,7 +405,7 @@ function assertInside(root, candidate, label) {
|
|
|
290
405
|
throw new Error(`${label} escapes its allowed root`);
|
|
291
406
|
}
|
|
292
407
|
}
|
|
293
|
-
async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
408
|
+
async function readEvidenceFile(repoRoot, evidenceRoot, relative, layout) {
|
|
294
409
|
if (!safeRelativePathSchema.safeParse(relative).success) {
|
|
295
410
|
throw new Error(`unsafe evidence path: ${relative}`);
|
|
296
411
|
}
|
|
@@ -302,12 +417,16 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
|
302
417
|
const normalized = relative;
|
|
303
418
|
const evidenceRootRel = path.relative(repoRootResolved, evidenceRootResolved).replaceAll("\\", "/");
|
|
304
419
|
const isCurrentCaseRepoPath = normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`);
|
|
305
|
-
const isRepoRelative = normalized.startsWith(
|
|
420
|
+
const isRepoRelative = (normalized.startsWith(`${layout.testRoot}/`) ||
|
|
421
|
+
normalized.startsWith("testcase/")) &&
|
|
422
|
+
!isCurrentCaseRepoPath;
|
|
306
423
|
const absolute = isRepoRelative
|
|
307
424
|
? path.resolve(repoRootResolved, normalized)
|
|
308
425
|
: path.resolve(evidenceRootResolved, normalized);
|
|
309
426
|
assertInside(repoRootResolved, absolute, "evidence path");
|
|
310
|
-
if (isRepoRelative &&
|
|
427
|
+
if (isRepoRelative &&
|
|
428
|
+
(normalized.startsWith(`${layout.evidenceDir}/`) ||
|
|
429
|
+
normalized.startsWith("testcase/frontend/evidence/"))) {
|
|
311
430
|
throw new Error(`evidence path escapes evidence root: ${relative}`);
|
|
312
431
|
}
|
|
313
432
|
if (!isRepoRelative)
|
|
@@ -330,9 +449,16 @@ export async function materializeFrontendTestResult(input) {
|
|
|
330
449
|
if (!sourceBinding?.taskId || !sourceBinding.sources?.length) {
|
|
331
450
|
throw new Error("frontend-test result requires frozen sourceBinding");
|
|
332
451
|
}
|
|
333
|
-
const
|
|
452
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
453
|
+
const manifestPath = frontendTestManifestPath(layout);
|
|
334
454
|
const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
|
|
335
|
-
|
|
455
|
+
let manifest;
|
|
456
|
+
try {
|
|
457
|
+
manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
throw new Error("invalid or empty frontend case manifest: malformed JSON");
|
|
461
|
+
}
|
|
336
462
|
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.cases) || manifest.cases.length === 0) {
|
|
337
463
|
throw new Error("invalid or empty frontend case manifest");
|
|
338
464
|
}
|
|
@@ -340,6 +466,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
340
466
|
const contentQuality = await validateFrontendCaseContent({
|
|
341
467
|
workspaceRoot: input.workspaceRoot,
|
|
342
468
|
requiredAcIds: sourceBinding.requirementIds ?? [],
|
|
469
|
+
layout,
|
|
343
470
|
});
|
|
344
471
|
advisoryFindings.push(...contentQuality.findings);
|
|
345
472
|
const requiredIds = new Set(sourceBinding.requirementIds ?? []);
|
|
@@ -351,7 +478,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
351
478
|
advisoryFindings.push({ ruleId: "invalid-manifest-case", detail: "manifest case is missing caseId, acIds, or evidenceDir" });
|
|
352
479
|
continue;
|
|
353
480
|
}
|
|
354
|
-
const evidencePrefix =
|
|
481
|
+
const evidencePrefix = frontendTestEvidencePrefix(layout, item.caseId);
|
|
355
482
|
if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
|
|
356
483
|
throw new Error(`unsafe evidenceDir for ${item.caseId}`);
|
|
357
484
|
}
|
|
@@ -382,7 +509,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
382
509
|
if (typeof evidencePath !== "string" || evidencePath === "execution.md" && evidence.some((entry) => entry.path.endsWith("/execution.md")))
|
|
383
510
|
continue;
|
|
384
511
|
try {
|
|
385
|
-
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
|
|
512
|
+
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath, layout));
|
|
386
513
|
}
|
|
387
514
|
catch (error) {
|
|
388
515
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -395,7 +522,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
395
522
|
advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
|
|
396
523
|
let blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
|
|
397
524
|
if (status === "passed") {
|
|
398
|
-
const receiptSummary = await
|
|
525
|
+
const { receipts: caseReceipts, summary: receiptSummary } = await loadCaseBrowserReceiptsWithSummary({
|
|
399
526
|
runDir: input.runDir,
|
|
400
527
|
caseId: item.caseId,
|
|
401
528
|
});
|
|
@@ -409,8 +536,26 @@ export async function materializeFrontendTestResult(input) {
|
|
|
409
536
|
detail: "passed case lacks controller-owned open + interaction/assertion + cleanup receipts",
|
|
410
537
|
});
|
|
411
538
|
}
|
|
539
|
+
else {
|
|
540
|
+
// Controller-side assertion audit: every `find "literal" >=N` declared
|
|
541
|
+
// in the case document must be backed by a successful find receipt
|
|
542
|
+
// whose parsed matchCount satisfies the operator. Pass authority moves
|
|
543
|
+
// from model prose to receipt evidence.
|
|
544
|
+
const caseMarkdown = await readCaseMarkdownForAudit(input.workspaceRoot, item.casePath);
|
|
545
|
+
const declared = extractDeclaredFindAssertions(caseMarkdown);
|
|
546
|
+
const audit = auditDeclaredFindAssertions(declared, caseReceipts);
|
|
547
|
+
if (!audit.ok) {
|
|
548
|
+
status = "blocked";
|
|
549
|
+
blockedReason = audit.reason;
|
|
550
|
+
advisoryFindings.push({
|
|
551
|
+
ruleId: "assertion-not-receipt-backed",
|
|
552
|
+
caseId: item.caseId,
|
|
553
|
+
detail: `declared find assertions without receipt backing: ${audit.missing.join("; ")}`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
412
557
|
}
|
|
413
|
-
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
558
|
+
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath, layout);
|
|
414
559
|
// Parse bounded fixed sections from execution.md. Missing optional
|
|
415
560
|
// summary/steps are advisory only and never downgrade a passed status.
|
|
416
561
|
let executionSummary = "";
|
|
@@ -459,6 +604,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
459
604
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
460
605
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
461
606
|
blocked: cases.filter((item) => item.status === "blocked").length,
|
|
607
|
+
firstTryPassed: cases.filter((item) => item.status === "passed" && !item.rerunAttempt).length,
|
|
462
608
|
};
|
|
463
609
|
const outcome = totals.failed > 0 ? "failed" : totals.blocked > 0 || missing.length > 0 ? "incomplete" : "passed";
|
|
464
610
|
const result = frontendTestResultContractSchema.parse({
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
|
|
4
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
|
|
5
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
|
|
6
|
+
const MINIMAL_STANDARD_SCENARIOS = {
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
id: "frontend-test-standard-scenarios-v1",
|
|
9
|
+
scenarios: [
|
|
10
|
+
{
|
|
11
|
+
id: "STD-FE-SMOKE-ENTRY",
|
|
12
|
+
title: "入口可打开",
|
|
13
|
+
category: "smoke",
|
|
14
|
+
priority: "must",
|
|
15
|
+
testPoints: ["open"],
|
|
16
|
+
minCases: 1,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
function isSafeRelativeDir(value) {
|
|
21
|
+
if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
25
|
+
}
|
|
26
|
+
export function listFrontendTestStandardScenarioCandidates(input) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const add = (relative) => {
|
|
29
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
30
|
+
if (!seen.has(normalized))
|
|
31
|
+
seen.add(normalized);
|
|
32
|
+
};
|
|
33
|
+
add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
34
|
+
const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
|
|
35
|
+
if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
|
|
36
|
+
add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
37
|
+
}
|
|
38
|
+
add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
39
|
+
return [...seen];
|
|
40
|
+
}
|
|
41
|
+
async function readGovernanceRoot(workspaceRoot) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
|
|
44
|
+
return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function copyFrontendTestStandardScenarios(input) {
|
|
51
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
52
|
+
const destRel = frontendTestStandardScenariosRel(layout);
|
|
53
|
+
const destAbs = path.join(input.workspaceRoot, destRel);
|
|
54
|
+
const candidates = listFrontendTestStandardScenarioCandidates({
|
|
55
|
+
governanceRoot: await readGovernanceRoot(input.workspaceRoot),
|
|
56
|
+
});
|
|
57
|
+
await mkdir(path.dirname(destAbs), { recursive: true });
|
|
58
|
+
for (const relative of candidates) {
|
|
59
|
+
const sourceAbs = path.join(input.workspaceRoot, relative);
|
|
60
|
+
try {
|
|
61
|
+
await copyFile(sourceAbs, destAbs);
|
|
62
|
+
return { status: "copied", from: relative, to: destRel };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// try the next candidate
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
|
|
69
|
+
return { status: "fallback", to: destRel };
|
|
70
|
+
}
|
|
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
4
4
|
export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
|
|
5
|
+
/**
|
|
6
|
+
* Shared marker for a project that exposes no usable verification command at
|
|
7
|
+
* generation time. The marker command runs (and logs not-run honestly), but it
|
|
8
|
+
* must never count as successful verification evidence: labels carrying this
|
|
9
|
+
* text are excluded from successful evidence and targets bound to it fail the
|
|
10
|
+
* trace gate instead of passing it.
|
|
11
|
+
*/
|
|
12
|
+
export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
|
|
13
|
+
export function isFrontendNoVerificationMarkerLabel(label) {
|
|
14
|
+
return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
|
|
15
|
+
}
|
|
16
|
+
export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
|
|
5
17
|
async function readJsonFile(filePath) {
|
|
6
18
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
7
19
|
}
|
|
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
|
|
|
52
64
|
const hardIssues = [];
|
|
53
65
|
for (const target of input.contract.verificationTargets) {
|
|
54
66
|
const issues = [];
|
|
55
|
-
|
|
67
|
+
if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
|
|
68
|
+
// A target bound to the not-run marker has no verification behind it;
|
|
69
|
+
// passing it would let the run finish green without any check.
|
|
70
|
+
issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
|
|
71
|
+
}
|
|
72
|
+
const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
|
|
73
|
+
? []
|
|
74
|
+
: (input.labelOwners.get(target.commandLabel) ?? []);
|
|
56
75
|
if (!owners?.length) {
|
|
57
76
|
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
58
77
|
}
|
|
@@ -189,11 +208,17 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
189
208
|
throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
|
|
190
209
|
}
|
|
191
210
|
if (input.evidence) {
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
// The no-op not-run marker never counts as successful verification
|
|
212
|
+
// evidence, even if it executed with exit code 0.
|
|
213
|
+
const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
214
|
+
const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
215
|
+
if (effectiveStaticLabels.length === 0) {
|
|
216
|
+
throw new Error(input.evidence.static.commandLabels.length > 0
|
|
217
|
+
? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
|
|
218
|
+
: "trace: no successful static verification commands in current run");
|
|
194
219
|
}
|
|
195
220
|
if (requiresBehaviorVerification &&
|
|
196
|
-
|
|
221
|
+
effectiveBehaviorLabels.length === 0) {
|
|
197
222
|
throw new Error("trace: no successful behavior verification commands in current run");
|
|
198
223
|
}
|
|
199
224
|
}
|