@tea-agent/loop-agent 0.39.0-next.9 → 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 +262 -69
- 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-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-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
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson,
|
|
8
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
9
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
10
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
11
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
12
|
+
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
11
13
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
12
14
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
13
15
|
/** Canonical contract serialization must stay at or below this byte size. */
|
|
@@ -39,6 +41,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
39
41
|
"component-spec-not-cited",
|
|
40
42
|
"component-choices-missing",
|
|
41
43
|
"contract-too-large",
|
|
44
|
+
"revision-patch-unmergeable",
|
|
42
45
|
]);
|
|
43
46
|
export const frontendPrewriteResultV1Schema = z
|
|
44
47
|
.object({
|
|
@@ -155,32 +158,80 @@ async function selectNode(runDir, primary, fallbacks) {
|
|
|
155
158
|
}
|
|
156
159
|
throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
|
|
157
160
|
}
|
|
158
|
-
async function
|
|
159
|
-
|
|
161
|
+
async function readFinishedNodeRecord(runDir, nodeId) {
|
|
162
|
+
let record;
|
|
163
|
+
try {
|
|
164
|
+
record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
160
169
|
if (record.status !== "FINISHED") {
|
|
161
170
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
162
171
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
return record;
|
|
173
|
+
}
|
|
174
|
+
async function readNodeText(runDir, nodeId) {
|
|
175
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
176
|
+
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
166
177
|
if (!text)
|
|
167
178
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
168
179
|
return text;
|
|
169
180
|
}
|
|
170
|
-
async function
|
|
171
|
-
const record =
|
|
172
|
-
|
|
181
|
+
async function readCanonicalContractFromNode(runDir, nodeId) {
|
|
182
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
183
|
+
if (!record.structuredArtifactPath) {
|
|
184
|
+
throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
|
|
185
|
+
}
|
|
186
|
+
const artifactPath = path.isAbsolute(record.structuredArtifactPath)
|
|
187
|
+
? record.structuredArtifactPath
|
|
188
|
+
: path.join(runDir, record.structuredArtifactPath);
|
|
189
|
+
const resolvedRun = path.resolve(runDir);
|
|
190
|
+
const resolvedArtifact = path.resolve(artifactPath);
|
|
191
|
+
const relative = path.relative(resolvedRun, resolvedArtifact);
|
|
192
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
193
|
+
throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
|
|
194
|
+
}
|
|
195
|
+
const bytes = await readFile(resolvedArtifact);
|
|
196
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
197
|
+
if (record.structuredArtifactSha256 &&
|
|
198
|
+
record.structuredArtifactSha256 !== digest) {
|
|
199
|
+
throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
|
|
200
|
+
}
|
|
201
|
+
if (record.structuredArtifactSchemaId &&
|
|
202
|
+
record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
203
|
+
throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
|
|
204
|
+
}
|
|
205
|
+
const contractText = bytes.toString("utf8");
|
|
206
|
+
let citationsText = "";
|
|
207
|
+
try {
|
|
208
|
+
const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(citationsRaw);
|
|
211
|
+
citationsText =
|
|
212
|
+
typeof parsed.block === "string" ? parsed.block : citationsRaw;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
citationsText = citationsRaw;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code !== "ENOENT")
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return { contractText, citationsText, rawSha256: digest };
|
|
173
223
|
}
|
|
174
224
|
function firstNonEmptyVerdictLine(text) {
|
|
175
225
|
const lines = text
|
|
176
226
|
.split(/\r?\n/)
|
|
177
227
|
.map((line) => line.trim())
|
|
178
228
|
.filter(Boolean);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
229
|
+
// Emphasis stripping (backtick + asterisk) is shared with the runIf
|
|
230
|
+
// selector and node-level output-protocol validation via
|
|
231
|
+
// normalizeVerdictCandidateLine. The gate keeps its own loose
|
|
232
|
+
// pass/request-revision normalization below (prefix match + trailing
|
|
233
|
+
// prose tolerance) for deterministic fail-closed behavior.
|
|
234
|
+
const normalizedLines = lines.map(normalizeVerdictCandidateLine);
|
|
184
235
|
const first = normalizedLines[0] ?? "";
|
|
185
236
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
186
237
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
@@ -397,7 +448,30 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
397
448
|
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
398
449
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
399
450
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
400
|
-
|
|
451
|
+
let planArtifact;
|
|
452
|
+
try {
|
|
453
|
+
planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return finalizePrewrite(input, {
|
|
457
|
+
planNodeId,
|
|
458
|
+
reviewNodeId,
|
|
459
|
+
verdict: "",
|
|
460
|
+
candidateRawSha256: sha256Hex(""),
|
|
461
|
+
candidateJsonSha256: null,
|
|
462
|
+
canonicalSha256: null,
|
|
463
|
+
normalizationActions: [],
|
|
464
|
+
mockStrategy: undefined,
|
|
465
|
+
artifact: undefined,
|
|
466
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
467
|
+
failureCode: "plan-text-unreadable",
|
|
468
|
+
classification: "retryable-invalid",
|
|
469
|
+
openspecReadPaths: [],
|
|
470
|
+
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
471
|
+
openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const candidateRawSha256 = planArtifact.rawSha256;
|
|
401
475
|
const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
|
|
402
476
|
const basePending = {
|
|
403
477
|
planNodeId,
|
|
@@ -416,18 +490,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
416
490
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
417
491
|
openspecPolicy,
|
|
418
492
|
};
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
return finalizePrewrite(input, {
|
|
425
|
-
...basePending,
|
|
426
|
-
classification: "retryable-invalid",
|
|
427
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
428
|
-
failureCode: "plan-text-unreadable",
|
|
429
|
-
});
|
|
430
|
-
}
|
|
493
|
+
const planText = [planArtifact.contractText, planArtifact.citationsText]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join("\n");
|
|
431
496
|
let reviewText;
|
|
432
497
|
try {
|
|
433
498
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -450,59 +515,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
450
515
|
failureCode: "verdict-not-pass",
|
|
451
516
|
});
|
|
452
517
|
}
|
|
453
|
-
|
|
454
|
-
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
455
|
-
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
456
|
-
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
457
|
-
planNodeId === input.config.planFromNodeId &&
|
|
458
|
-
input.config.planFallbackFromNodeIds.length > 0;
|
|
459
|
-
let effectiveContractText = null;
|
|
460
|
-
if (revisionPatchMode) {
|
|
461
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
462
|
-
let originalText;
|
|
463
|
-
try {
|
|
464
|
-
originalText = await readNodeText(input.runDir, originalNodeId);
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
return finalizePrewrite(input, {
|
|
468
|
-
...basePending,
|
|
469
|
-
verdict,
|
|
470
|
-
classification: "retryable-invalid",
|
|
471
|
-
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
472
|
-
failureCode: "contract-invalid",
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
let originalValue;
|
|
476
|
-
let patchValue;
|
|
477
|
-
try {
|
|
478
|
-
originalValue = extractFrontendImplementationJson(originalText);
|
|
479
|
-
patchValue = extractFrontendImplementationJson(planText);
|
|
480
|
-
}
|
|
481
|
-
catch (error) {
|
|
482
|
-
return finalizePrewrite(input, {
|
|
483
|
-
...basePending,
|
|
484
|
-
verdict,
|
|
485
|
-
classification: "retryable-invalid",
|
|
486
|
-
failureReason: `frontend prewrite gate revisionPatch mode failed to parse contract/patch: ${error instanceof Error ? error.message : String(error)}`,
|
|
487
|
-
failureCode: "contract-invalid",
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
let merged;
|
|
491
|
-
try {
|
|
492
|
-
merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
return finalizePrewrite(input, {
|
|
496
|
-
...basePending,
|
|
497
|
-
verdict,
|
|
498
|
-
classification: "retryable-invalid",
|
|
499
|
-
failureReason: `frontend prewrite gate revisionPatch apply failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
500
|
-
failureCode: "contract-invalid",
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
effectiveContractText = serializeDeterministicJson(merged);
|
|
504
|
-
}
|
|
505
|
-
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
518
|
+
const requirementIdSearchText = planArtifact.contractText;
|
|
506
519
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
507
520
|
if (missingIds.length > 0) {
|
|
508
521
|
return finalizePrewrite(input, {
|
|
@@ -550,14 +563,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
550
563
|
workspaceRoot,
|
|
551
564
|
});
|
|
552
565
|
}
|
|
553
|
-
// Candidate -> canonical conversion. The gate consumes the
|
|
554
|
-
//
|
|
566
|
+
// Candidate -> canonical conversion. The gate consumes the producing node's
|
|
567
|
+
// hash-bound canonical artifact and never re-merges plan/revision stdout.
|
|
555
568
|
let analysis;
|
|
556
569
|
try {
|
|
557
570
|
analysis = await analyzeFrontendImplementationContract({
|
|
558
571
|
runDir: input.runDir,
|
|
559
|
-
|
|
560
|
-
rawContractText: effectiveContractText ?? undefined,
|
|
572
|
+
rawContractText: planArtifact.contractText,
|
|
561
573
|
sourceBinding: input.sourceBinding,
|
|
562
574
|
});
|
|
563
575
|
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
5
5
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
6
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE, } from "./frontend-verification-trace.js";
|
|
6
7
|
export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
|
|
7
8
|
export const frontendRepairFailureClassSchema = z.enum([
|
|
8
9
|
"typecheck",
|
|
@@ -82,6 +83,12 @@ export function classifyFrontendFailure(input) {
|
|
|
82
83
|
if (input.failureCategory === "write-guard") {
|
|
83
84
|
return "path";
|
|
84
85
|
}
|
|
86
|
+
if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
|
|
87
|
+
// Verification unavailability is a generation-time contract gap: no
|
|
88
|
+
// authorized verification command exists. Bounded repair cannot fix it;
|
|
89
|
+
// the operator must declare commands and regenerate the DAG.
|
|
90
|
+
return "contract";
|
|
91
|
+
}
|
|
85
92
|
if (input.traceTarget || input.commandCategory === "trace") {
|
|
86
93
|
return "trace";
|
|
87
94
|
}
|
|
@@ -424,6 +431,13 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
424
431
|
: !baseEligible
|
|
425
432
|
? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
|
|
426
433
|
: `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
|
|
434
|
+
if (!eligible &&
|
|
435
|
+
`${primary.record.stderr ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
|
|
436
|
+
// Surface the actionable unavailable-verification reason in the
|
|
437
|
+
// assessment (and the non-repairable gate error) instead of a
|
|
438
|
+
// generic class label.
|
|
439
|
+
reason = FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE;
|
|
440
|
+
}
|
|
427
441
|
}
|
|
428
442
|
// fixScope and changedPaths must stay inside the implement writeSet.
|
|
429
443
|
if (evidence.fixScope?.length) {
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Deterministic frontend-implementation risk classifier (M4).
|
|
3
3
|
* Pure function: no I/O, no model, no side effects.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dependency-installation language in task text. Mentioning package.json or
|
|
7
|
+
* allowing its path is NOT dependency introduction by itself; the manifest
|
|
8
|
+
* path check in classifyFrontendRisk requires this language to escalate.
|
|
9
|
+
*/
|
|
10
|
+
const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
|
|
5
11
|
const HIGH_RISK_PATTERNS = [
|
|
6
12
|
{
|
|
7
13
|
id: "new-route",
|
|
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
|
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
id: "new-dependency",
|
|
28
|
-
re:
|
|
34
|
+
re: DEPENDENCY_INSTALL_LANGUAGE,
|
|
29
35
|
},
|
|
30
36
|
{
|
|
31
37
|
id: "schema-api-change",
|
|
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
|
|
|
88
94
|
const paths = input.allowedPaths ?? [];
|
|
89
95
|
const spread = pathSpreadScore(paths);
|
|
90
96
|
if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
|
|
91
|
-
|
|
97
|
+
if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
|
|
98
|
+
signals.push("new-dependency");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Editing manifest scripts/config is not dependency introduction;
|
|
102
|
+
// record the rejected escalation instead of forcing full gates.
|
|
103
|
+
rejectedSignals.push("new-dependency:manifest-path-without-install-language");
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
// Explicit multi-domain language only — path count alone is not high-risk
|
|
94
107
|
if (spread >= 3 &&
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isPlaywrightCliCommand } from "../../shared/playwright-cli-command-policy.js";
|
|
4
|
+
import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
|
|
5
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
|
|
4
6
|
const COMMAND_FENCE_LANGUAGE = /^(?:|bash|sh|shell|zsh|fish|console|terminal|command|cmd|powershell|pwsh|javascript|js|typescript|ts|python|py)$/i;
|
|
5
7
|
const LIST_ITEM = /^\s*(?:[-+*]|\d+[.)])\s+(.+)$/;
|
|
6
8
|
const INDENTED_CODE = /^(?: {4,}|\t+)(\S.*)$/;
|
|
7
9
|
const EXPLICIT_COMMAND = /^(?:(?:shell|terminal)(?:\s+command)?|command|run command|execute command)\s*:\s*(.+)$/i;
|
|
8
10
|
const BLOCKED_REASON_PREFIX = /^(?:(?:blocked|forbidden|reject(?:ed)?|disallow(?:ed)?|prohibited)(?:\s+reason)?\s*(?::|\bbecause\b)|(?:do not|must not|never)\s+(?:run|execute|use)\b)/i;
|
|
9
11
|
/** Standalone blockedReason / environment tokens (not executable commands). */
|
|
10
|
-
const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
|
|
12
|
+
const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|unique-control-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
|
|
11
13
|
const BLOCKED_PROSE_HINT = /(?:blockedReason|blocked\s*reason|environmentProbe|evidenceDir|\bunavailable\b|不可用|写\s*blocked|写入\s*blocked|伪命令|自然语言|元数据|fail-closed|探测失败)/i;
|
|
12
14
|
const IMPERATIVE_COMMAND = /^(?:(?:run|execute|use)(?:\s+(?:the\s+)?(?:(?:shell|terminal)\s+)?command)?|in\s+(?:the\s+)?(?:shell|terminal|console)\s*,?\s*(?:run|execute|use))\s*:?\s+(.+)$/i;
|
|
13
15
|
const INLINE_CODE_STEP = /^`([^`\r\n]+)`[.!?]?$/;
|
|
16
|
+
/** Assertion-count annotations appended to a playwright-cli literal (e.g. `find "锚点">=1`, `>=N`, `≥N`, `(≥1)`) document an expected match count; they are documentation, not shell arguments. */
|
|
17
|
+
const ASSERTION_COUNT_SUFFIX = /\s*[((]?\s*(?:>=|≥|>|超过|至少)\s*\d+\s*[))]?\s*$/;
|
|
14
18
|
const AUTOMATION_EXECUTABLE = /^(?:playwright-cli\b|playwright\b|@playwright\/test\b|npx\b|npm\b|pnpm\b|yarn\b|bunx?\b|node(?:js)?\b|python(?:3)?\b|bash\b|sh\b|zsh\b|fish\b|powershell\b|pwsh\b|cmd(?:\.exe)?\b|cypress\b|selenium\b|webdriverio\b|chromedriver\b|google-chrome\b|chrome\b|firefox\b|curl\b|wget\b)/i;
|
|
15
19
|
const SHELL_WRAPPED_EXECUTABLE = /^(?:sudo\s+|env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s]+)*\s+|(?:\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\/)[^\s]+)/i;
|
|
16
20
|
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)/;
|
|
@@ -44,6 +48,11 @@ function isBlockedReason(value) {
|
|
|
44
48
|
return true;
|
|
45
49
|
if (/^blockedReason\s*[:=]/i.test(trimmed))
|
|
46
50
|
return true;
|
|
51
|
+
// Expected-outcome field lines ("status=blocked" / "status: failed") document
|
|
52
|
+
// the intended terminal state of a designed-negative case; they are
|
|
53
|
+
// case-result metadata, never executable commands.
|
|
54
|
+
if (/^["'`]?status["'`]?\s*[:=]\s*["'`]?(passed|failed|blocked)["'`]?\s*[.。]?$/i.test(trimmed))
|
|
55
|
+
return true;
|
|
47
56
|
return false;
|
|
48
57
|
}
|
|
49
58
|
/**
|
|
@@ -238,6 +247,12 @@ function commandGateIssue(input) {
|
|
|
238
247
|
if (input.instruction.fromFence && isBlockedReason(input.instruction.command)) {
|
|
239
248
|
return null;
|
|
240
249
|
}
|
|
250
|
+
// An expected-match-count annotation (`find "锚点">=1`, with or without a
|
|
251
|
+
// playwright-cli prefix) documents the assertion; it is not part of the
|
|
252
|
+
// command and never executable input.
|
|
253
|
+
if (/^(?:playwright-cli\s+)?find\s+"[^"]*"/i.test(input.instruction.command) && ASSERTION_COUNT_SUFFIX.test(input.instruction.command)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
241
256
|
const commandMatch = input.instruction.command.match(/^playwright-cli\s+([^\s`]+)/i);
|
|
242
257
|
if (commandMatch) {
|
|
243
258
|
const command = commandMatch[1].toLowerCase().replace(/^[`'"]+|[`'":,,。→]+$/g, "");
|
|
@@ -276,13 +291,20 @@ function commandGateIssue(input) {
|
|
|
276
291
|
};
|
|
277
292
|
}
|
|
278
293
|
export async function validateFrontendCaseChecklist(input) {
|
|
279
|
-
const
|
|
294
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
295
|
+
const root = path.join(input.workspaceRoot, layout.casesDir);
|
|
280
296
|
const draft = path.join(root, "manifest.draft.json");
|
|
281
297
|
const final = path.join(root, "manifest.json");
|
|
282
298
|
const manifestPath = await exists(draft) ? draft : await exists(final) ? final : "";
|
|
283
299
|
if (!manifestPath)
|
|
284
300
|
throw new Error("checklist: missing manifest.draft.json or manifest.json");
|
|
285
|
-
|
|
301
|
+
let manifest;
|
|
302
|
+
try {
|
|
303
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new Error("checklist: manifest is malformed JSON");
|
|
307
|
+
}
|
|
286
308
|
if (!Array.isArray(manifest.cases) || manifest.cases.length === 0)
|
|
287
309
|
throw new Error("checklist: empty cases");
|
|
288
310
|
const declaredAc = new Set(input.declaredAcIds ?? []);
|
|
@@ -299,7 +321,7 @@ export async function validateFrontendCaseChecklist(input) {
|
|
|
299
321
|
if (typeof item?.caseId === "string" && /^AC-/i.test(item.caseId))
|
|
300
322
|
issues.push({ ruleId: "case-id-is-ac", caseId: id, detail: "caseId must not be an acceptance id" });
|
|
301
323
|
const casePath = typeof item?.casePath === "string" ? item.casePath : "";
|
|
302
|
-
const expectedPath = typeof item?.caseId === "string" ?
|
|
324
|
+
const expectedPath = typeof item?.caseId === "string" ? frontendTestCasePath(layout, item.caseId) : "";
|
|
303
325
|
const absolute = path.resolve(input.workspaceRoot, casePath);
|
|
304
326
|
const relative = path.relative(input.workspaceRoot, absolute);
|
|
305
327
|
if (!casePath || relative.startsWith("..") || path.isAbsolute(relative) || !(await exists(absolute))) {
|
|
@@ -334,6 +356,10 @@ export async function validateFrontendCaseChecklist(input) {
|
|
|
334
356
|
for (const ac of item.acIds) {
|
|
335
357
|
if (typeof ac !== "string" || !acIdRe.test(ac))
|
|
336
358
|
issues.push({ ruleId: "ac-id-shape", caseId: id, casePath, detail: String(ac) });
|
|
359
|
+
else if (declaredAc.size > 0 && isTruncatedFrontendAcId(ac))
|
|
360
|
+
issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
|
|
361
|
+
else if (declaredAc.size === 0 && isTruncatedWhenUndeclaredFrontendAcId(ac))
|
|
362
|
+
issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
|
|
337
363
|
else if (declaredAc.size > 0 && !declaredAc.has(ac))
|
|
338
364
|
issues.push({ ruleId: "unknown-ac", caseId: id, casePath, detail: `${ac} not in sourceBinding` });
|
|
339
365
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile, rename, unlink, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
|
|
4
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, frontendTestEvidenceDir, frontendTestManifestDraftPath, frontendTestManifestPath, } from "./frontend-test-layout.js";
|
|
3
5
|
const DIMENSIONS = new Set(["core", "boundary", "flow", "backend"]);
|
|
4
6
|
function fail(ruleId, detail) {
|
|
5
7
|
const msg = JSON.stringify({ ruleId, detail: String(detail ?? "") });
|
|
@@ -18,8 +20,9 @@ function fail(ruleId, detail) {
|
|
|
18
20
|
* present). When empty, ac-id-shape still applies.
|
|
19
21
|
*/
|
|
20
22
|
export async function materializeFrontendTestCaseManifest(input) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
24
|
+
const draft = path.join(input.workspaceRoot, frontendTestManifestDraftPath(layout));
|
|
25
|
+
const file = path.join(input.workspaceRoot, frontendTestManifestPath(layout));
|
|
23
26
|
const tmp = `${file}.tmp`;
|
|
24
27
|
const maxCases = input.maxCases ?? 32;
|
|
25
28
|
const declaredAc = new Set(input.declaredAcIds ?? []);
|
|
@@ -62,12 +65,16 @@ export async function materializeFrontendTestCaseManifest(input) {
|
|
|
62
65
|
for (const ac of c.acIds) {
|
|
63
66
|
if (typeof ac !== "string" || !acIdRe.test(ac))
|
|
64
67
|
fail("ac-id-shape", `acIds entry must be AC-* acceptance id, not caseId: ${ac}`);
|
|
68
|
+
if ((declaredAc.size > 0
|
|
69
|
+
? isTruncatedFrontendAcId(ac)
|
|
70
|
+
: isTruncatedWhenUndeclaredFrontendAcId(ac)))
|
|
71
|
+
fail("ac-id-truncated", `acIds entry is a truncated acceptance id (missing feature prefix or sequence number): ${ac}`);
|
|
65
72
|
if (declaredAc.size > 0 && !declaredAc.has(ac))
|
|
66
73
|
fail("unknown-ac", `${ac} not in sourceBinding; repair generator input or AC list`);
|
|
67
74
|
acIds.push(ac);
|
|
68
75
|
}
|
|
69
|
-
const casePath =
|
|
70
|
-
const evidenceDir =
|
|
76
|
+
const casePath = frontendTestCasePath(layout, c.caseId);
|
|
77
|
+
const evidenceDir = frontendTestEvidenceDir(layout, c.caseId);
|
|
71
78
|
for (const [k, v] of [
|
|
72
79
|
["casePath", casePath],
|
|
73
80
|
["evidenceDir", evidenceDir],
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { markdownSection } from "./frontend-test-markdown.js";
|
|
4
|
+
import { defaultFrontendTestLayout, frontendTestCasePath, } from "./frontend-test-layout.js";
|
|
3
5
|
const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
|
|
4
6
|
const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
|
|
5
7
|
const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
|
|
6
8
|
const SECTION_ALIASES = {
|
|
7
9
|
purpose: ["Test Purpose", "测试目的", "测试场景"],
|
|
8
10
|
source: ["Source References", "需求依据"],
|
|
9
|
-
preconditions: ["Preconditions", "前置条件"],
|
|
10
|
-
steps: ["Steps", "操作步骤"],
|
|
11
|
+
preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
|
|
12
|
+
steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
|
|
11
13
|
expected: ["Expected Results", "预期结果"],
|
|
12
14
|
automation: ["Automation Notes", "自动化映射", "自动化说明"],
|
|
13
15
|
};
|
|
14
|
-
function section(body, names) {
|
|
15
|
-
const marker = new RegExp(`^###\\s+(?:${names.map((v) => v.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")).join("|")})\\s*$`, "mi");
|
|
16
|
-
const hit = marker.exec(body);
|
|
17
|
-
if (!hit)
|
|
18
|
-
return "";
|
|
19
|
-
const rest = body.slice(hit.index + hit[0].length);
|
|
20
|
-
const next = /^###\s+/m.exec(rest);
|
|
21
|
-
return rest.slice(0, next?.index ?? rest.length).trim();
|
|
22
|
-
}
|
|
23
16
|
function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
|
|
24
17
|
function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
|
|
25
18
|
export async function validateFrontendCaseContent(input) {
|
|
26
|
-
const
|
|
19
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
20
|
+
const root = path.join(input.workspaceRoot, layout.casesDir);
|
|
27
21
|
const manifestPath = path.join(root, "manifest.json");
|
|
28
22
|
const findings = [];
|
|
29
23
|
let manifest;
|
|
@@ -57,7 +51,7 @@ export async function validateFrontendCaseContent(input) {
|
|
|
57
51
|
findings.push({ ruleId: "unknown-ac", caseId: id, detail: `${ac} is not declared by the task` });
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
|
-
const file = path.join(input.workspaceRoot, item?.casePath ||
|
|
54
|
+
const file = path.join(input.workspaceRoot, item?.casePath || frontendTestCasePath(layout, id));
|
|
61
55
|
let body = "";
|
|
62
56
|
try {
|
|
63
57
|
body = await readFile(file, "utf8");
|
|
@@ -69,9 +63,9 @@ export async function validateFrontendCaseContent(input) {
|
|
|
69
63
|
if (PLACEHOLDER.test(body))
|
|
70
64
|
findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
|
|
71
65
|
for (const [key, names] of Object.entries(SECTION_ALIASES))
|
|
72
|
-
if (!
|
|
66
|
+
if (!markdownSection(body, names))
|
|
73
67
|
findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
|
|
74
|
-
const steps =
|
|
68
|
+
const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
|
|
75
69
|
if (!hasList(steps))
|
|
76
70
|
findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
|
|
77
71
|
if (!hasList(expected) || !hasAssertion(expected))
|
|
@@ -97,7 +91,8 @@ function render(findings) {
|
|
|
97
91
|
}
|
|
98
92
|
export async function writeFrontendCaseQualityReports(input) {
|
|
99
93
|
const result = await validateFrontendCaseContent(input);
|
|
100
|
-
const
|
|
94
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
95
|
+
const dir = path.join(input.workspaceRoot, input.outputDir ?? layout.reportsDir);
|
|
101
96
|
await mkdir(dir, { recursive: true });
|
|
102
97
|
await writeFile(path.join(dir, "frontend-test-case-quality-advisory.md"), result.markdown, "utf8");
|
|
103
98
|
await writeFile(path.join(dir, "frontend-test-case-quality-advisory.html"), result.html, "utf8");
|