@tea-agent/loop-agent 0.39.0-next.9 → 0.39.1
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 +269 -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 +16 -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/update/runtime-activity.js +1 -29
- 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 +555 -34
- 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 +27 -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 +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +28 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +3 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +20 -1
- package/dist/task/contract/schema.js +8 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +18 -2
- 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/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +320 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- 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 +46 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +111 -7
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-B4i9cVxX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +406 -0
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-aGmcjgTn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-Biyo-tqA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +6 -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/app/useTaskWizard.js +73 -1
- 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/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +67 -1
- 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 +60 -0
- package/dist/worker/observe/static/index.html +5 -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/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +85 -0
- package/dist/worker/observe/static/views/dag-inspector.js +912 -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/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +856 -54
- 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 +343 -33
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +454 -36
- 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 +110 -61
- 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 +465 -6
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +293 -84
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- 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 +1019 -209
- package/dist/workflows/dag/node-execution.js +228 -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 +185 -12
- package/dist/workflows/dag/validate.js +64 -20
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +6 -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 +2 -2
- 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 +681 -24
- 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/loop-agent/references/hybrid-dag.md +7 -0
- 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
|
@@ -6,6 +6,7 @@ import { createWriteStream } from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
|
|
9
|
+
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
9
10
|
import { parseJacocoXml, } from "./backend-test-coverage-contract.js";
|
|
10
11
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
@@ -14,10 +15,22 @@ const execFileAsync = promisify(execFile);
|
|
|
14
15
|
// heading width cannot collapse the whole manifest/report to zero cases.
|
|
15
16
|
const CASE_ID = /\bBE-[A-Z0-9_-]+-\d{2,3}\b/g;
|
|
16
17
|
const CANONICAL_CASE_ID = /\bBE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}\b/;
|
|
18
|
+
const SHARED_SETUP_REF_LINE = /^\s*引用前置\s*:\s*(\S+)\s*#\s*([A-Za-z0-9_.\-\u4e00-\u9fa5]+)\s*$/;
|
|
19
|
+
const sharedSetupAnchorRe = /^SS-[A-Z0-9][A-Z0-9._-]*$/i;
|
|
20
|
+
export function extractBackendSharedSetupRefs(markdown) {
|
|
21
|
+
const refs = [];
|
|
22
|
+
for (const line of markdown.split("\n")) {
|
|
23
|
+
const match = SHARED_SETUP_REF_LINE.exec(line);
|
|
24
|
+
if (match) {
|
|
25
|
+
refs.push({ raw: match[0].trim(), path: match[1], anchor: match[2] });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return refs;
|
|
29
|
+
}
|
|
17
30
|
const AC_ID = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
18
31
|
const SECRET = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization)\s*[:=]\s*\S+/i;
|
|
19
32
|
const SECRET_ASSIGNMENT = /\b(password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization|cookie|set-cookie|credential)\b\s*[:=]\s*([^\r\n,;]+)/gi;
|
|
20
|
-
const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA
|
|
33
|
+
const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b|\bsk[-_][-_A-Za-z0-9]{10,}\b/i;
|
|
21
34
|
const PLACEHOLDER_SECRET_VALUE = /^(?:<[^>]+>|\$\{[^}]+\}|\*+|x+|redacted|masked|example(?:[-_].*)?|sample(?:[-_].*)?|dummy(?:[-_].*)?|test(?:[-_].*)?|your[-_].*)$/i;
|
|
22
35
|
const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|后续补充|暂不实现|接口正常|结果正确|按实际情况处理/i;
|
|
23
36
|
const CASE_SECTION_ALIASES = {
|
|
@@ -187,13 +200,170 @@ export async function inspectBackendTestEnvironment(input) {
|
|
|
187
200
|
serverEntries,
|
|
188
201
|
};
|
|
189
202
|
}
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Filenames declared by the canonical `## Module Index` section of the backend
|
|
205
|
+
* README. Only a unique exact `## Module Index` heading is authoritative; a
|
|
206
|
+
* missing/duplicate section yields an empty declaration set (callers then fall
|
|
207
|
+
* back to the whole directory so legacy workspaces without a manifest keep
|
|
208
|
+
* their prior behaviour). Reads `[x](./x.md)`, `` `testcase/md/<x>.md` `` and
|
|
209
|
+
* inline `x.md` tokens inside the section; README.md itself is never a module.
|
|
210
|
+
*/
|
|
211
|
+
export function extractDeclaredModuleIndexFilenames(readme) {
|
|
212
|
+
const lines = readme
|
|
213
|
+
.replaceAll("\r\n", "\n")
|
|
214
|
+
.replaceAll("\r", "\n")
|
|
215
|
+
.split("\n");
|
|
216
|
+
const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
|
|
217
|
+
if (headings.length !== 1)
|
|
218
|
+
return [];
|
|
219
|
+
const start = headings[0] + 1;
|
|
220
|
+
const relativeEnd = lines
|
|
221
|
+
.slice(start)
|
|
222
|
+
.findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
223
|
+
const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
|
|
224
|
+
const section = lines.slice(start, end).join("\n");
|
|
225
|
+
return unique([...section.matchAll(/`?[A-Za-z0-9_.-]+\.md`?/g)]
|
|
226
|
+
.map((match) => match[0].replaceAll("`", ""))
|
|
227
|
+
.filter((name) => name.toLowerCase() !== "readme.md"));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Count exact `## Module Index` headings in the backend README.
|
|
231
|
+
*/
|
|
232
|
+
function moduleIndexHeadingCountIn(readme) {
|
|
233
|
+
return readme
|
|
234
|
+
.replaceAll("\r\n", "\n")
|
|
235
|
+
.replaceAll("\r", "\n")
|
|
236
|
+
.split("\n")
|
|
237
|
+
.filter((line) => line.trim() === "## Module Index").length;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Machine-readable fail-closed finding for an invalid backend-test Module Index.
|
|
241
|
+
* These are distinct states (missing/unreadable/duplicate/empty-unparsable) so a
|
|
242
|
+
* stripped Module Index can never silently fall back to a whole-directory scan.
|
|
243
|
+
*/
|
|
244
|
+
export function backendManifestStatusFinding(manifestStatus, moduleIndexHeadingCount = 0) {
|
|
245
|
+
switch (manifestStatus) {
|
|
246
|
+
case "missing":
|
|
247
|
+
return "README is missing; backend-test current round fails closed without a declarative Module Index";
|
|
248
|
+
case "unreadable":
|
|
249
|
+
return "README is unreadable; backend-test current round fails closed without a usable Module Index";
|
|
250
|
+
case "duplicate":
|
|
251
|
+
return moduleIndexHeadingCount === 0
|
|
252
|
+
? "README Module Index heading is missing; backend-test current round fails closed without a declarative Module Index"
|
|
253
|
+
: `duplicate ## Module Index heading (${moduleIndexHeadingCount} occurrences); backend-test current round fails closed without a unique Module Index`;
|
|
254
|
+
case "empty-unparsable":
|
|
255
|
+
return "README Module Index is empty/unparsable; backend-test current round fails closed with no declared module";
|
|
256
|
+
case "valid":
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Resolve the current-round Markdown module manifest. The README `## Module
|
|
262
|
+
* Index` is the authoritative source for which module files belong to this
|
|
263
|
+
* round. Fail closed on every non-`valid` state (missing/unreadable/duplicate/
|
|
264
|
+
* empty-unparsable): the backend-test current round must NEVER re-scan the whole
|
|
265
|
+
* directory, or historical `.md` files could leak back into Case/coverage/
|
|
266
|
+
* partition facts.
|
|
267
|
+
*
|
|
268
|
+
* - `manifestStatus==="valid"` — unique heading with >= 1 declared `.md` file.
|
|
269
|
+
* - `missing` — README file does not exist.
|
|
270
|
+
* - `unreadable` — README exists but could not be read.
|
|
271
|
+
* - `duplicate` — README has 0 or duplicate `## Module Index`.
|
|
272
|
+
* - `empty-unparsable` — unique heading but no `.md` filename parsed.
|
|
273
|
+
* - `declaredMdFiles` — declared filenames that exist on disk (relative paths).
|
|
274
|
+
* - `declaredMissing` — declared filenames with no file on disk (fail-closed
|
|
275
|
+
* evidence, never silently dropped).
|
|
276
|
+
* - `unmanifested` — existing `.md` files that the Module Index does not
|
|
277
|
+
* declare (non-silent advisory; they must not enter Case/coverage/partition).
|
|
278
|
+
*/
|
|
279
|
+
export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
|
|
280
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
281
|
+
const readmePath = path.join(workspaceRoot, ...resolved.readmePath.split("/"));
|
|
282
|
+
const invalid = (manifestStatus, moduleIndexHeadingCount) => ({
|
|
283
|
+
hasManifest: false,
|
|
284
|
+
manifestStatus,
|
|
285
|
+
moduleIndexHeadingCount,
|
|
286
|
+
declaredStems: [],
|
|
287
|
+
declaredMdFiles: [],
|
|
288
|
+
declaredMissing: [],
|
|
289
|
+
unmanifested: [],
|
|
290
|
+
});
|
|
291
|
+
let readme = "";
|
|
292
|
+
if (await exists(readmePath)) {
|
|
293
|
+
try {
|
|
294
|
+
readme = await readFile(readmePath, "utf8");
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Unreadable README is fail-closed (not silently treated as no manifest).
|
|
298
|
+
return invalid("unreadable", 0);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
return invalid("missing", 0);
|
|
303
|
+
}
|
|
304
|
+
const moduleIndexHeadingCount = moduleIndexHeadingCountIn(readme);
|
|
305
|
+
if (moduleIndexHeadingCount !== 1) {
|
|
306
|
+
return invalid("duplicate", moduleIndexHeadingCount);
|
|
307
|
+
}
|
|
308
|
+
const declaredStems = extractDeclaredModuleIndexFilenames(readme);
|
|
309
|
+
if (declaredStems.length === 0) {
|
|
310
|
+
return invalid("empty-unparsable", moduleIndexHeadingCount);
|
|
311
|
+
}
|
|
312
|
+
const hasManifest = true;
|
|
313
|
+
const directory = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
|
|
314
|
+
let existingNames = [];
|
|
315
|
+
try {
|
|
316
|
+
existingNames = (await readdir(directory, { withFileTypes: true }))
|
|
317
|
+
.filter((entry) => entry.isFile() &&
|
|
318
|
+
entry.name.toLowerCase().endsWith(".md") &&
|
|
319
|
+
entry.name.toLowerCase() !== "readme.md")
|
|
320
|
+
.map((entry) => entry.name);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// missing markdown directory -> no existing modules
|
|
324
|
+
}
|
|
325
|
+
const declaredByLower = new Map();
|
|
326
|
+
for (const name of declaredStems) {
|
|
327
|
+
const base = path.posix.basename(name);
|
|
328
|
+
declaredByLower.set(base.toLowerCase(), base);
|
|
329
|
+
}
|
|
330
|
+
const declaredMdFiles = [];
|
|
331
|
+
const declaredMissing = [];
|
|
332
|
+
for (const base of declaredByLower.values()) {
|
|
333
|
+
const disk = existingNames.find((name) => name.toLowerCase() === base.toLowerCase());
|
|
334
|
+
if (disk)
|
|
335
|
+
declaredMdFiles.push(`${resolved.markdownDir}/${disk}`);
|
|
336
|
+
else
|
|
337
|
+
declaredMissing.push(`${resolved.markdownDir}/${base}`);
|
|
338
|
+
}
|
|
339
|
+
const unmanifested = existingNames
|
|
340
|
+
.filter((name) => !declaredByLower.has(name.toLowerCase()))
|
|
341
|
+
.map((name) => `${resolved.markdownDir}/${name}`);
|
|
342
|
+
return {
|
|
343
|
+
hasManifest,
|
|
344
|
+
manifestStatus: "valid",
|
|
345
|
+
moduleIndexHeadingCount,
|
|
346
|
+
declaredStems,
|
|
347
|
+
declaredMdFiles: declaredMdFiles.sort(),
|
|
348
|
+
declaredMissing: declaredMissing.sort(),
|
|
349
|
+
unmanifested: unmanifested.sort(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export async function markdownFiles(root, layout) {
|
|
353
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
354
|
+
const directory = path.join(root, ...resolved.markdownDir.split("/"));
|
|
192
355
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
193
|
-
|
|
356
|
+
const all = entries
|
|
194
357
|
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
195
358
|
.map((entry) => path.join(directory, entry.name))
|
|
196
359
|
.sort();
|
|
360
|
+
const manifest = await resolveBackendTestManifestModules(root, resolved);
|
|
361
|
+
if (manifest.manifestStatus !== "valid")
|
|
362
|
+
return [];
|
|
363
|
+
const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(root, ...rel.split("/")).toLowerCase()));
|
|
364
|
+
return all
|
|
365
|
+
.filter((file) => declaredAbs.has(file.toLowerCase()))
|
|
366
|
+
.sort();
|
|
197
367
|
}
|
|
198
368
|
function canonicalBackendCaseId(value) {
|
|
199
369
|
const upper = value.toUpperCase();
|
|
@@ -303,6 +473,8 @@ function isCleanupStatement(value) {
|
|
|
303
473
|
return false;
|
|
304
474
|
if (/\b(?:assert|pytest\.fail|raise)\b/.test(trimmed))
|
|
305
475
|
return false;
|
|
476
|
+
if (/\b[A-Za-z_][A-Za-z0-9_.]*request\s*\(\s*["']DELETE["']/i.test(trimmed))
|
|
477
|
+
return true;
|
|
306
478
|
return /(?:^|[._])(?:cleanup|clean_up|teardown|delete|remove|close|release|rollback|disconnect|dispose|unlink)(?:[_(.]|$)/i.test(trimmed);
|
|
307
479
|
}
|
|
308
480
|
function findUnsafeSwallowedException(source) {
|
|
@@ -326,7 +498,18 @@ function findUnsafeSwallowedException(source) {
|
|
|
326
498
|
continue;
|
|
327
499
|
}
|
|
328
500
|
const exceptionType = exceptMatch[2]?.trim() || "bare except";
|
|
329
|
-
|
|
501
|
+
const narrowDecodeTypes = exceptionType
|
|
502
|
+
.replace(/[()]/g, "")
|
|
503
|
+
.split(",")
|
|
504
|
+
.map((value) => value.trim())
|
|
505
|
+
.filter(Boolean);
|
|
506
|
+
const precedingDecodeRegion = lines.slice(Math.max(0, index - 8), index).join("\n");
|
|
507
|
+
if (narrowDecodeTypes.length > 0 &&
|
|
508
|
+
narrowDecodeTypes.every((value) => value === "UnicodeDecodeError" || value === "json.JSONDecodeError") &&
|
|
509
|
+
/\.json\s*\(\s*\)/.test(precedingDecodeRegion)) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!new Set(["requests.RequestException", "urllib.error.URLError", "httpx.RequestError"]).has(exceptionType)) {
|
|
330
513
|
return exceptionType;
|
|
331
514
|
}
|
|
332
515
|
let tryIndex = index - 1;
|
|
@@ -366,9 +549,18 @@ function httpLoggingFindings(relativeScript, source) {
|
|
|
366
549
|
if (!hasHttpRequest)
|
|
367
550
|
return [];
|
|
368
551
|
const findings = [];
|
|
369
|
-
const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
|
|
370
|
-
|
|
371
|
-
|
|
552
|
+
const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
|
|
553
|
+
|| /["']record["']\s*:\s*["']request["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
554
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
555
|
+
|| /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)REQUEST\b/i.test(source) && /["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
|
|
556
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source);
|
|
557
|
+
const hasResponseLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:response|响应)[^\n]*(?:status|status_code|body|json|text|result|结果)/i.test(source)
|
|
558
|
+
|| /["']record["']\s*:\s*["']response["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
559
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
560
|
+
|| /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)RESPONSE\b/i.test(source) && /["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
|
|
561
|
+
|| /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source);
|
|
562
|
+
const hasSanitizer = /\b(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(|\b[A-Za-z0-9_]*(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(/i.test(source)
|
|
563
|
+
|| /(?:SENSITIVE_KEYS|sensitive_keys)[\s\S]{0,1600}["']<redacted>["']/i.test(source);
|
|
372
564
|
const hasBoundedOutput = /(?:MAX_[A-Z_]*(?:LOG|BODY|RESPONSE)|max_(?:log|body|response)|truncate|truncat|\[:\s*\d{3,}\s*\])/i.test(source);
|
|
373
565
|
if (!hasRequestLog)
|
|
374
566
|
findings.push(`request logging evidence is missing in mapped backend pytest: ${relativeScript}`);
|
|
@@ -403,22 +595,34 @@ export function hasBlockingBackendMarkdownModuleStemFindings(report) {
|
|
|
403
595
|
return /^## Module Stem Status\s*\r?\n\s*BLOCKED\s*$/m.test(report);
|
|
404
596
|
}
|
|
405
597
|
export async function validateBackendMarkdownCases(input) {
|
|
598
|
+
const layout = input.layout ?? resolveBackendTestLayout(undefined);
|
|
406
599
|
const findings = [];
|
|
407
600
|
const safetyFindings = [];
|
|
408
601
|
const moduleStemFindings = [];
|
|
409
602
|
let files = [];
|
|
410
603
|
try {
|
|
411
|
-
files = await markdownFiles(input.workspaceRoot);
|
|
604
|
+
files = await markdownFiles(input.workspaceRoot, layout);
|
|
412
605
|
}
|
|
413
606
|
catch {
|
|
414
|
-
findings.push(
|
|
607
|
+
findings.push(`missing ${layout.markdownDir} directory`);
|
|
415
608
|
}
|
|
416
|
-
const readme = path.join(input.workspaceRoot, "
|
|
609
|
+
const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
|
|
417
610
|
if (!(await exists(readme)))
|
|
418
|
-
findings.push(
|
|
611
|
+
findings.push(`missing ${layout.readmePath}`);
|
|
612
|
+
const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
|
|
613
|
+
// Fail closed on README Module Index entries that were declared but never
|
|
614
|
+
// materialized — never let a declared module silently vanish from the round.
|
|
615
|
+
if (manifest.declaredMissing.length > 0) {
|
|
616
|
+
findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
|
|
617
|
+
}
|
|
618
|
+
// Fail closed on every non-valid Module Index state: without a valid unique
|
|
619
|
+
// Module Index the current round must not re-scan historical `.md` files.
|
|
620
|
+
if (manifest.manifestStatus !== "valid") {
|
|
621
|
+
findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
|
|
622
|
+
}
|
|
419
623
|
const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
|
|
420
624
|
if (moduleFiles.length === 0)
|
|
421
|
-
findings.push(
|
|
625
|
+
findings.push(`no ${layout.markdownDir} module case file found`);
|
|
422
626
|
const seen = new Map();
|
|
423
627
|
const coveredAc = new Set();
|
|
424
628
|
let caseCount = 0;
|
|
@@ -436,6 +640,18 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
436
640
|
if (PLACEHOLDER.test(markdown)) {
|
|
437
641
|
findings.push(`placeholder or non-assertable wording in ${relativeFile}`);
|
|
438
642
|
}
|
|
643
|
+
const sharedSetupRefs = extractBackendSharedSetupRefs(markdown);
|
|
644
|
+
for (const ref of sharedSetupRefs) {
|
|
645
|
+
if (!input.sharedSetup) {
|
|
646
|
+
findings.push(`shared setup reference without a bound document: ${ref.raw} in ${relativeFile}`);
|
|
647
|
+
}
|
|
648
|
+
else if (ref.path !== input.sharedSetup.path) {
|
|
649
|
+
findings.push(`shared setup reference cites unbound path ${ref.path} (bound: ${input.sharedSetup.path}) in ${relativeFile}`);
|
|
650
|
+
}
|
|
651
|
+
else if (ref.anchor !== "SS-DEFAULT" && !sharedSetupAnchorRe.test(ref.anchor)) {
|
|
652
|
+
findings.push(`shared setup reference anchor is not a stable SS ID and not SS-DEFAULT: ${ref.raw} in ${relativeFile}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
439
655
|
const cases = splitCases(markdown);
|
|
440
656
|
if (cases.length === 0) {
|
|
441
657
|
findings.push(`no BE-<MODULE>-<NNN> case heading in ${relativeFile}`);
|
|
@@ -459,6 +675,15 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
459
675
|
findings.push(`${testCase.id} missing section: ${sectionAliases.join(" or ")}`);
|
|
460
676
|
}
|
|
461
677
|
}
|
|
678
|
+
const bindingFinding = markdownTestPointBindingFinding(testCase.id, testCase.body);
|
|
679
|
+
if (bindingFinding)
|
|
680
|
+
findings.push(bindingFinding);
|
|
681
|
+
const payloadLabelFinding = markdownPayloadLabelFinding(testCase.id, testCase.body);
|
|
682
|
+
if (payloadLabelFinding)
|
|
683
|
+
findings.push(payloadLabelFinding);
|
|
684
|
+
const scenarioIntentFinding = markdownScenarioIntentFinding(testCase.id, testCase.body);
|
|
685
|
+
if (scenarioIntentFinding)
|
|
686
|
+
findings.push(scenarioIntentFinding);
|
|
462
687
|
const acIds = unique(testCase.body.match(AC_ID) ?? []);
|
|
463
688
|
acIds.forEach((id) => coveredAc.add(id));
|
|
464
689
|
const steps = sectionBody(testCase.body, CASE_SECTION_ALIASES.steps);
|
|
@@ -469,9 +694,12 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
469
694
|
if (!hasAssertableExpectedResult(expected)) {
|
|
470
695
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
471
696
|
}
|
|
472
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
697
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, layout);
|
|
473
698
|
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
474
|
-
if (
|
|
699
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
700
|
+
// Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
|
|
701
|
+
}
|
|
702
|
+
else if (mappedScripts.length === 0) {
|
|
475
703
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
476
704
|
}
|
|
477
705
|
else {
|
|
@@ -501,6 +729,7 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
501
729
|
`- Cases: ${caseCount}`,
|
|
502
730
|
`- Covered AC IDs: ${[...coveredAc].sort().join(", ") || "none"}`,
|
|
503
731
|
`- Environment evidence present: ${input.environmentMarkdown.trim() ? "yes" : "no"}`,
|
|
732
|
+
`- Manifest scope: ${manifest.manifestStatus === "valid" ? "declared-modules" : `fails-closed:${manifest.manifestStatus}`} (declared-missing=${manifest.declaredMissing.length}, unmanifested-advisory=${manifest.unmanifested.length}, module-index-heading-count=${manifest.moduleIndexHeadingCount})`,
|
|
504
733
|
`- Findings: ${findings.length}`,
|
|
505
734
|
"- Required sections: Preconditions/前置条件, Steps/操作步骤, Expected Results/预期结果",
|
|
506
735
|
"- Source-reference validity check: not applied",
|
|
@@ -522,6 +751,13 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
522
751
|
? safetyFindings.map((finding) => `- ${finding}`)
|
|
523
752
|
: ["- No secret-shaped values detected."]),
|
|
524
753
|
"",
|
|
754
|
+
"## Unmanifested Markdown Modules",
|
|
755
|
+
"",
|
|
756
|
+
"- Advisory: existing .md files not declared by the README Module Index are excluded from this round's Case/coverage/partition scope.",
|
|
757
|
+
...(manifest.unmanifested.length > 0
|
|
758
|
+
? manifest.unmanifested.map((item) => `- ${item}`)
|
|
759
|
+
: ["- None"]),
|
|
760
|
+
"",
|
|
525
761
|
"## Findings",
|
|
526
762
|
"",
|
|
527
763
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
|
|
@@ -549,8 +785,9 @@ export function normalizeBackendTestModuleStem(moduleFileName) {
|
|
|
549
785
|
return stem;
|
|
550
786
|
}
|
|
551
787
|
/** Expected pytest script path for a Markdown module file (basename or relative path). */
|
|
552
|
-
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
|
|
553
|
-
|
|
788
|
+
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName, layout) {
|
|
789
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
790
|
+
return `${resolved.scriptDir}/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
|
|
554
791
|
}
|
|
555
792
|
function extractMappedPytestScripts(testCaseBody) {
|
|
556
793
|
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
@@ -560,12 +797,79 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
560
797
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
561
798
|
.filter((value) => !value.includes("..")));
|
|
562
799
|
}
|
|
563
|
-
function
|
|
800
|
+
function markdownScenarioIntentFinding(testCaseId, testCaseBody) {
|
|
801
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
802
|
+
if (!automation.trim())
|
|
803
|
+
return null;
|
|
804
|
+
const variantLine = /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/im.exec(automation)?.[1] ?? "";
|
|
805
|
+
const variants = unique(variantLine.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
|
|
806
|
+
if (variants.length === 0)
|
|
807
|
+
return null;
|
|
808
|
+
const localIntentIds = unique([...testCaseBody.matchAll(/(?:场景意图|Scenario Intent)\s*[::]\s*(TP-[A-Z0-9-]+)/gi)].map((match) => match[1].toUpperCase()));
|
|
809
|
+
const missing = variants.filter((id) => !localIntentIds.includes(id.toUpperCase()));
|
|
810
|
+
return missing.length > 0
|
|
811
|
+
? `${testCaseId} variant Test Points require a local Scenario Intent line inside the same Case: missing=${missing.join(", ")}`
|
|
812
|
+
: null;
|
|
813
|
+
}
|
|
814
|
+
function markdownPayloadLabelFinding(testCaseId, testCaseBody) {
|
|
815
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
816
|
+
if (!automation.trim())
|
|
817
|
+
return null;
|
|
818
|
+
const payloadLabel = /Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/gi;
|
|
819
|
+
for (const line of automation.split(/\r?\n/)) {
|
|
820
|
+
const matches = line.match(payloadLabel) ?? [];
|
|
821
|
+
if (matches.length === 0)
|
|
822
|
+
continue;
|
|
823
|
+
if (matches.length !== 1 || !/^\s*[-*+]\s*Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/i.test(line)) {
|
|
824
|
+
return `${testCaseId} payload contract labels must each occupy one independent machine-readable list line`;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
function markdownTestPointBindingFinding(testCaseId, testCaseBody) {
|
|
830
|
+
const declaredSection = sectionBody(testCaseBody, ["测试点", "Test Points"]);
|
|
831
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
832
|
+
if (!declaredSection.trim() || !automation.trim())
|
|
833
|
+
return null;
|
|
834
|
+
const declared = unique(declaredSection.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
|
|
835
|
+
const bindingLines = [
|
|
836
|
+
/^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/gim,
|
|
837
|
+
/^(?:\s*[-*+]\s*)?(?:场景断言测试点|Assertion Test Points)\s*[::]\s*(.*)$/gim,
|
|
838
|
+
/^(?:\s*[-*+]\s*)?(?:横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(.*)$/gim,
|
|
839
|
+
];
|
|
840
|
+
const bound = unique(bindingLines.flatMap((pattern) => [...automation.matchAll(pattern)].flatMap((match) => match[1]?.match(/\bTP-[A-Z0-9-]+\b/g) ?? [])));
|
|
841
|
+
const missingBindings = declared.filter((id) => !bound.includes(id));
|
|
842
|
+
const undeclaredBindings = bound.filter((id) => !declared.includes(id));
|
|
843
|
+
if (missingBindings.length === 0 && undeclaredBindings.length === 0)
|
|
844
|
+
return null;
|
|
845
|
+
return `${testCaseId} Test Point set must exactly equal variant/assertion/cross-cutting binding union: unclassified=${missingBindings.join(", ") || "none"}; undeclared-bindings=${undeclaredBindings.join(", ") || "none"}`;
|
|
846
|
+
}
|
|
847
|
+
function isEvidenceOnlyMarkdownCaseBody(testCaseBody) {
|
|
848
|
+
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
849
|
+
if (!automation.trim())
|
|
850
|
+
return false;
|
|
851
|
+
const scriptLine = /^\s*[-*+]\s*(?:脚本|script)\s*[::]\s*(.*)$/im.exec(automation)?.[1]?.replaceAll("`", "").trim() ??
|
|
852
|
+
"";
|
|
853
|
+
const primaryLine = /^\s*[-*+]\s*(?:primary symbol|Primary Symbol|主测试符号)\s*[::]\s*(.*)$/im.exec(automation)?.[1]
|
|
854
|
+
?.replaceAll("`", "")
|
|
855
|
+
.trim() ?? "";
|
|
856
|
+
const noneLike = /^(?:none|无|n\/a|not-applicable)(?:\s*[((].*[))])?$/i;
|
|
857
|
+
const scriptIsNone = noneLike.test(scriptLine) || scriptLine.length === 0;
|
|
858
|
+
const primaryIsNone = noneLike.test(primaryLine) || primaryLine.length === 0;
|
|
859
|
+
const hasMappedScript = extractMappedPytestScripts(testCaseBody).length > 0;
|
|
860
|
+
const hasPrimary = /\btest_[A-Za-z0-9_]+\b/.test(primaryLine);
|
|
861
|
+
const hasVariant = /(?:变体测试点|Variant Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
862
|
+
const hasAssertionOrCross = /(?:场景断言测试点|Assertion Test Points|横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
|
|
863
|
+
return scriptIsNone && primaryIsNone && !hasMappedScript && !hasPrimary && !hasVariant && hasAssertionOrCross;
|
|
864
|
+
}
|
|
865
|
+
function isSafeBackendPytestScript(script, workspaceRoot, layout) {
|
|
866
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
564
867
|
const absolute = path.resolve(workspaceRoot, script);
|
|
565
868
|
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
869
|
+
const allowedRoots = [`${resolved.testRoot}/`];
|
|
566
870
|
return (!relative.startsWith("..") &&
|
|
567
871
|
!path.isAbsolute(relative) &&
|
|
568
|
-
script.startsWith(
|
|
872
|
+
allowedRoots.some((prefix) => script.startsWith(prefix)) &&
|
|
569
873
|
!script.includes("..") &&
|
|
570
874
|
/^test_.*\.py$/i.test(path.basename(script)));
|
|
571
875
|
}
|
|
@@ -609,8 +913,8 @@ function symbolCaseId(symbol) {
|
|
|
609
913
|
? canonicalBackendCaseId(match[1].replaceAll("_", "-"))
|
|
610
914
|
: undefined;
|
|
611
915
|
}
|
|
612
|
-
export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
613
|
-
const files = await markdownFiles(workspaceRoot);
|
|
916
|
+
export async function validateBackendMarkdownTraceability(workspaceRoot, layout) {
|
|
917
|
+
const files = await markdownFiles(workspaceRoot, layout);
|
|
614
918
|
const markdownIds = new Set();
|
|
615
919
|
const expectedIdsByScript = new Map();
|
|
616
920
|
const findings = [];
|
|
@@ -619,8 +923,11 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
619
923
|
continue;
|
|
620
924
|
const content = await readFile(file, "utf8");
|
|
621
925
|
for (const testCase of splitCases(content)) {
|
|
622
|
-
markdownIds.add(testCase.id);
|
|
623
926
|
const scripts = extractMappedPytestScripts(testCase.body);
|
|
927
|
+
if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
markdownIds.add(testCase.id);
|
|
624
931
|
if (scripts.length === 0) {
|
|
625
932
|
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
|
|
626
933
|
continue;
|
|
@@ -632,6 +939,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
632
939
|
}
|
|
633
940
|
}
|
|
634
941
|
}
|
|
942
|
+
const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
|
|
635
943
|
const mappings = new Map();
|
|
636
944
|
const automationIds = new Set();
|
|
637
945
|
let requestResponseLoggingFindingCount = 0;
|
|
@@ -640,7 +948,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
640
948
|
const relative = path.relative(workspaceRoot, absolute);
|
|
641
949
|
if (relative.startsWith("..") ||
|
|
642
950
|
path.isAbsolute(relative) ||
|
|
643
|
-
!relativeScript.startsWith(
|
|
951
|
+
!relativeScript.startsWith(`${resolvedLayout.testRoot}/`) ||
|
|
644
952
|
!/^test_.*\.py$/i.test(path.basename(relativeScript))) {
|
|
645
953
|
findings.push(`unsafe mapped pytest script: ${relativeScript}`);
|
|
646
954
|
continue;
|
|
@@ -713,8 +1021,9 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
|
|
|
713
1021
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
|
|
714
1022
|
].join("\n") + "\n");
|
|
715
1023
|
}
|
|
716
|
-
export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
717
|
-
const
|
|
1024
|
+
export async function resolveBackendTestMappedPytestScripts(workspaceRoot, layout) {
|
|
1025
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
1026
|
+
const files = await markdownFiles(workspaceRoot, resolved);
|
|
718
1027
|
const expectedScripts = new Set();
|
|
719
1028
|
const existingScripts = new Set();
|
|
720
1029
|
const missingScripts = new Set();
|
|
@@ -735,7 +1044,7 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
735
1044
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
736
1045
|
continue;
|
|
737
1046
|
const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
|
|
738
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
1047
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, resolved);
|
|
739
1048
|
const markdown = await readFile(file, "utf8");
|
|
740
1049
|
const mappedFromModule = new Set();
|
|
741
1050
|
for (const testCase of splitCases(markdown)) {
|
|
@@ -755,14 +1064,14 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
755
1064
|
continue;
|
|
756
1065
|
}
|
|
757
1066
|
for (const script of mappedFromModule) {
|
|
758
|
-
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
1067
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot, resolved)) {
|
|
759
1068
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
760
1069
|
}
|
|
761
1070
|
if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
|
|
762
1071
|
recordScript(script, relativeFile, true);
|
|
763
1072
|
continue;
|
|
764
1073
|
}
|
|
765
|
-
if (!isSafeBackendPytestScript(expectedScript, workspaceRoot)) {
|
|
1074
|
+
if (!isSafeBackendPytestScript(expectedScript, workspaceRoot, resolved)) {
|
|
766
1075
|
throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
|
|
767
1076
|
}
|
|
768
1077
|
recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
|
|
@@ -777,8 +1086,8 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
777
1086
|
missingScripts: unique(missingScripts),
|
|
778
1087
|
};
|
|
779
1088
|
}
|
|
780
|
-
export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
781
|
-
const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot);
|
|
1089
|
+
export async function collectBackendTestMappedPytestScripts(workspaceRoot, layout) {
|
|
1090
|
+
const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot, layout);
|
|
782
1091
|
if (resolution.missingScripts.length > 0) {
|
|
783
1092
|
throw new Error(`mapped pytest script is missing: ${resolution.missingScripts[0]}`);
|
|
784
1093
|
}
|
|
@@ -791,8 +1100,8 @@ function cleanCaseTitle(rawHeading, id) {
|
|
|
791
1100
|
.replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "")
|
|
792
1101
|
.trim() || id;
|
|
793
1102
|
}
|
|
794
|
-
export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
|
|
795
|
-
const files = await markdownFiles(workspaceRoot);
|
|
1103
|
+
export async function collectBackendTestHumanCaseCatalog(workspaceRoot, layout) {
|
|
1104
|
+
const files = await markdownFiles(workspaceRoot, layout);
|
|
796
1105
|
const catalog = new Map();
|
|
797
1106
|
for (const file of files) {
|
|
798
1107
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
@@ -814,7 +1123,8 @@ export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
|
|
|
814
1123
|
});
|
|
815
1124
|
}
|
|
816
1125
|
}
|
|
817
|
-
const
|
|
1126
|
+
const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
|
|
1127
|
+
const testcaseRoot = path.join(workspaceRoot, ...resolvedLayout.testRoot.split("/"));
|
|
818
1128
|
const pythonFiles = [];
|
|
819
1129
|
async function walk(directory) {
|
|
820
1130
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
const PRIORITY_ONLY_MODULE_STEMS = new Set(["p0", "p1", "p2"]);
|
|
3
|
+
const OPAQUE_HASH_MODULE_STEM = /^[a-f][a-f0-9]{6,63}$/;
|
|
4
|
+
export const BACKEND_TEST_MAX_MODULE_COUNT = 8;
|
|
3
5
|
export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
4
6
|
return path.basename(raw)
|
|
5
7
|
.replace(/\.(?:md|py)$/i, "")
|
|
@@ -11,6 +13,9 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
|
11
13
|
export function isPriorityOnlyBackendTestModuleStem(raw) {
|
|
12
14
|
return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
|
|
13
15
|
}
|
|
16
|
+
export function isOpaqueHashBackendTestModuleStem(raw) {
|
|
17
|
+
return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
|
|
18
|
+
}
|
|
14
19
|
export function isPriorityOnlyBackendPytestScript(script) {
|
|
15
20
|
const basename = path.posix.basename(script.replaceAll("\\", "/"));
|
|
16
21
|
const match = /^test_(.+)\.py$/i.exec(basename);
|