@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +114 -109
- package/README.md +2 -2
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +107 -20
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/governance/checks.js +1 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -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 +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -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/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 +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- 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 +20 -2
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +133 -7
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- 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/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +239 -52
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +267 -238
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -200,14 +200,170 @@ export async function inspectBackendTestEnvironment(input) {
|
|
|
200
200
|
serverEntries,
|
|
201
201
|
};
|
|
202
202
|
}
|
|
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
|
+
}
|
|
203
352
|
export async function markdownFiles(root, layout) {
|
|
204
353
|
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
205
354
|
const directory = path.join(root, ...resolved.markdownDir.split("/"));
|
|
206
355
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
207
|
-
|
|
356
|
+
const all = entries
|
|
208
357
|
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
209
358
|
.map((entry) => path.join(directory, entry.name))
|
|
210
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();
|
|
211
367
|
}
|
|
212
368
|
function canonicalBackendCaseId(value) {
|
|
213
369
|
const upper = value.toUpperCase();
|
|
@@ -453,6 +609,17 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
453
609
|
const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
|
|
454
610
|
if (!(await exists(readme)))
|
|
455
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
|
+
}
|
|
456
623
|
const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
|
|
457
624
|
if (moduleFiles.length === 0)
|
|
458
625
|
findings.push(`no ${layout.markdownDir} module case file found`);
|
|
@@ -562,6 +729,7 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
562
729
|
`- Cases: ${caseCount}`,
|
|
563
730
|
`- Covered AC IDs: ${[...coveredAc].sort().join(", ") || "none"}`,
|
|
564
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})`,
|
|
565
733
|
`- Findings: ${findings.length}`,
|
|
566
734
|
"- Required sections: Preconditions/前置条件, Steps/操作步骤, Expected Results/预期结果",
|
|
567
735
|
"- Source-reference validity check: not applied",
|
|
@@ -583,6 +751,13 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
583
751
|
? safetyFindings.map((finding) => `- ${finding}`)
|
|
584
752
|
: ["- No secret-shaped values detected."]),
|
|
585
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
|
+
"",
|
|
586
761
|
"## Findings",
|
|
587
762
|
"",
|
|
588
763
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
|
|
@@ -20,6 +20,14 @@ export const backendPytestCollectionFindingSchema = z.object({
|
|
|
20
20
|
classification: z.literal("test-asset-defect"),
|
|
21
21
|
repairability: z.enum(["repairable", "blocked"]),
|
|
22
22
|
detail: z.string().min(1),
|
|
23
|
+
caseId: z.string().min(1).optional(),
|
|
24
|
+
reasonCodes: z.array(z.string().min(1)).optional(),
|
|
25
|
+
repairPaths: z.array(z.string().min(1)).optional(),
|
|
26
|
+
payloadExpectedPaths: z.array(z.string()).optional(),
|
|
27
|
+
payloadObservedPaths: z.array(z.string()).optional(),
|
|
28
|
+
payloadMissingPaths: z.array(z.string()).optional(),
|
|
29
|
+
payloadUnexpectedPaths: z.array(z.string()).optional(),
|
|
30
|
+
payloadEnumMismatches: z.array(z.string()).optional(),
|
|
23
31
|
}).strict();
|
|
24
32
|
export const backendPytestCollectionFactsSchema = z.object({
|
|
25
33
|
schemaId: z.literal("backend-test-pytest-collection-v3"),
|
|
@@ -61,11 +69,22 @@ export const backendPytestCollectionFactsSchema = z.object({
|
|
|
61
69
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
|
|
62
70
|
}
|
|
63
71
|
});
|
|
72
|
+
const backendTestScenarioParamDiagnosticSchema = z.object({
|
|
73
|
+
reasonCode: z.enum(["SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
|
|
74
|
+
tpId: z.string().min(1).max(256),
|
|
75
|
+
field: z.string().min(1).max(256).optional(),
|
|
76
|
+
expectedRaw: z.string().max(512).optional(),
|
|
77
|
+
observedRaw: z.string().max(512),
|
|
78
|
+
expectedNormalized: z.string().max(512).optional(),
|
|
79
|
+
observedNormalized: z.string().max(512).optional(),
|
|
80
|
+
sourceLocation: z.string().max(512).optional(),
|
|
81
|
+
}).strict();
|
|
64
82
|
const backendTestExcludedItemSchema = z.object({
|
|
65
83
|
itemId: z.string().min(1),
|
|
66
84
|
caseId: z.string().optional(),
|
|
67
85
|
symbol: z.string().optional(),
|
|
68
86
|
reasons: z.array(z.string().min(1)).min(1),
|
|
87
|
+
scenarioParamDiagnostics: z.array(backendTestScenarioParamDiagnosticSchema).max(16).optional(),
|
|
69
88
|
}).strict();
|
|
70
89
|
export const backendTestExecutionReadinessSchema = z.object({
|
|
71
90
|
schemaId: z.literal("backend-test-execution-readiness-v2"),
|
|
@@ -81,6 +100,14 @@ export const backendTestExecutionReadinessSchema = z.object({
|
|
|
81
100
|
collectedItemIds: z.array(z.string()),
|
|
82
101
|
eligibleItemIds: z.array(z.string()),
|
|
83
102
|
excludedItems: z.array(backendTestExcludedItemSchema),
|
|
103
|
+
exclusionSummary: z.object({
|
|
104
|
+
excludedItemCount: z.number().int().min(0),
|
|
105
|
+
excludedCaseCount: z.number().int().min(0),
|
|
106
|
+
excludedByReason: z.record(z.string(), z.number().int().min(1)),
|
|
107
|
+
excludedByCase: z.record(z.string(), z.number().int().min(1)),
|
|
108
|
+
}).strict(),
|
|
109
|
+
pytestSpawned: z.literal(false),
|
|
110
|
+
businessTestBodyExecutedCount: z.literal(0),
|
|
84
111
|
fixtureIssues: z.array(z.string()),
|
|
85
112
|
assetHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
86
113
|
eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
@@ -131,6 +158,23 @@ function normalizeHazardOccurrenceIdentity(line) {
|
|
|
131
158
|
// Unclosed quote or no out-of-string `#`: do not invent a comment boundary.
|
|
132
159
|
return trimmed;
|
|
133
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Bare `__NNN_*` helper module names imported by a Python source snippet.
|
|
163
|
+
* Only the head token of an `import`/`from` statement is considered a module
|
|
164
|
+
* name; dotted submodules (`__551_manager_helpers.x`) collapse to the head.
|
|
165
|
+
*/
|
|
166
|
+
function importedBareHelperModules(source) {
|
|
167
|
+
const names = [];
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
for (const match of normalizeSourceNewlines(source).matchAll(/\b(?:import|from)\s+([A-Za-z0-9_.]+)/g)) {
|
|
170
|
+
const head = match[1].split(".")[0];
|
|
171
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(head) && !seen.has(head)) {
|
|
172
|
+
seen.add(head);
|
|
173
|
+
names.push(head);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return names;
|
|
177
|
+
}
|
|
134
178
|
function listPatternOccurrences(source, patternSource, flags = "") {
|
|
135
179
|
const normalized = normalizeSourceNewlines(source);
|
|
136
180
|
const pattern = new RegExp(patternSource, flags.includes("g") ? flags : `${flags}g`);
|
|
@@ -221,6 +265,8 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
221
265
|
const inputHashes = {};
|
|
222
266
|
const hazardOccurrenceCounts = {};
|
|
223
267
|
const hazardOccurrences = {};
|
|
268
|
+
const scriptBareImports = {};
|
|
269
|
+
const mappedScriptSet = new Set(normalizedScripts);
|
|
224
270
|
for (const file of assetFiles) {
|
|
225
271
|
const bytes = await readFile(path.resolve(workspaceRoot, file));
|
|
226
272
|
const source = bytes.toString("utf8");
|
|
@@ -228,8 +274,10 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
|
|
|
228
274
|
const lists = scanRepairHazardOccurrenceLists(source);
|
|
229
275
|
hazardOccurrences[file] = lists;
|
|
230
276
|
hazardOccurrenceCounts[file] = Object.fromEntries(Object.entries(lists).map(([label, identities]) => [label, identities.length]));
|
|
277
|
+
if (mappedScriptSet.has(file))
|
|
278
|
+
scriptBareImports[file] = importedBareHelperModules(source);
|
|
231
279
|
}
|
|
232
|
-
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences };
|
|
280
|
+
return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences, scriptBareImports };
|
|
233
281
|
}
|
|
234
282
|
function bounded(value) {
|
|
235
283
|
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
@@ -237,13 +285,14 @@ function bounded(value) {
|
|
|
237
285
|
? normalized
|
|
238
286
|
: `${normalized.slice(0, MAX_DIAGNOSTIC_CHARS)}\n...[truncated]`;
|
|
239
287
|
}
|
|
240
|
-
function collectionItems(stdout) {
|
|
288
|
+
function collectionItems(stdout, layout) {
|
|
289
|
+
const rootPrefix = `${(layout ?? resolveBackendTestLayout(undefined)).testRoot}/`;
|
|
241
290
|
return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
|
|
242
291
|
if (!line.includes("::"))
|
|
243
292
|
return false;
|
|
244
293
|
if (/^(ERROR|FAILED|E\s+)/.test(line))
|
|
245
294
|
return false;
|
|
246
|
-
return
|
|
295
|
+
return line.replaceAll("\\", "/").startsWith(rootPrefix);
|
|
247
296
|
}).map((line) => line.replaceAll("\\", "/")))];
|
|
248
297
|
}
|
|
249
298
|
function escapeRegExp(value) {
|
|
@@ -308,8 +357,68 @@ function classifyFixtureResolutionFailure(output, layout) {
|
|
|
308
357
|
repairPaths: paths,
|
|
309
358
|
};
|
|
310
359
|
}
|
|
311
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Generated-local bare helper module signatures found in the bound pytest
|
|
362
|
+
* asset inventory. A file such as `testcase/__551_manager_helpers.py` in the
|
|
363
|
+
* test root/script dir is generated-local: when collection reports
|
|
364
|
+
* `No module named '__551_manager_helpers'`, the defect can be precisely
|
|
365
|
+
* attributed to the generated mapped script instead of a missing third-party
|
|
366
|
+
* package.
|
|
367
|
+
*/
|
|
368
|
+
function generatedLocalBareHelperNames(inventory) {
|
|
369
|
+
if (!inventory)
|
|
370
|
+
return [];
|
|
371
|
+
const names = [];
|
|
372
|
+
for (const file of inventory.assetFiles) {
|
|
373
|
+
const stem = path.posix.basename(file).replace(/\.py$/, "");
|
|
374
|
+
if (/^__\d+[A-Za-z0-9_]*$/.test(stem))
|
|
375
|
+
names.push(stem);
|
|
376
|
+
}
|
|
377
|
+
return [...new Set(names)];
|
|
378
|
+
}
|
|
379
|
+
function classifyCollectionFailure(output, evidence) {
|
|
312
380
|
const normalized = output.replaceAll("\\", "/");
|
|
381
|
+
const helperNames = generatedLocalBareHelperNames(evidence?.inventory);
|
|
382
|
+
const helperByLower = new Map(helperNames.map((name) => [name.toLowerCase(), name]));
|
|
383
|
+
// A bare `__NNN_...` module import that collection cannot find is only a
|
|
384
|
+
// candidate generated-local helper signature. REPAIRABLE requires two
|
|
385
|
+
// deterministic pieces of evidence: (1) a helper asset of the same name in
|
|
386
|
+
// the bound inventory, and (2) at least one mapped generated script whose
|
|
387
|
+
// source truly imports that bare module. Without both — or when attribution
|
|
388
|
+
// is impossible — fail closed as BLOCKED so an un-attributable dependency
|
|
389
|
+
// error is never widened into a repair. Genuine third-party modules (no
|
|
390
|
+
// `__NNN_` signature) stay BLOCKED below.
|
|
391
|
+
const missingModule = /No module named ['\"]([^'\"]+)['\"]/i.exec(normalized)?.[1];
|
|
392
|
+
if (missingModule && /^__\d+[A-Za-z0-9_]*$/.test(missingModule)) {
|
|
393
|
+
const helperAsset = helperByLower.get(missingModule.toLowerCase());
|
|
394
|
+
const inventory = evidence?.inventory;
|
|
395
|
+
const scriptBareImports = inventory?.scriptBareImports ?? {};
|
|
396
|
+
const missingLower = missingModule.toLowerCase();
|
|
397
|
+
const importedByMappedScript = inventory
|
|
398
|
+
? inventory.mappedScripts.some((script) => (scriptBareImports[script] ?? []).some((name) => name.toLowerCase() === missingLower))
|
|
399
|
+
: false;
|
|
400
|
+
if (helperAsset && importedByMappedScript && inventory) {
|
|
401
|
+
const repairPaths = [...new Set([
|
|
402
|
+
...(helperAsset
|
|
403
|
+
? inventory.assetFiles.filter((file) => path.posix.basename(file).replace(/\.py$/, "") === helperAsset)
|
|
404
|
+
: []),
|
|
405
|
+
...inventory.mappedScripts,
|
|
406
|
+
])].sort();
|
|
407
|
+
return {
|
|
408
|
+
status: "REPAIRABLE",
|
|
409
|
+
kind: "missing-generated-local-helper",
|
|
410
|
+
detail: `generated testcase asset imports missing bare generated-local helper module ${missingModule}; inline the helper or remove the generated shared dependency`,
|
|
411
|
+
repairPaths,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
status: "BLOCKED",
|
|
416
|
+
kind: "missing-generated-local-helper-without-evidence",
|
|
417
|
+
detail: helperAsset
|
|
418
|
+
? `missing bare generated-local helper module ${missingModule} has a name match but no mapped generated script imports it (no precise generated-local attribution)`
|
|
419
|
+
: `missing bare __NNN_* module ${missingModule} has no generated-local helper asset in the bound inventory (cannot be precisely attributed)`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
313
422
|
const blockedPatterns = [
|
|
314
423
|
[/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
|
|
315
424
|
[/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
|
|
@@ -335,7 +444,7 @@ function classifyCollectionFailure(output) {
|
|
|
335
444
|
return { status: "BLOCKED", kind: "unclassified-collection-error", detail: "collection failure cannot be safely attributed to generated testcase assets" };
|
|
336
445
|
}
|
|
337
446
|
export function assessBackendPytestCollection(input) {
|
|
338
|
-
const items = collectionItems(input.stdout);
|
|
447
|
+
const items = collectionItems(input.stdout, input.layout);
|
|
339
448
|
const fixtureResolution = input.fixtureResolution ?? { exitCode: 0, stdout: "fixture resolution assumed by direct assessor", stderr: "" };
|
|
340
449
|
if (input.exitCode === 0 && fixtureResolution.exitCode === 0) {
|
|
341
450
|
return backendPytestCollectionFactsSchema.parse({
|
|
@@ -401,7 +510,7 @@ export function assessBackendPytestCollection(input) {
|
|
|
401
510
|
fixtureStderrExcerpt: bounded(fixtureResolution.stderr),
|
|
402
511
|
});
|
|
403
512
|
}
|
|
404
|
-
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}
|
|
513
|
+
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`, { layout: input.layout, inventory: input.inventory });
|
|
405
514
|
return backendPytestCollectionFactsSchema.parse({
|
|
406
515
|
schemaId: "backend-test-pytest-collection-v3",
|
|
407
516
|
phase: input.phase,
|
|
@@ -412,7 +521,9 @@ export function assessBackendPytestCollection(input) {
|
|
|
412
521
|
fixtureResolutionAttempted: false,
|
|
413
522
|
fixtureResolutionStatus: "NOT_RUN",
|
|
414
523
|
fixtureResolutionExitCode: null,
|
|
415
|
-
repairPaths:
|
|
524
|
+
repairPaths: classification.repairPaths && classification.repairPaths.length > 0
|
|
525
|
+
? classification.repairPaths
|
|
526
|
+
: testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
|
|
416
527
|
mappedScripts: input.inventory.mappedScripts,
|
|
417
528
|
existingMappedScripts: input.inventory.mappedScripts,
|
|
418
529
|
missingMappedScripts: [],
|
|
@@ -618,6 +729,7 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
|
618
729
|
const parameterTokens = orderedUnique((itemId.match(/TP-[A-Z0-9-]+/g) ?? []));
|
|
619
730
|
const mapping = input.correspondenceEntries.find((entry) => symbol && (entry.declaredPrimarySymbol === symbol || entry.pytestSymbols.includes(symbol)));
|
|
620
731
|
const reasons = [];
|
|
732
|
+
const scenarioParamDiagnostics = [];
|
|
621
733
|
if (!mapping)
|
|
622
734
|
reasons.push("no exact Markdown Case/primary-symbol correspondence");
|
|
623
735
|
if (mapping && mapping.status !== "EXACT_1_TO_1")
|
|
@@ -640,13 +752,30 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
|
|
|
640
752
|
// or observed Python payload shape.
|
|
641
753
|
const normalizedField = entry.field?.toLowerCase();
|
|
642
754
|
const fieldIsPayloadBound = Boolean(normalizedField && payloadPaths.some((item) => item === normalizedField || item.endsWith(`.${normalizedField}`)));
|
|
643
|
-
if (fieldIsPayloadBound && entry.status !== "MATCH")
|
|
755
|
+
if (fieldIsPayloadBound && entry.status !== "MATCH") {
|
|
644
756
|
reasons.push(`scenario-param ${entry.tpId} field ${entry.field} is ${entry.status}`);
|
|
757
|
+
scenarioParamDiagnostics.push({
|
|
758
|
+
reasonCode: entry.status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
759
|
+
tpId: entry.tpId,
|
|
760
|
+
...(entry.field ? { field: entry.field } : {}),
|
|
761
|
+
...(entry.expectedRaw !== undefined ? { expectedRaw: entry.expectedRaw.slice(0, 512) } : {}),
|
|
762
|
+
observedRaw: (entry.observedRaw ?? "unavailable").slice(0, 512),
|
|
763
|
+
...(entry.expectedNormalized !== undefined ? { expectedNormalized: entry.expectedNormalized.slice(0, 512) } : {}),
|
|
764
|
+
...(entry.observedNormalized !== undefined ? { observedNormalized: entry.observedNormalized.slice(0, 512) } : {}),
|
|
765
|
+
...(entry.sourceLocation ? { sourceLocation: entry.sourceLocation.slice(0, 512) } : {}),
|
|
766
|
+
});
|
|
767
|
+
}
|
|
645
768
|
}
|
|
646
769
|
if (reasons.length === 0)
|
|
647
770
|
eligibleItemIds.push(itemId);
|
|
648
771
|
else
|
|
649
|
-
excludedItems.push({
|
|
772
|
+
excludedItems.push({
|
|
773
|
+
itemId,
|
|
774
|
+
...(mapping?.caseId ? { caseId: mapping.caseId } : {}),
|
|
775
|
+
...(symbol ? { symbol } : {}),
|
|
776
|
+
reasons: orderedUnique(reasons),
|
|
777
|
+
...(scenarioParamDiagnostics.length > 0 ? { scenarioParamDiagnostics: scenarioParamDiagnostics.slice(0, 16) } : {}),
|
|
778
|
+
});
|
|
650
779
|
}
|
|
651
780
|
return { eligibleItemIds, excludedItems };
|
|
652
781
|
}
|
|
@@ -657,7 +786,7 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
657
786
|
if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
|
|
658
787
|
throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
|
|
659
788
|
}
|
|
660
|
-
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts);
|
|
789
|
+
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts, { layout: input.layout });
|
|
661
790
|
assertSameInventory(input.effective, current);
|
|
662
791
|
const eligibility = input.eligibility ?? { eligibleItemIds: input.effective.collectedItemIds, excludedItems: [] };
|
|
663
792
|
const status = eligibility.eligibleItemIds.length === 0
|
|
@@ -679,6 +808,9 @@ export async function materializeBackendTestExecutionReadiness(input) {
|
|
|
679
808
|
collectedItemIds: input.effective.collectedItemIds,
|
|
680
809
|
eligibleItemIds: eligibility.eligibleItemIds,
|
|
681
810
|
excludedItems: eligibility.excludedItems,
|
|
811
|
+
exclusionSummary: summarizeBackendTestExclusions(eligibility.excludedItems),
|
|
812
|
+
pytestSpawned: false,
|
|
813
|
+
businessTestBodyExecutedCount: 0,
|
|
682
814
|
fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
|
|
683
815
|
assetHashes: input.effective.inputHashes,
|
|
684
816
|
eligibilityInputHashes: input.eligibilityInputHashes ?? {},
|
|
@@ -694,6 +826,26 @@ export async function readBackendTestExecutionReadiness(filePath) {
|
|
|
694
826
|
function formatReadinessCounts(readiness) {
|
|
695
827
|
return `mapped=${readiness.mappedScripts.length} collected=${readiness.collectedItemIds.length} eligible=${readiness.eligibleItemIds.length} excluded=${readiness.excludedItems.length}`;
|
|
696
828
|
}
|
|
829
|
+
export function summarizeBackendTestExclusions(excludedItems) {
|
|
830
|
+
const reasonCounts = new Map();
|
|
831
|
+
const caseCounts = new Map();
|
|
832
|
+
for (const item of excludedItems) {
|
|
833
|
+
if (item.caseId)
|
|
834
|
+
caseCounts.set(item.caseId, (caseCounts.get(item.caseId) ?? 0) + 1);
|
|
835
|
+
for (const reason of item.reasons) {
|
|
836
|
+
const category = classifyExclusionReason(reason);
|
|
837
|
+
reasonCounts.set(category, (reasonCounts.get(category) ?? 0) + 1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
excludedItemCount: excludedItems.length,
|
|
842
|
+
excludedCaseCount: caseCounts.size,
|
|
843
|
+
excludedByReason: Object.fromEntries(READINESS_EXCLUSION_CATEGORIES
|
|
844
|
+
.filter((category) => (reasonCounts.get(category) ?? 0) > 0)
|
|
845
|
+
.map((category) => [category, reasonCounts.get(category)])),
|
|
846
|
+
excludedByCase: Object.fromEntries([...caseCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
697
849
|
export function classifyExclusionReason(reason) {
|
|
698
850
|
if (reason === "no exact Markdown Case/primary-symbol correspondence")
|
|
699
851
|
return "correspondence-missing";
|
|
@@ -756,7 +908,7 @@ async function firstUnreadableAssetPath(workspaceRoot, readiness) {
|
|
|
756
908
|
}
|
|
757
909
|
return undefined;
|
|
758
910
|
}
|
|
759
|
-
export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
|
|
911
|
+
export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness, layout) {
|
|
760
912
|
if (!["PASS", "PARTIAL"].includes(readiness.status)) {
|
|
761
913
|
const counts = formatReadinessCounts(readiness);
|
|
762
914
|
const reasonTally = readiness.eligibleItemIds.length === 0 && readiness.excludedItems.length > 0
|
|
@@ -779,7 +931,7 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
|
|
|
779
931
|
}
|
|
780
932
|
let current;
|
|
781
933
|
try {
|
|
782
|
-
current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
|
|
934
|
+
current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts, { layout });
|
|
783
935
|
}
|
|
784
936
|
catch {
|
|
785
937
|
const fallbackPath = [...readiness.mappedScripts].sort()[0] ?? Object.keys(readiness.assetHashes).sort()[0] ?? "unknown";
|
|
@@ -814,11 +966,11 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
|
|
|
814
966
|
}
|
|
815
967
|
}
|
|
816
968
|
}
|
|
817
|
-
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
|
|
969
|
+
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective, layout) {
|
|
818
970
|
if (effective.phase !== "effective" || effective.status !== "PASS") {
|
|
819
971
|
throw new Error("backend pytest execution requires effective collection PASS facts");
|
|
820
972
|
}
|
|
821
|
-
const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
|
|
973
|
+
const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts, { layout });
|
|
822
974
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
823
975
|
if (priorityOnly.length > 0)
|
|
824
976
|
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
@@ -826,7 +978,7 @@ export async function assertBackendPytestCollectionFresh(workspaceRoot, effectiv
|
|
|
826
978
|
}
|
|
827
979
|
export async function materializeEffectiveBackendPytestCollection(input) {
|
|
828
980
|
if (input.initial.status === "PASS") {
|
|
829
|
-
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
|
|
981
|
+
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts, { layout: input.layout });
|
|
830
982
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
831
983
|
if (priorityOnly.length > 0)
|
|
832
984
|
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
@@ -844,7 +996,7 @@ export async function materializeEffectiveBackendPytestCollection(input) {
|
|
|
844
996
|
if (input.final.status !== "PASS")
|
|
845
997
|
throw new Error("backend pytest collection still fails after bounded repair");
|
|
846
998
|
await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
|
|
847
|
-
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
|
|
999
|
+
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts, { layout: input.layout });
|
|
848
1000
|
const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
|
|
849
1001
|
if (priorityOnly.length > 0)
|
|
850
1002
|
throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
|
|
@@ -70,6 +70,7 @@ export const backendTestResultContractSchema = z
|
|
|
70
70
|
schemaVersion: z.literal(1),
|
|
71
71
|
/** Process-level status of the pytest invocation. Task Pool: use with outcome. */
|
|
72
72
|
executionStatus: backendTestExecutionStatusSchema,
|
|
73
|
+
pytestSpawned: z.literal(true),
|
|
73
74
|
pytestExitCode: z.number().int().min(0).max(255),
|
|
74
75
|
collectionStatus: backendTestCollectionStatusSchema,
|
|
75
76
|
/** Total tests counted from JUnit (or 0 when report unavailable). */
|
|
@@ -659,6 +660,7 @@ export function deriveBackendTestResult(input) {
|
|
|
659
660
|
const result = {
|
|
660
661
|
schemaVersion: 1,
|
|
661
662
|
executionStatus,
|
|
663
|
+
pytestSpawned: true,
|
|
662
664
|
pytestExitCode: input.pytestExitCode,
|
|
663
665
|
collectionStatus: "unknown",
|
|
664
666
|
tests: 0,
|
|
@@ -733,6 +735,7 @@ export function deriveBackendTestResult(input) {
|
|
|
733
735
|
const result = {
|
|
734
736
|
schemaVersion: 1,
|
|
735
737
|
executionStatus,
|
|
738
|
+
pytestSpawned: true,
|
|
736
739
|
pytestExitCode: exit,
|
|
737
740
|
collectionStatus,
|
|
738
741
|
tests: parsed.tests,
|
|
@@ -936,6 +939,7 @@ export async function materializeBackendTestResultFromPytestHtml(input) {
|
|
|
936
939
|
const result = backendTestResultContractSchema.parse({
|
|
937
940
|
schemaVersion: 1,
|
|
938
941
|
executionStatus,
|
|
942
|
+
pytestSpawned: true,
|
|
939
943
|
pytestExitCode: exit,
|
|
940
944
|
collectionStatus,
|
|
941
945
|
tests: parsed.tests,
|