@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
|
@@ -3,8 +3,18 @@ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { priorityOnlyBackendPytestScripts } from "./backend-test-module-stem.js";
|
|
6
|
+
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
6
7
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
7
8
|
const MAX_DIAGNOSTIC_CHARS = 12_000;
|
|
9
|
+
/** Upper bound for assertBackendTestExecutionReadinessFresh Error.message (not collection excerpts). */
|
|
10
|
+
export const MAX_READINESS_ERROR_CHARS = 2_048;
|
|
11
|
+
const READINESS_EXCLUSION_CATEGORIES = [
|
|
12
|
+
"correspondence-missing",
|
|
13
|
+
"correspondence-status",
|
|
14
|
+
"payload-contract-status",
|
|
15
|
+
"scenario-param-field",
|
|
16
|
+
"other",
|
|
17
|
+
];
|
|
8
18
|
export const backendPytestCollectionFindingSchema = z.object({
|
|
9
19
|
kind: z.string().min(1),
|
|
10
20
|
classification: z.literal("test-asset-defect"),
|
|
@@ -27,6 +37,8 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
27
37
|
missingMappedScripts: z.array(z.string()),
|
|
28
38
|
assetFiles: z.array(z.string()),
|
|
29
39
|
inputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
40
|
+
hazardOccurrenceCounts: z.record(z.string(), z.record(z.string(), z.number().int().min(0))),
|
|
41
|
+
hazardOccurrences: z.record(z.string(), z.record(z.string(), z.array(z.string()))).optional(),
|
|
30
42
|
pytestExitCode: z.number().int().nullable(),
|
|
31
43
|
collectedItemCount: z.number().int().min(0),
|
|
32
44
|
collectedItemIds: z.array(z.string()),
|
|
@@ -49,8 +61,14 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
49
61
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
|
|
50
62
|
}
|
|
51
63
|
});
|
|
64
|
+
const backendTestExcludedItemSchema = z.object({
|
|
65
|
+
itemId: z.string().min(1),
|
|
66
|
+
caseId: z.string().optional(),
|
|
67
|
+
symbol: z.string().optional(),
|
|
68
|
+
reasons: z.array(z.string().min(1)).min(1),
|
|
69
|
+
}).strict();
|
|
52
70
|
export const backendTestExecutionReadinessSchema = z.object({
|
|
53
|
-
schemaId: z.literal("backend-test-execution-readiness-
|
|
71
|
+
schemaId: z.literal("backend-test-execution-readiness-v2"),
|
|
54
72
|
status: z.enum(["PASS", "PARTIAL", "BLOCKED"]),
|
|
55
73
|
collectionStatus: z.literal("PASS"),
|
|
56
74
|
fixtureResolutionStatus: z.literal("PASS"),
|
|
@@ -61,9 +79,111 @@ export const backendTestExecutionReadinessSchema = z.object({
|
|
|
61
79
|
}).strict(),
|
|
62
80
|
mappedScripts: z.array(z.string()).min(1),
|
|
63
81
|
collectedItemIds: z.array(z.string()),
|
|
82
|
+
eligibleItemIds: z.array(z.string()),
|
|
83
|
+
excludedItems: z.array(backendTestExcludedItemSchema),
|
|
64
84
|
fixtureIssues: z.array(z.string()),
|
|
65
85
|
assetHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
86
|
+
eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
66
87
|
}).strict();
|
|
88
|
+
const REPAIR_HAZARD_PATTERNS = [
|
|
89
|
+
{ label: "skip/xfail", source: "pytest\\.mark\\.(?:skip|skipif|xfail)\\b|pytest\\.(?:skip|xfail)\\s*\\(" },
|
|
90
|
+
{ label: "sys.path/PYTHONPATH mutation", source: "sys\\.path\\s*\\.|PYTHONPATH" },
|
|
91
|
+
{ label: "ImportError fallback", source: "except\\s+ImportError\\b" },
|
|
92
|
+
{ label: "broad exception swallowing", source: "except\\s+(?:Exception|BaseException)\\b[\\s\\S]{0,160}?\\bpass\\b" },
|
|
93
|
+
];
|
|
94
|
+
function normalizeSourceNewlines(source) {
|
|
95
|
+
return source.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a hazard occurrence identity by stripping trailing Python `#` comments
|
|
99
|
+
* while preserving `#` characters inside string literals. Fail closed: if quote
|
|
100
|
+
* state is ambiguous, keep the original trimmed line rather than over-stripping.
|
|
101
|
+
*/
|
|
102
|
+
function normalizeHazardOccurrenceIdentity(line) {
|
|
103
|
+
const trimmed = line.trimEnd().trim();
|
|
104
|
+
if (!trimmed.includes("#"))
|
|
105
|
+
return trimmed;
|
|
106
|
+
let inSingle = false;
|
|
107
|
+
let inDouble = false;
|
|
108
|
+
let escaped = false;
|
|
109
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
110
|
+
const ch = trimmed[i];
|
|
111
|
+
if (escaped) {
|
|
112
|
+
escaped = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if ((inSingle || inDouble) && ch === "\\") {
|
|
116
|
+
escaped = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!inDouble && ch === "'") {
|
|
120
|
+
inSingle = !inSingle;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!inSingle && ch === '"') {
|
|
124
|
+
inDouble = !inDouble;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!inSingle && !inDouble && ch === "#") {
|
|
128
|
+
return trimmed.slice(0, i).trimEnd();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Unclosed quote or no out-of-string `#`: do not invent a comment boundary.
|
|
132
|
+
return trimmed;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Bare `__NNN_*` helper module names imported by a Python source snippet.
|
|
136
|
+
* Only the head token of an `import`/`from` statement is considered a module
|
|
137
|
+
* name; dotted submodules (`__551_manager_helpers.x`) collapse to the head.
|
|
138
|
+
*/
|
|
139
|
+
function importedBareHelperModules(source) {
|
|
140
|
+
const names = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const match of normalizeSourceNewlines(source).matchAll(/\b(?:import|from)\s+([A-Za-z0-9_.]+)/g)) {
|
|
143
|
+
const head = match[1].split(".")[0];
|
|
144
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(head) && !seen.has(head)) {
|
|
145
|
+
seen.add(head);
|
|
146
|
+
names.push(head);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return names;
|
|
150
|
+
}
|
|
151
|
+
function listPatternOccurrences(source, patternSource, flags = "") {
|
|
152
|
+
const normalized = normalizeSourceNewlines(source);
|
|
153
|
+
const pattern = new RegExp(patternSource, flags.includes("g") ? flags : `${flags}g`);
|
|
154
|
+
const identities = [];
|
|
155
|
+
for (const match of normalized.matchAll(pattern)) {
|
|
156
|
+
const index = match.index ?? 0;
|
|
157
|
+
const lineStart = normalized.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
|
|
158
|
+
const lineEnd = normalized.indexOf("\n", index);
|
|
159
|
+
const line = normalized.slice(lineStart, lineEnd === -1 ? normalized.length : lineEnd).trimEnd();
|
|
160
|
+
identities.push(normalizeHazardOccurrenceIdentity(line));
|
|
161
|
+
}
|
|
162
|
+
return identities;
|
|
163
|
+
}
|
|
164
|
+
function scanRepairHazardOccurrenceLists(source) {
|
|
165
|
+
const lists = {};
|
|
166
|
+
for (const rule of REPAIR_HAZARD_PATTERNS) {
|
|
167
|
+
lists[rule.label] = listPatternOccurrences(source, rule.source, rule.flags);
|
|
168
|
+
}
|
|
169
|
+
return lists;
|
|
170
|
+
}
|
|
171
|
+
function multisetCounts(identities) {
|
|
172
|
+
const counts = new Map();
|
|
173
|
+
for (const identity of identities) {
|
|
174
|
+
counts.set(identity, (counts.get(identity) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
return counts;
|
|
177
|
+
}
|
|
178
|
+
function hasMultisetIncrement(initialIdentities, finalIdentities) {
|
|
179
|
+
const initial = multisetCounts(initialIdentities);
|
|
180
|
+
const final = multisetCounts(finalIdentities);
|
|
181
|
+
for (const [identity, finalCount] of final) {
|
|
182
|
+
if (finalCount > (initial.get(identity) ?? 0))
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
67
187
|
function repoRef(workspaceRoot, absolutePath) {
|
|
68
188
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
69
189
|
}
|
|
@@ -94,27 +214,43 @@ async function walkPythonFiles(root) {
|
|
|
94
214
|
return results;
|
|
95
215
|
}
|
|
96
216
|
export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScripts, options = {}) {
|
|
217
|
+
const layout = options.layout ?? resolveBackendTestLayout(undefined);
|
|
218
|
+
const scriptRoot = layout.scriptDir;
|
|
97
219
|
const normalizedScripts = [...new Set(mappedScripts.map((value) => value.replaceAll("\\", "/")))].sort();
|
|
98
220
|
if ((options.requireMappedScripts ?? true) && normalizedScripts.length === 0) {
|
|
99
221
|
throw new Error("backend pytest collection requires mapped scripts");
|
|
100
222
|
}
|
|
223
|
+
const scriptPattern = new RegExp(`^${scriptRoot.replaceAll(/[.*+?^[\]{}()|$]/g, "\\$&")}(?:/[A-Za-z0-9_.-]+)*/test_[A-Za-z0-9_.-]+\\.py$`);
|
|
224
|
+
const rootScriptPattern = new RegExp(`^${scriptRoot.replaceAll(/[.*+?^[\]{}()|$]/g, "\\$&")}/test_[A-Za-z0-9_.-]+\\.py$`);
|
|
101
225
|
for (const script of normalizedScripts) {
|
|
102
|
-
if (
|
|
226
|
+
if (!scriptPattern.test(script) && !rootScriptPattern.test(script)) {
|
|
103
227
|
throw new Error(`unsafe backend pytest mapped script: ${script}`);
|
|
104
228
|
}
|
|
105
229
|
const absolute = path.resolve(workspaceRoot, script);
|
|
106
|
-
const testcaseRoot = path.resolve(workspaceRoot,
|
|
230
|
+
const testcaseRoot = path.resolve(workspaceRoot, layout.testRoot);
|
|
107
231
|
if (absolute !== testcaseRoot && !absolute.startsWith(`${testcaseRoot}${path.sep}`)) {
|
|
108
|
-
throw new Error(`backend pytest mapped script escapes
|
|
232
|
+
throw new Error(`backend pytest mapped script escapes test root ${layout.testRoot}: ${script}`);
|
|
109
233
|
}
|
|
110
234
|
await readFile(absolute);
|
|
111
235
|
}
|
|
112
|
-
const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot,
|
|
236
|
+
const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot, layout.testRoot));
|
|
113
237
|
const assetFiles = [...new Set(pythonFiles.map((file) => repoRef(workspaceRoot, file)))].sort();
|
|
114
238
|
const inputHashes = {};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
239
|
+
const hazardOccurrenceCounts = {};
|
|
240
|
+
const hazardOccurrences = {};
|
|
241
|
+
const scriptBareImports = {};
|
|
242
|
+
const mappedScriptSet = new Set(normalizedScripts);
|
|
243
|
+
for (const file of assetFiles) {
|
|
244
|
+
const bytes = await readFile(path.resolve(workspaceRoot, file));
|
|
245
|
+
const source = bytes.toString("utf8");
|
|
246
|
+
inputHashes[file] = sha256(bytes);
|
|
247
|
+
const lists = scanRepairHazardOccurrenceLists(source);
|
|
248
|
+
hazardOccurrences[file] = lists;
|
|
249
|
+
hazardOccurrenceCounts[file] = Object.fromEntries(Object.entries(lists).map(([label, identities]) => [label, identities.length]));
|
|
250
|
+
if (mappedScriptSet.has(file))
|
|
251
|
+
scriptBareImports[file] = importedBareHelperModules(source);
|
|
252
|
+
}
|
|
253
|
+
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences, scriptBareImports };
|
|
118
254
|
}
|
|
119
255
|
function bounded(value) {
|
|
120
256
|
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
@@ -131,13 +267,26 @@ function collectionItems(stdout) {
|
|
|
131
267
|
return /^testcase\//.test(line.replaceAll("\\", "/"));
|
|
132
268
|
}).map((line) => line.replaceAll("\\", "/")))];
|
|
133
269
|
}
|
|
134
|
-
function
|
|
135
|
-
return
|
|
270
|
+
function escapeRegExp(value) {
|
|
271
|
+
return value.replace(/[.*+?^[\]{}()|$]/g, "\\$&");
|
|
136
272
|
}
|
|
137
|
-
function
|
|
273
|
+
function testcasePythonPaths(output, layout) {
|
|
274
|
+
const root = (layout ?? resolveBackendTestLayout(undefined)).testRoot;
|
|
275
|
+
const pattern = new RegExp(`(?:${escapeRegExp(root)})/[A-Za-z0-9_./-]+\\.py`, "gi");
|
|
276
|
+
return [...new Set((output.replaceAll("\\", "/").match(pattern) ?? []).map((item) => item.replace(/:\d+$/, "")))].sort();
|
|
277
|
+
}
|
|
278
|
+
function classifyFixtureResolutionFailure(output, layout) {
|
|
279
|
+
const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
|
|
280
|
+
const rootPrefix = `${resolvedLayout.testRoot}/`;
|
|
138
281
|
const normalized = output.replaceAll("\\", "/");
|
|
139
|
-
const paths = testcasePythonPaths(normalized);
|
|
140
|
-
const generatedProviderPaths = paths.filter((item) =>
|
|
282
|
+
const paths = testcasePythonPaths(normalized, resolvedLayout);
|
|
283
|
+
const generatedProviderPaths = paths.filter((item) => new RegExp(`^${escapeRegExp(rootPrefix)}(?:helpers|factories)/`).test(item) ||
|
|
284
|
+
new RegExp(`^${escapeRegExp(resolvedLayout.scriptDir)}/(?:helpers|factories)/`).test(item));
|
|
285
|
+
const generatedTestModulePaths = paths.filter((item) => new RegExp(`^${escapeRegExp(resolvedLayout.scriptDir)}(?:/[A-Za-z0-9_./-]+)?/test_[A-Za-z0-9_.-]+\\.py$`, "i").test(item) ||
|
|
286
|
+
new RegExp(`^${escapeRegExp(rootPrefix)}test_[A-Za-z0-9_.-]+\\.py$`, "i").test(item));
|
|
287
|
+
const onlyGeneratedTestcasePaths = paths.length > 0 && paths.every((item) => item.startsWith(rootPrefix));
|
|
288
|
+
const mentionsProduction = /(?:apps|src|server)\/[A-Za-z0-9_./-]+\.py/i.test(normalized) ||
|
|
289
|
+
new RegExp(`No module named ['\"](?!${escapeRegExp(resolvedLayout.testRoot)}(?:\\.|['\"]))`, "i").test(normalized);
|
|
141
290
|
if (/fixture ['"][^'"]+['"] not found/i.test(normalized) && generatedProviderPaths.length > 0) {
|
|
142
291
|
return {
|
|
143
292
|
status: "REPAIRABLE",
|
|
@@ -146,6 +295,33 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
146
295
|
repairPaths: generatedProviderPaths,
|
|
147
296
|
};
|
|
148
297
|
}
|
|
298
|
+
// ScopeMismatch and other setup failures fully attributed to generated test modules are
|
|
299
|
+
// writer-local and may be fixed by one bounded repair (align scopes / define fixtures).
|
|
300
|
+
if (!mentionsProduction &&
|
|
301
|
+
onlyGeneratedTestcasePaths &&
|
|
302
|
+
generatedTestModulePaths.length > 0 &&
|
|
303
|
+
/ScopeMismatch|scope mismatch|function scoped fixture|module scoped request object|module scoped fixture|ERROR at setup of/i.test(normalized)) {
|
|
304
|
+
const isScope = /ScopeMismatch|scope mismatch|function scoped fixture|module scoped request object|module scoped fixture/i.test(normalized);
|
|
305
|
+
return {
|
|
306
|
+
status: "REPAIRABLE",
|
|
307
|
+
kind: isScope ? "generated-fixture-scope-mismatch" : "generated-fixture-setup-defect",
|
|
308
|
+
detail: isScope
|
|
309
|
+
? "generated pytest fixture scopes are inconsistent inside mapped test modules"
|
|
310
|
+
: "generated pytest fixture setup failed inside mapped test modules",
|
|
311
|
+
repairPaths: [...new Set([...generatedTestModulePaths, ...generatedProviderPaths])].sort(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (!mentionsProduction &&
|
|
315
|
+
onlyGeneratedTestcasePaths &&
|
|
316
|
+
generatedTestModulePaths.length > 0 &&
|
|
317
|
+
/fixture ['"][^'"]+['"] not found/i.test(normalized)) {
|
|
318
|
+
return {
|
|
319
|
+
status: "REPAIRABLE",
|
|
320
|
+
kind: "missing-generated-fixture",
|
|
321
|
+
detail: "mapped pytest module requests a fixture not defined in generated testcase assets",
|
|
322
|
+
repairPaths: [...new Set([...generatedTestModulePaths, ...generatedProviderPaths])].sort(),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
149
325
|
return {
|
|
150
326
|
status: "BLOCKED",
|
|
151
327
|
kind: "unresolved-fixture-dependency",
|
|
@@ -153,8 +329,68 @@ function classifyFixtureResolutionFailure(output) {
|
|
|
153
329
|
repairPaths: paths,
|
|
154
330
|
};
|
|
155
331
|
}
|
|
156
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Generated-local bare helper module signatures found in the bound pytest
|
|
334
|
+
* asset inventory. A file such as `testcase/__551_manager_helpers.py` in the
|
|
335
|
+
* test root/script dir is generated-local: when collection reports
|
|
336
|
+
* `No module named '__551_manager_helpers'`, the defect can be precisely
|
|
337
|
+
* attributed to the generated mapped script instead of a missing third-party
|
|
338
|
+
* package.
|
|
339
|
+
*/
|
|
340
|
+
function generatedLocalBareHelperNames(inventory) {
|
|
341
|
+
if (!inventory)
|
|
342
|
+
return [];
|
|
343
|
+
const names = [];
|
|
344
|
+
for (const file of inventory.assetFiles) {
|
|
345
|
+
const stem = path.posix.basename(file).replace(/\.py$/, "");
|
|
346
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(stem))
|
|
347
|
+
names.push(stem);
|
|
348
|
+
}
|
|
349
|
+
return [...new Set(names)];
|
|
350
|
+
}
|
|
351
|
+
function classifyCollectionFailure(output, evidence) {
|
|
157
352
|
const normalized = output.replaceAll("\\", "/");
|
|
353
|
+
const helperNames = generatedLocalBareHelperNames(evidence?.inventory);
|
|
354
|
+
const helperByLower = new Map(helperNames.map((name) => [name.toLowerCase(), name]));
|
|
355
|
+
// A bare `__NNN_...` module import that collection cannot find is only a
|
|
356
|
+
// candidate generated-local helper signature. REPAIRABLE requires two
|
|
357
|
+
// deterministic pieces of evidence: (1) a helper asset of the same name in
|
|
358
|
+
// the bound inventory, and (2) at least one mapped generated script whose
|
|
359
|
+
// source truly imports that bare module. Without both — or when attribution
|
|
360
|
+
// is impossible — fail closed as BLOCKED so an un-attributable dependency
|
|
361
|
+
// error is never widened into a repair. Genuine third-party modules (no
|
|
362
|
+
// `__NNN_` signature) stay BLOCKED below.
|
|
363
|
+
const missingModule = /No module named ['\"]([^'\"]+)['\"]/i.exec(normalized)?.[1];
|
|
364
|
+
if (missingModule && /^__\d+[A-Za-z0-9_]*$/.test(missingModule)) {
|
|
365
|
+
const helperAsset = helperByLower.get(missingModule.toLowerCase());
|
|
366
|
+
const inventory = evidence?.inventory;
|
|
367
|
+
const scriptBareImports = inventory?.scriptBareImports ?? {};
|
|
368
|
+
const missingLower = missingModule.toLowerCase();
|
|
369
|
+
const importedByMappedScript = inventory
|
|
370
|
+
? inventory.mappedScripts.some((script) => (scriptBareImports[script] ?? []).some((name) => name.toLowerCase() === missingLower))
|
|
371
|
+
: false;
|
|
372
|
+
if (helperAsset && importedByMappedScript && inventory) {
|
|
373
|
+
const repairPaths = [...new Set([
|
|
374
|
+
...(helperAsset
|
|
375
|
+
? inventory.assetFiles.filter((file) => path.posix.basename(file).replace(/\.py$/, "") === helperAsset)
|
|
376
|
+
: []),
|
|
377
|
+
...inventory.mappedScripts,
|
|
378
|
+
])].sort();
|
|
379
|
+
return {
|
|
380
|
+
status: "REPAIRABLE",
|
|
381
|
+
kind: "missing-generated-local-helper",
|
|
382
|
+
detail: `generated testcase asset imports missing bare generated-local helper module ${missingModule}; inline the helper or remove the generated shared dependency`,
|
|
383
|
+
repairPaths,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
status: "BLOCKED",
|
|
388
|
+
kind: "missing-generated-local-helper-without-evidence",
|
|
389
|
+
detail: helperAsset
|
|
390
|
+
? `missing bare generated-local helper module ${missingModule} has a name match but no mapped generated script imports it (no precise generated-local attribution)`
|
|
391
|
+
: `missing bare __NNN_* module ${missingModule} has no generated-local helper asset in the bound inventory (cannot be precisely attributed)`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
158
394
|
const blockedPatterns = [
|
|
159
395
|
[/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
|
|
160
396
|
[/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
|
|
@@ -199,6 +435,8 @@ export function assessBackendPytestCollection(input) {
|
|
|
199
435
|
missingMappedScripts: [],
|
|
200
436
|
assetFiles: input.inventory.assetFiles,
|
|
201
437
|
inputHashes: input.inventory.inputHashes,
|
|
438
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
439
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
202
440
|
pytestExitCode: input.exitCode,
|
|
203
441
|
collectedItemCount: items.length,
|
|
204
442
|
collectedItemIds: items,
|
|
@@ -227,6 +465,8 @@ export function assessBackendPytestCollection(input) {
|
|
|
227
465
|
missingMappedScripts: [],
|
|
228
466
|
assetFiles: input.inventory.assetFiles,
|
|
229
467
|
inputHashes: input.inventory.inputHashes,
|
|
468
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
469
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
230
470
|
pytestExitCode: input.exitCode,
|
|
231
471
|
collectedItemCount: items.length,
|
|
232
472
|
collectedItemIds: items,
|
|
@@ -242,7 +482,7 @@ export function assessBackendPytestCollection(input) {
|
|
|
242
482
|
fixtureStderrExcerpt: bounded(fixtureResolution.stderr),
|
|
243
483
|
});
|
|
244
484
|
}
|
|
245
|
-
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}
|
|
485
|
+
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`, { layout: input.layout, inventory: input.inventory });
|
|
246
486
|
return backendPytestCollectionFactsSchema.parse({
|
|
247
487
|
schemaId: "backend-test-pytest-collection-v3",
|
|
248
488
|
phase: input.phase,
|
|
@@ -253,12 +493,16 @@ export function assessBackendPytestCollection(input) {
|
|
|
253
493
|
fixtureResolutionAttempted: false,
|
|
254
494
|
fixtureResolutionStatus: "NOT_RUN",
|
|
255
495
|
fixtureResolutionExitCode: null,
|
|
256
|
-
repairPaths:
|
|
496
|
+
repairPaths: classification.repairPaths && classification.repairPaths.length > 0
|
|
497
|
+
? classification.repairPaths
|
|
498
|
+
: testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
|
|
257
499
|
mappedScripts: input.inventory.mappedScripts,
|
|
258
500
|
existingMappedScripts: input.inventory.mappedScripts,
|
|
259
501
|
missingMappedScripts: [],
|
|
260
502
|
assetFiles: input.inventory.assetFiles,
|
|
261
503
|
inputHashes: input.inventory.inputHashes,
|
|
504
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
505
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
262
506
|
pytestExitCode: input.exitCode,
|
|
263
507
|
collectedItemCount: items.length,
|
|
264
508
|
collectedItemIds: items,
|
|
@@ -294,6 +538,8 @@ export function assessPriorityOnlyBackendPytestModules(input) {
|
|
|
294
538
|
missingMappedScripts: [],
|
|
295
539
|
assetFiles: input.inventory.assetFiles,
|
|
296
540
|
inputHashes: input.inventory.inputHashes,
|
|
541
|
+
hazardOccurrenceCounts: input.inventory.hazardOccurrenceCounts,
|
|
542
|
+
hazardOccurrences: input.inventory.hazardOccurrences,
|
|
297
543
|
pytestExitCode: null,
|
|
298
544
|
collectedItemCount: 0,
|
|
299
545
|
collectedItemIds: [],
|
|
@@ -326,6 +572,8 @@ export function assessMissingBackendPytestScripts(input) {
|
|
|
326
572
|
missingMappedScripts: [...input.missingMappedScripts],
|
|
327
573
|
assetFiles: [...input.assetFiles],
|
|
328
574
|
inputHashes: input.inputHashes,
|
|
575
|
+
hazardOccurrenceCounts: input.hazardOccurrenceCounts ?? Object.fromEntries(input.assetFiles.map((file) => [file, {}])),
|
|
576
|
+
hazardOccurrences: input.hazardOccurrences ?? Object.fromEntries(input.assetFiles.map((file) => [file, {}])),
|
|
329
577
|
pytestExitCode: null,
|
|
330
578
|
collectedItemCount: 0,
|
|
331
579
|
collectedItemIds: [],
|
|
@@ -415,17 +663,21 @@ async function assertBackendPytestRepairSafety(workspaceRoot, initial, final) {
|
|
|
415
663
|
if (JSON.stringify(initial.mappedScripts) !== JSON.stringify(final.mappedScripts)) {
|
|
416
664
|
throw new Error("backend pytest repair changed mapped script scope");
|
|
417
665
|
}
|
|
418
|
-
const forbidden = [
|
|
419
|
-
[/pytest\.mark\.(?:skip|skipif|xfail)\b|pytest\.(?:skip|xfail)\s*\(/, "skip/xfail"],
|
|
420
|
-
[/sys\.path\s*\.|PYTHONPATH/, "sys.path/PYTHONPATH mutation"],
|
|
421
|
-
[/except\s+ImportError\b/, "ImportError fallback"],
|
|
422
|
-
[/except\s+(?:Exception|BaseException)\b[\s\S]{0,160}?\bpass\b/, "broad exception swallowing"],
|
|
423
|
-
];
|
|
424
666
|
for (const file of final.assetFiles) {
|
|
425
667
|
const source = await readFile(path.resolve(workspaceRoot, file), "utf8");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
668
|
+
const liveHash = sha256(source);
|
|
669
|
+
if (final.inputHashes[file] && final.inputHashes[file] !== liveHash) {
|
|
670
|
+
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
671
|
+
}
|
|
672
|
+
if (initial.inputHashes[file] === final.inputHashes[file])
|
|
673
|
+
continue;
|
|
674
|
+
const finalLists = scanRepairHazardOccurrenceLists(source);
|
|
675
|
+
for (const rule of REPAIR_HAZARD_PATTERNS) {
|
|
676
|
+
const baseline = initial.hazardOccurrences?.[file]?.[rule.label] ?? [];
|
|
677
|
+
const finalIdentities = finalLists[rule.label] ?? [];
|
|
678
|
+
if (hasMultisetIncrement(baseline, finalIdentities)) {
|
|
679
|
+
throw new Error(`backend pytest repair safety blocked ${rule.label}: ${file}`);
|
|
680
|
+
}
|
|
429
681
|
}
|
|
430
682
|
}
|
|
431
683
|
}
|
|
@@ -439,23 +691,65 @@ function assertSameInventory(expected, actual) {
|
|
|
439
691
|
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
440
692
|
}
|
|
441
693
|
}
|
|
694
|
+
export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
695
|
+
const eligibleItemIds = [];
|
|
696
|
+
const excludedItems = [];
|
|
697
|
+
for (const itemId of collectedItemIds) {
|
|
698
|
+
const segments = itemId.split("::");
|
|
699
|
+
const symbolSegment = [...segments].reverse().find((segment) => /^test_/.test(segment));
|
|
700
|
+
const symbol = symbolSegment?.replace(/\[.*$/, "");
|
|
701
|
+
const parameterTokens = orderedUnique((itemId.match(/TP-[A-Z0-9-]+/g) ?? []));
|
|
702
|
+
const mapping = input.correspondenceEntries.find((entry) => symbol && (entry.declaredPrimarySymbol === symbol || entry.pytestSymbols.includes(symbol)));
|
|
703
|
+
const reasons = [];
|
|
704
|
+
if (!mapping)
|
|
705
|
+
reasons.push("no exact Markdown Case/primary-symbol correspondence");
|
|
706
|
+
if (mapping && mapping.status !== "EXACT_1_TO_1")
|
|
707
|
+
reasons.push(`correspondence status is ${mapping.status}`);
|
|
708
|
+
if (mapping && mapping.payloadAssessment.status !== "SAFE")
|
|
709
|
+
reasons.push(`payload contract status is ${mapping.payloadAssessment.status}`);
|
|
710
|
+
const scenarioEntries = mapping?.caseId ? input.scenarioEntries.filter((entry) => entry.caseId === mapping.caseId) : [];
|
|
711
|
+
const relevantScenarioEntries = parameterTokens.length > 0
|
|
712
|
+
? scenarioEntries.filter((entry) => parameterTokens.includes(entry.tpId))
|
|
713
|
+
: scenarioEntries;
|
|
714
|
+
const payloadPaths = orderedUnique([
|
|
715
|
+
...(mapping?.payloadAssessment.requiredPaths ?? []),
|
|
716
|
+
...(mapping?.payloadAssessment.allowedPaths ?? []),
|
|
717
|
+
...(mapping?.payloadAssessment.observedPaths ?? []),
|
|
718
|
+
]).map((item) => item.toLowerCase());
|
|
719
|
+
for (const entry of relevantScenarioEntries) {
|
|
720
|
+
// Field names heuristically inferred only from a TP token (for example
|
|
721
|
+
// TP-HEALTH-HTTP-200 -> hEALTH) are not execution-safety evidence. Exclude
|
|
722
|
+
// only when the field is also present in the deterministic payload contract
|
|
723
|
+
// or observed Python payload shape.
|
|
724
|
+
const normalizedField = entry.field?.toLowerCase();
|
|
725
|
+
const fieldIsPayloadBound = Boolean(normalizedField && payloadPaths.some((item) => item === normalizedField || item.endsWith(`.${normalizedField}`)));
|
|
726
|
+
if (fieldIsPayloadBound && entry.status !== "MATCH")
|
|
727
|
+
reasons.push(`scenario-param ${entry.tpId} field ${entry.field} is ${entry.status}`);
|
|
728
|
+
}
|
|
729
|
+
if (reasons.length === 0)
|
|
730
|
+
eligibleItemIds.push(itemId);
|
|
731
|
+
else
|
|
732
|
+
excludedItems.push({ itemId, ...(mapping?.caseId ? { caseId: mapping.caseId } : {}), ...(symbol ? { symbol } : {}), reasons: orderedUnique(reasons) });
|
|
733
|
+
}
|
|
734
|
+
return { eligibleItemIds, excludedItems };
|
|
735
|
+
}
|
|
736
|
+
function orderedUnique(values) {
|
|
737
|
+
return [...new Set(values)];
|
|
738
|
+
}
|
|
442
739
|
export async function materializeBackendTestExecutionReadiness(input) {
|
|
443
740
|
if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
|
|
444
741
|
throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
|
|
445
742
|
}
|
|
446
743
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts);
|
|
447
|
-
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
448
|
-
if (priorityOnly.length > 0) {
|
|
449
|
-
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
450
|
-
}
|
|
451
744
|
assertSameInventory(input.effective, current);
|
|
452
|
-
const
|
|
745
|
+
const eligibility = input.eligibility ?? { eligibleItemIds: input.effective.collectedItemIds, excludedItems: [] };
|
|
746
|
+
const status = eligibility.eligibleItemIds.length === 0
|
|
453
747
|
? "BLOCKED"
|
|
454
|
-
: input.scenarioParamStatus === "PASS"
|
|
748
|
+
: input.scenarioParamStatus === "PASS" && eligibility.excludedItems.length === 0
|
|
455
749
|
? "PASS"
|
|
456
750
|
: "PARTIAL";
|
|
457
751
|
const readiness = backendTestExecutionReadinessSchema.parse({
|
|
458
|
-
schemaId: "backend-test-execution-readiness-
|
|
752
|
+
schemaId: "backend-test-execution-readiness-v2",
|
|
459
753
|
status,
|
|
460
754
|
collectionStatus: "PASS",
|
|
461
755
|
fixtureResolutionStatus: "PASS",
|
|
@@ -466,8 +760,11 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
466
760
|
},
|
|
467
761
|
mappedScripts: input.effective.mappedScripts,
|
|
468
762
|
collectedItemIds: input.effective.collectedItemIds,
|
|
763
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
764
|
+
excludedItems: eligibility.excludedItems,
|
|
469
765
|
fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
|
|
470
766
|
assetHashes: input.effective.inputHashes,
|
|
767
|
+
eligibilityInputHashes: input.eligibilityInputHashes ?? {},
|
|
471
768
|
});
|
|
472
769
|
const contractsDir = path.join(input.runDir, "contracts");
|
|
473
770
|
await mkdir(contractsDir, { recursive: true });
|
|
@@ -477,27 +774,145 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
477
774
|
export async function readBackendTestExecutionReadiness(filePath) {
|
|
478
775
|
return backendTestExecutionReadinessSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
479
776
|
}
|
|
777
|
+
function formatReadinessCounts(readiness) {
|
|
778
|
+
return `mapped=${readiness.mappedScripts.length} collected=${readiness.collectedItemIds.length} eligible=${readiness.eligibleItemIds.length} excluded=${readiness.excludedItems.length}`;
|
|
779
|
+
}
|
|
780
|
+
export function classifyExclusionReason(reason) {
|
|
781
|
+
if (reason === "no exact Markdown Case/primary-symbol correspondence")
|
|
782
|
+
return "correspondence-missing";
|
|
783
|
+
if (/^correspondence status is\b/.test(reason))
|
|
784
|
+
return "correspondence-status";
|
|
785
|
+
if (/^payload contract status is\b/.test(reason))
|
|
786
|
+
return "payload-contract-status";
|
|
787
|
+
if (/^scenario-param\b.*\bfield\b/.test(reason))
|
|
788
|
+
return "scenario-param-field";
|
|
789
|
+
return "other";
|
|
790
|
+
}
|
|
791
|
+
/** Fixed-category tally for readiness Error messages — never echoes raw reason text. */
|
|
792
|
+
function tallyExclusionReasonCategories(excludedItems) {
|
|
793
|
+
const counts = new Map();
|
|
794
|
+
for (const item of excludedItems) {
|
|
795
|
+
for (const reason of item.reasons) {
|
|
796
|
+
const category = classifyExclusionReason(reason);
|
|
797
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const parts = READINESS_EXCLUSION_CATEGORIES
|
|
801
|
+
.filter((category) => (counts.get(category) ?? 0) > 0)
|
|
802
|
+
.map((category) => `${category}=${counts.get(category)}`);
|
|
803
|
+
return parts.length > 0 ? parts.join("; ") : "none";
|
|
804
|
+
}
|
|
805
|
+
const READINESS_ERROR_TRUNCATION_SUFFIX = "...[truncated]";
|
|
806
|
+
export function boundReadinessErrorMessage(message) {
|
|
807
|
+
if (message.length <= MAX_READINESS_ERROR_CHARS)
|
|
808
|
+
return message;
|
|
809
|
+
// Truncate to leave room for the suffix so the final message never exceeds
|
|
810
|
+
// MAX_READINESS_ERROR_CHARS. Never append beyond the declared limit.
|
|
811
|
+
const limit = MAX_READINESS_ERROR_CHARS - READINESS_ERROR_TRUNCATION_SUFFIX.length;
|
|
812
|
+
return `${message.slice(0, Math.max(0, limit))}${READINESS_ERROR_TRUNCATION_SUFFIX}`;
|
|
813
|
+
}
|
|
814
|
+
function throwReadinessError(message) {
|
|
815
|
+
throw new Error(boundReadinessErrorMessage(message));
|
|
816
|
+
}
|
|
817
|
+
function firstInventoryDeltaPath(expectedHashes, actualHashes) {
|
|
818
|
+
const expectedFiles = Object.keys(expectedHashes).sort();
|
|
819
|
+
const actualFiles = Object.keys(actualHashes).sort();
|
|
820
|
+
const actualSet = new Set(actualFiles);
|
|
821
|
+
const expectedSet = new Set(expectedFiles);
|
|
822
|
+
const onlyExpected = expectedFiles.find((file) => !actualSet.has(file));
|
|
823
|
+
if (onlyExpected)
|
|
824
|
+
return onlyExpected;
|
|
825
|
+
return actualFiles.find((file) => !expectedSet.has(file));
|
|
826
|
+
}
|
|
827
|
+
async function firstUnreadableAssetPath(workspaceRoot, readiness) {
|
|
828
|
+
const candidatePaths = [...new Set([
|
|
829
|
+
...Object.keys(readiness.assetHashes),
|
|
830
|
+
...readiness.mappedScripts,
|
|
831
|
+
])].sort();
|
|
832
|
+
for (const relativePath of candidatePaths) {
|
|
833
|
+
try {
|
|
834
|
+
await readFile(path.resolve(workspaceRoot, relativePath));
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return relativePath;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
480
842
|
export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
|
|
481
843
|
if (!["PASS", "PARTIAL"].includes(readiness.status)) {
|
|
482
|
-
|
|
844
|
+
const counts = formatReadinessCounts(readiness);
|
|
845
|
+
const reasonTally = readiness.eligibleItemIds.length === 0 && readiness.excludedItems.length > 0
|
|
846
|
+
? `; exclusionReasons: ${tallyExclusionReasonCategories(readiness.excludedItems)}`
|
|
847
|
+
: "";
|
|
848
|
+
throwReadinessError(`backend pytest execution readiness is ${readiness.status}: ${counts}${reasonTally}`);
|
|
849
|
+
}
|
|
850
|
+
if (readiness.eligibleItemIds.length === 0) {
|
|
851
|
+
throwReadinessError(`backend pytest execution readiness authorizes zero eligible items: ${tallyExclusionReasonCategories(readiness.excludedItems)}`);
|
|
852
|
+
}
|
|
853
|
+
const missingEligibleCount = readiness.eligibleItemIds.filter((itemId) => !readiness.collectedItemIds.includes(itemId)).length;
|
|
854
|
+
if (missingEligibleCount > 0) {
|
|
855
|
+
// Do not replay raw pytest item IDs: they may contain TP-*, field, payload,
|
|
856
|
+
// secret-like or parameter markers. Report only the fixed category + count.
|
|
857
|
+
throwReadinessError(`backend pytest readiness has ${missingEligibleCount} eligible item(s) absent from collected items: ${formatReadinessCounts(readiness)}`);
|
|
858
|
+
}
|
|
859
|
+
const unreadableAsset = await firstUnreadableAssetPath(workspaceRoot, readiness);
|
|
860
|
+
if (unreadableAsset) {
|
|
861
|
+
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${unreadableAsset}`);
|
|
862
|
+
}
|
|
863
|
+
let current;
|
|
864
|
+
try {
|
|
865
|
+
current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
const fallbackPath = [...readiness.mappedScripts].sort()[0] ?? Object.keys(readiness.assetHashes).sort()[0] ?? "unknown";
|
|
869
|
+
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${fallbackPath}`);
|
|
483
870
|
}
|
|
484
|
-
const current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
485
871
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
486
872
|
if (priorityOnly.length > 0) {
|
|
487
|
-
|
|
873
|
+
throwReadinessError(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
874
|
+
}
|
|
875
|
+
const expectedHashes = readiness.assetHashes;
|
|
876
|
+
const inventoryDelta = firstInventoryDeltaPath(expectedHashes, current.inputHashes);
|
|
877
|
+
if (inventoryDelta) {
|
|
878
|
+
throwReadinessError(`backend pytest collection asset inventory drift: firstPath=${inventoryDelta}`);
|
|
879
|
+
}
|
|
880
|
+
for (const file of Object.keys(expectedHashes).sort()) {
|
|
881
|
+
if (expectedHashes[file] !== current.inputHashes[file]) {
|
|
882
|
+
throwReadinessError(`backend pytest collection hash drift: ${file}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
for (const relativePath of Object.keys(readiness.eligibilityInputHashes).sort()) {
|
|
886
|
+
const expectedHash = readiness.eligibilityInputHashes[relativePath];
|
|
887
|
+
const absolute = path.resolve(workspaceRoot, relativePath);
|
|
888
|
+
let actualHash;
|
|
889
|
+
try {
|
|
890
|
+
actualHash = sha256(await readFile(absolute));
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
throwReadinessError(`backend pytest eligibility input hash drift: firstPath=${relativePath}`);
|
|
894
|
+
}
|
|
895
|
+
if (actualHash !== expectedHash) {
|
|
896
|
+
throwReadinessError(`backend pytest eligibility input hash drift: ${relativePath}`);
|
|
897
|
+
}
|
|
488
898
|
}
|
|
489
|
-
assertSameInventory({ mappedScripts: readiness.mappedScripts, assetFiles: Object.keys(readiness.assetHashes), inputHashes: readiness.assetHashes }, current);
|
|
490
899
|
}
|
|
491
900
|
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
|
|
492
901
|
if (effective.phase !== "effective" || effective.status !== "PASS") {
|
|
493
902
|
throw new Error("backend pytest execution requires effective collection PASS facts");
|
|
494
903
|
}
|
|
495
904
|
const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
|
|
905
|
+
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
906
|
+
if (priorityOnly.length > 0)
|
|
907
|
+
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
496
908
|
assertSameInventory(effective, current);
|
|
497
909
|
}
|
|
498
910
|
export async function materializeEffectiveBackendPytestCollection(input) {
|
|
499
911
|
if (input.initial.status === "PASS") {
|
|
500
912
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
|
|
913
|
+
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
914
|
+
if (priorityOnly.length > 0)
|
|
915
|
+
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
501
916
|
assertSameInventory(input.initial, current);
|
|
502
917
|
return backendPytestCollectionFactsSchema.parse({
|
|
503
918
|
...input.initial,
|
|
@@ -513,6 +928,9 @@ export async function materializeEffectiveBackendPytestCollection(input) {
|
|
|
513
928
|
throw new Error("backend pytest collection still fails after bounded repair");
|
|
514
929
|
await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
|
|
515
930
|
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
|
|
931
|
+
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
932
|
+
if (priorityOnly.length > 0)
|
|
933
|
+
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
516
934
|
assertSameInventory(input.final, current);
|
|
517
935
|
return backendPytestCollectionFactsSchema.parse({
|
|
518
936
|
...input.final,
|