@tea-agent/loop-agent 0.38.0 → 0.38.2
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/CHANGELOG.md +191 -0
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/executors/dag-pi-executor.js +42 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/shared/openspec-spec.js +76 -0
- package/dist/shared/operator/capabilities.js +149 -27
- package/dist/task/config-types.js +43 -0
- package/dist/task/frontend-project-capability.js +292 -38
- 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/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +477 -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 +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +128 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +8 -1
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
- package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -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 +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- 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/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
- package/dist/workflows/dag/init-hybrid.js +920 -110
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +77 -8
- package/dist/workflows/dag/prompt.js +45 -0
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +33 -0
- package/dist/workflows/dag/types.js +148 -4
- package/dist/workflows/dag/validate.js +7 -2
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-design-contract.md +40 -14
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-task-constraints.md +40 -13
- package/docs/templates/frontend-task-requirement.md +54 -20
- package/harness.json +3 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +15 -11
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
|
@@ -27,11 +27,13 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
27
27
|
import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
|
|
28
28
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
29
29
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
30
|
+
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
31
|
+
import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
|
|
30
32
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
31
33
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
32
|
-
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
34
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
33
35
|
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
34
|
-
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
36
|
+
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
35
37
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
36
38
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
37
39
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
@@ -48,6 +50,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
48
50
|
phase: "initial",
|
|
49
51
|
repairAttempt: 0,
|
|
50
52
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
53
|
+
layout: input.layout,
|
|
51
54
|
});
|
|
52
55
|
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
53
56
|
runDir: input.runDir,
|
|
@@ -73,6 +76,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
73
76
|
phase: "final",
|
|
74
77
|
repairAttempt: 1,
|
|
75
78
|
strictScenarioParamGate: input.strictScenarioParamGate,
|
|
79
|
+
layout: input.layout,
|
|
76
80
|
});
|
|
77
81
|
finalFacts = reassessed.facts;
|
|
78
82
|
finalMarkdown = reassessed.markdown;
|
|
@@ -492,11 +496,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
|
|
|
492
496
|
async function executeBackendTestPipeline(input, meta) {
|
|
493
497
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
494
498
|
const started = Date.now();
|
|
499
|
+
// Plan A: prefer the frozen layout carried by the compiled spec; fall back
|
|
500
|
+
// to the historical default for hand-written legacy DAGs.
|
|
501
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
502
|
+
? {
|
|
503
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
504
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
505
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
506
|
+
}
|
|
507
|
+
: undefined);
|
|
495
508
|
try {
|
|
496
509
|
if (!pipeline)
|
|
497
510
|
throw new Error("missing backend-test pipeline id");
|
|
498
511
|
const outputs = [];
|
|
499
|
-
if (pipeline === "
|
|
512
|
+
if (pipeline === "ingest-backend-test-gap") {
|
|
513
|
+
// Plan D: deterministic gap ingest. Identity (taskId + requirement
|
|
514
|
+
// sha256) is fail-closed; a mismatched plan blocks every writer.
|
|
515
|
+
const gapDocRel = meta.spec.globalConstraints
|
|
516
|
+
?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
|
|
517
|
+
.find(Boolean);
|
|
518
|
+
const binding = meta.spec.sourceBinding;
|
|
519
|
+
const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
|
|
520
|
+
if (!gapDocRel || !binding || !requirementSha256) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
stdout: outputs.join("\n"),
|
|
524
|
+
stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
|
|
525
|
+
failureCategory: "invalid-output",
|
|
526
|
+
durationMs: Date.now() - started,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
stdout: outputs.join("\n"),
|
|
538
|
+
stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
|
|
539
|
+
failureCategory: "invalid-output",
|
|
540
|
+
durationMs: Date.now() - started,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const doc = backendTestGapDocSchema.safeParse(parsed);
|
|
544
|
+
if (!doc.success) {
|
|
545
|
+
return {
|
|
546
|
+
ok: false,
|
|
547
|
+
stdout: outputs.join("\n"),
|
|
548
|
+
stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
|
|
549
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
550
|
+
.join("; ")}`,
|
|
551
|
+
failureCategory: "invalid-output",
|
|
552
|
+
durationMs: Date.now() - started,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// previous N6 facts (may be absent on a fresh checkout)
|
|
556
|
+
const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
|
|
557
|
+
let previousMissingSlots;
|
|
558
|
+
let previousCoveredSlots;
|
|
559
|
+
try {
|
|
560
|
+
const facts = JSON.parse(await readFile(factsPath, "utf8"));
|
|
561
|
+
previousMissingSlots = facts.scenarioPartitions?.missingSlots;
|
|
562
|
+
previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// no previous facts in the workspace: the gap doc alone drives the plan
|
|
566
|
+
}
|
|
567
|
+
// module index comes from the README on disk
|
|
568
|
+
let existingModuleStems = [];
|
|
569
|
+
try {
|
|
570
|
+
const files = await markdownFiles(input.cwd, backendLayout);
|
|
571
|
+
existingModuleStems = files
|
|
572
|
+
.map((file) => path.basename(file, ".md"))
|
|
573
|
+
.filter((stem) => stem.toLowerCase() !== "readme");
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// no markdown tree: every slot becomes a conflict
|
|
577
|
+
}
|
|
578
|
+
const plan = ingestBackendTestGap({
|
|
579
|
+
gapDoc: doc.data,
|
|
580
|
+
previousMissingSlots,
|
|
581
|
+
previousCoveredSlots,
|
|
582
|
+
existingModuleStems,
|
|
583
|
+
sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
|
|
584
|
+
layout: backendLayout,
|
|
585
|
+
taskId: binding.taskId,
|
|
586
|
+
requirementSha256,
|
|
587
|
+
});
|
|
588
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
589
|
+
await mkdir(contractsDir, { recursive: true });
|
|
590
|
+
const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
|
|
591
|
+
await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
|
|
592
|
+
outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
|
|
593
|
+
if (plan.status === "error") {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
stdout: outputs.join("\n"),
|
|
597
|
+
stderr: [
|
|
598
|
+
"backend-test gap ingest failed closed (identity/conflicts):",
|
|
599
|
+
...plan.conflicts,
|
|
600
|
+
].join("\n"),
|
|
601
|
+
failureCategory: "invalid-output",
|
|
602
|
+
durationMs: Date.now() - started,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else if (pipeline === "markdown-environment") {
|
|
500
607
|
const results = await executePipelineCommands(input, meta);
|
|
501
608
|
if (!results.every((result) => result.ok)) {
|
|
502
609
|
const failure = results.find((result) => !result.ok);
|
|
@@ -530,6 +637,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
530
637
|
environmentMarkdown,
|
|
531
638
|
requiredRequirementIds,
|
|
532
639
|
sourceBinding: meta.spec.sourceBinding,
|
|
640
|
+
layout: backendLayout,
|
|
641
|
+
sharedSetup: meta.spec.backendTestSharedSetup ?? null,
|
|
533
642
|
});
|
|
534
643
|
}
|
|
535
644
|
catch (error) {
|
|
@@ -583,6 +692,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
583
692
|
const coverage = await analyzeBackendTestCaseCoverage({
|
|
584
693
|
workspaceRoot: input.cwd,
|
|
585
694
|
sourceBinding: normalizedBinding,
|
|
695
|
+
layout: backendLayout,
|
|
586
696
|
});
|
|
587
697
|
const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
|
|
588
698
|
const contractsDir = path.join(meta.runDir, "contracts");
|
|
@@ -590,6 +700,23 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
590
700
|
const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
|
|
591
701
|
await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
|
|
592
702
|
outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
|
|
703
|
+
// Plan C: scenario-partition slot coverage is fail-closed. General
|
|
704
|
+
// coverage findings stay advisory, but a declared filter axis missing
|
|
705
|
+
// an each-value/omitted slot or its complement slot must block pytest
|
|
706
|
+
// generation until the Markdown declares the slot.
|
|
707
|
+
const partitionBlocking = coverage.facts.findings.filter((finding) => /^(?:MISSING_PARTITION|MISSING_PARTITION_SLOT|MISSING_COMPLEMENT_SLOT|MISSING_PARTITION_TABLE)/.test(finding));
|
|
708
|
+
if (partitionBlocking.length > 0) {
|
|
709
|
+
return {
|
|
710
|
+
ok: false,
|
|
711
|
+
stdout: outputs.join("\n\n"),
|
|
712
|
+
stderr: [
|
|
713
|
+
"backend-test scenario partitions are incomplete (fail-closed):",
|
|
714
|
+
...partitionBlocking,
|
|
715
|
+
].join("\n"),
|
|
716
|
+
failureCategory: "invalid-output",
|
|
717
|
+
durationMs: Date.now() - started,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
593
720
|
}
|
|
594
721
|
catch (error) {
|
|
595
722
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -606,9 +733,9 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
606
733
|
}
|
|
607
734
|
}
|
|
608
735
|
else if (pipeline === "markdown-collection-assess") {
|
|
609
|
-
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
|
|
736
|
+
const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
610
737
|
let facts;
|
|
611
|
-
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
738
|
+
const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
612
739
|
const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
|
|
613
740
|
phase: "initial",
|
|
614
741
|
mappedScripts: resolution.expectedScripts,
|
|
@@ -618,7 +745,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
618
745
|
facts = priorityOnlyFacts;
|
|
619
746
|
}
|
|
620
747
|
else if (resolution.missingScripts.length > 0) {
|
|
621
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
|
|
748
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
|
|
622
749
|
facts = assessMissingBackendPytestScripts({
|
|
623
750
|
mappedScripts: resolution.expectedScripts,
|
|
624
751
|
existingMappedScripts: resolution.existingScripts,
|
|
@@ -632,10 +759,11 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
632
759
|
const scenario = await runBackendScenarioParamPreflight({
|
|
633
760
|
workspaceRoot: input.cwd,
|
|
634
761
|
runDir: meta.runDir,
|
|
762
|
+
layout: backendLayout,
|
|
635
763
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
636
764
|
});
|
|
637
765
|
outputs.push(...scenario.outputs);
|
|
638
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
766
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
639
767
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
640
768
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
641
769
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -669,6 +797,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
669
797
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
670
798
|
}
|
|
671
799
|
facts = assessBackendPytestCollection({
|
|
800
|
+
layout: backendLayout,
|
|
672
801
|
phase: "initial",
|
|
673
802
|
mappedScripts,
|
|
674
803
|
inventory,
|
|
@@ -678,6 +807,43 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
678
807
|
fixtureResolution,
|
|
679
808
|
});
|
|
680
809
|
}
|
|
810
|
+
try {
|
|
811
|
+
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
812
|
+
workspaceRoot: input.cwd,
|
|
813
|
+
layout: backendLayout,
|
|
814
|
+
taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
|
|
815
|
+
});
|
|
816
|
+
const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
|
|
817
|
+
const contractsDir = path.join(meta.runDir, "contracts");
|
|
818
|
+
await mkdir(contractsDir, { recursive: true });
|
|
819
|
+
const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
|
|
820
|
+
await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
|
|
821
|
+
outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
|
|
822
|
+
if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
|
|
823
|
+
const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
|
|
824
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
825
|
+
.flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
|
|
826
|
+
.filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
|
|
827
|
+
if (correspondenceRepairPaths.length > 0) {
|
|
828
|
+
facts.status = "REPAIRABLE";
|
|
829
|
+
facts.repairEligible = true;
|
|
830
|
+
facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
|
|
831
|
+
facts.findings.push({
|
|
832
|
+
kind: "markdown-pytest-correspondence",
|
|
833
|
+
classification: "test-asset-defect",
|
|
834
|
+
repairability: "repairable",
|
|
835
|
+
detail: correspondence.facts.entries
|
|
836
|
+
.filter((entry) => entry.status !== "EXACT_1_TO_1")
|
|
837
|
+
.flatMap((entry) => entry.findings)
|
|
838
|
+
.join("; ")
|
|
839
|
+
.slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
|
|
846
|
+
}
|
|
681
847
|
const artifacts = await writeBackendPytestCollectionArtifacts({
|
|
682
848
|
runDir: meta.runDir,
|
|
683
849
|
stem: "initial",
|
|
@@ -715,10 +881,11 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
715
881
|
const scenario = await runBackendScenarioParamPreflight({
|
|
716
882
|
workspaceRoot: input.cwd,
|
|
717
883
|
runDir: meta.runDir,
|
|
884
|
+
layout: backendLayout,
|
|
718
885
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
719
886
|
});
|
|
720
887
|
outputs.push(...scenario.outputs);
|
|
721
|
-
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
|
|
888
|
+
const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
|
|
722
889
|
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
723
890
|
const targets = mappedScripts.map(shellQuote).join(" ");
|
|
724
891
|
const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
|
|
@@ -753,6 +920,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
753
920
|
fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
|
|
754
921
|
}
|
|
755
922
|
const finalFacts = assessBackendPytestCollection({
|
|
923
|
+
layout: backendLayout,
|
|
756
924
|
phase: "final",
|
|
757
925
|
mappedScripts,
|
|
758
926
|
inventory,
|
|
@@ -776,12 +944,57 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
776
944
|
facts: effective,
|
|
777
945
|
});
|
|
778
946
|
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
947
|
+
const correspondence = meta.spec.sourceBinding
|
|
948
|
+
? await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
949
|
+
workspaceRoot: input.cwd,
|
|
950
|
+
layout: backendLayout,
|
|
951
|
+
taskId: meta.spec.sourceBinding.taskId,
|
|
952
|
+
})
|
|
953
|
+
: undefined;
|
|
954
|
+
const eligibility = correspondence
|
|
955
|
+
? buildBackendTestItemEligibility(effective.collectedItemIds, {
|
|
956
|
+
correspondenceEntries: correspondence.facts.entries,
|
|
957
|
+
scenarioEntries: scenarioFacts.entries,
|
|
958
|
+
})
|
|
959
|
+
: { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
|
|
960
|
+
const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
|
|
961
|
+
const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
|
|
962
|
+
await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
|
|
963
|
+
await writeFile(eligibilityFactsPath, `${JSON.stringify({
|
|
964
|
+
schemaId: "backend-test-execution-eligibility-v1",
|
|
965
|
+
status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
966
|
+
collectedItemCount: effective.collectedItemIds.length,
|
|
967
|
+
eligibleItemCount: eligibility.eligibleItemIds.length,
|
|
968
|
+
excludedItemCount: eligibility.excludedItems.length,
|
|
969
|
+
eligibleItemIds: eligibility.eligibleItemIds,
|
|
970
|
+
excludedItems: eligibility.excludedItems,
|
|
971
|
+
inputHashes: eligibilityInputHashes,
|
|
972
|
+
}, null, 2)}\n`, "utf8");
|
|
973
|
+
const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
|
|
974
|
+
"# Backend Test Execution Eligibility",
|
|
975
|
+
"",
|
|
976
|
+
"## Status",
|
|
977
|
+
"",
|
|
978
|
+
eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
|
|
979
|
+
"",
|
|
980
|
+
`- Collected items: ${effective.collectedItemIds.length}`,
|
|
981
|
+
`- Eligible items: ${eligibility.eligibleItemIds.length}`,
|
|
982
|
+
`- Excluded items: ${eligibility.excludedItems.length}`,
|
|
983
|
+
"",
|
|
984
|
+
"## Excluded Items",
|
|
985
|
+
"",
|
|
986
|
+
...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
|
|
987
|
+
"",
|
|
988
|
+
].join("\n"));
|
|
989
|
+
outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
|
|
779
990
|
const readiness = await materializeBackendTestExecutionReadiness({
|
|
780
991
|
runDir: meta.runDir,
|
|
781
992
|
workspaceRoot: input.cwd,
|
|
782
993
|
effective,
|
|
783
994
|
scenarioParamStatus: scenarioFacts.overallStatus,
|
|
784
995
|
scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
|
|
996
|
+
eligibility,
|
|
997
|
+
eligibilityInputHashes,
|
|
785
998
|
});
|
|
786
999
|
return {
|
|
787
1000
|
ok: true,
|
|
@@ -792,6 +1005,8 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
792
1005
|
collectedItemCount: effective.collectedItemCount,
|
|
793
1006
|
fixtureResolutionStatus: effective.fixtureResolutionStatus,
|
|
794
1007
|
executionReadinessStatus: readiness.status,
|
|
1008
|
+
eligibleItemCount: readiness.eligibleItemIds.length,
|
|
1009
|
+
excludedItemCount: readiness.excludedItems.length,
|
|
795
1010
|
factsPath: "contracts/backend-test-pytest-collection-effective.json",
|
|
796
1011
|
readinessPath: "contracts/backend-test-execution-readiness.json",
|
|
797
1012
|
reportPath: artifacts.reportPath,
|
|
@@ -826,6 +1041,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
826
1041
|
try {
|
|
827
1042
|
const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
|
|
828
1043
|
workspaceRoot: input.cwd,
|
|
1044
|
+
layout: backendLayout,
|
|
829
1045
|
taskId: sourceBinding.taskId,
|
|
830
1046
|
});
|
|
831
1047
|
const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
|
|
@@ -888,9 +1104,32 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
888
1104
|
else if (pipeline === "markdown-execute-html") {
|
|
889
1105
|
const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
|
|
890
1106
|
await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
|
|
891
|
-
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
892
|
-
|
|
893
|
-
|
|
1107
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
|
|
1108
|
+
let eligibleItemIds = executionReadiness.eligibleItemIds;
|
|
1109
|
+
// Plan D gap-fill executeScope=affected: when a gap plan exists in
|
|
1110
|
+
// this run, prefer the newly planned items (pytest node ids contain
|
|
1111
|
+
// the TP slot id) plus every collected item of the patched modules;
|
|
1112
|
+
// scope=all keeps the full eligible set.
|
|
1113
|
+
const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
|
|
1114
|
+
if (affectedScope) {
|
|
1115
|
+
try {
|
|
1116
|
+
const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
|
|
1117
|
+
const newPoints = new Set(plan.newTestPoints ?? []);
|
|
1118
|
+
const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
|
|
1119
|
+
const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
|
|
1120
|
+
[...modules].some((script) => id.includes(script)) ||
|
|
1121
|
+
[...newPoints].some((point) => id.includes(point)));
|
|
1122
|
+
if (affected.length > 0)
|
|
1123
|
+
eligibleItemIds = affected;
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// no gap plan in this run: execute the full eligible set
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
|
|
1130
|
+
const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
|
|
1131
|
+
await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
|
|
1132
|
+
const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
|
|
894
1133
|
// Split into short bash -c commands (aligned with markdown-environment).
|
|
895
1134
|
// A single ultra-long compound command has been observed on Windows to
|
|
896
1135
|
// exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
|
|
@@ -967,8 +1206,10 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
967
1206
|
'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
|
|
968
1207
|
'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
|
|
969
1208
|
'echo "STEP=pytest-run"',
|
|
970
|
-
`echo "- pytestCommand: python -m pytest
|
|
971
|
-
`
|
|
1209
|
+
`echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
|
|
1210
|
+
`echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
|
|
1211
|
+
`echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
|
|
1212
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
|
|
972
1213
|
"STATUS=$?",
|
|
973
1214
|
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
974
1215
|
'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
|
|
@@ -1053,11 +1294,13 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1053
1294
|
htmlContent: pytestHtmlContent,
|
|
1054
1295
|
pytestExitCode,
|
|
1055
1296
|
});
|
|
1056
|
-
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
1297
|
+
const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
|
|
1057
1298
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
1058
1299
|
const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
|
|
1059
1300
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
1060
|
-
const
|
|
1301
|
+
const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
|
|
1302
|
+
const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
|
|
1303
|
+
const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
|
|
1061
1304
|
const htmlContent = renderBackendTestHtml({
|
|
1062
1305
|
title: meta.spec.title,
|
|
1063
1306
|
parsed,
|
|
@@ -1245,6 +1488,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1245
1488
|
fromNodeId: "backend-test-execution-envelope",
|
|
1246
1489
|
artifactName: "backend-test-execution.json",
|
|
1247
1490
|
outputDir: "contracts",
|
|
1491
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
1248
1492
|
});
|
|
1249
1493
|
outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
|
|
1250
1494
|
}
|
|
@@ -1360,6 +1604,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1360
1604
|
artifactName: "backend-test-result-initial.json",
|
|
1361
1605
|
outputDir: "contracts",
|
|
1362
1606
|
junitRelativePath: "reports/backend-test-initial-junit.xml",
|
|
1607
|
+
testRoot: backendLayout.testRoot,
|
|
1363
1608
|
});
|
|
1364
1609
|
outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
|
|
1365
1610
|
}
|
|
@@ -2246,6 +2491,13 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2246
2491
|
}
|
|
2247
2492
|
if (shell?.jsonArtifactGate) {
|
|
2248
2493
|
const started = Date.now();
|
|
2494
|
+
const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
|
|
2495
|
+
? {
|
|
2496
|
+
testRoot: meta.spec.backendTestLayout.testRoot,
|
|
2497
|
+
markdownRel: meta.spec.backendTestLayout.markdownRel,
|
|
2498
|
+
scriptRel: meta.spec.backendTestLayout.scriptRel,
|
|
2499
|
+
}
|
|
2500
|
+
: undefined);
|
|
2249
2501
|
try {
|
|
2250
2502
|
const gate = shell.jsonArtifactGate;
|
|
2251
2503
|
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
@@ -2273,6 +2525,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2273
2525
|
fromNodeId: sourceNodeId,
|
|
2274
2526
|
artifactName: gate.artifactName,
|
|
2275
2527
|
outputDir: gate.outputDir,
|
|
2528
|
+
allowedTestRoot: backendLayout.testRoot,
|
|
2276
2529
|
});
|
|
2277
2530
|
break;
|
|
2278
2531
|
case "backend-test-result-v1":
|
|
@@ -2282,6 +2535,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2282
2535
|
artifactName: gate.artifactName,
|
|
2283
2536
|
outputDir: gate.outputDir,
|
|
2284
2537
|
junitRelativePath: gate.junitRelativePath,
|
|
2538
|
+
testRoot: backendLayout.testRoot,
|
|
2285
2539
|
});
|
|
2286
2540
|
break;
|
|
2287
2541
|
case "backend-test-classification-v1":
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertHarnessWriteAllowed, } from "./completed-facts-guard.js";
|
|
4
|
+
let atomicWriteSequence = 0;
|
|
4
5
|
async function writeAtomic(targetPath, content, options) {
|
|
5
6
|
assertHarnessWriteAllowed(targetPath, options);
|
|
6
7
|
const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
|
|
7
8
|
? path.resolve(options.repoRoot, targetPath)
|
|
8
9
|
: path.resolve(targetPath);
|
|
9
10
|
await mkdir(path.dirname(resolved), { recursive: true });
|
|
10
|
-
const tempPath = `${resolved}.${process.pid}.tmp`;
|
|
11
|
+
const tempPath = `${resolved}.${process.pid}.${(atomicWriteSequence += 1)}.tmp`;
|
|
11
12
|
try {
|
|
12
13
|
await writeFile(tempPath, content, "utf-8");
|
|
13
14
|
try {
|
|
@@ -14,6 +14,30 @@ export const OPENSPEC_SPEC_DIRS = [
|
|
|
14
14
|
];
|
|
15
15
|
/** Allowed spec file extensions (case-insensitive). */
|
|
16
16
|
export const OPENSPEC_SPEC_EXT_RE = /\.(md|mdx|json|yaml|yml)$/i;
|
|
17
|
+
/**
|
|
18
|
+
* Default governance subtree excluded from candidate discovery. Aligned with
|
|
19
|
+
* init.ts's DEFAULT_GOVERNANCE_ROOT; discovery derives the real root from
|
|
20
|
+
* harness.json.governanceRoot and falls back to this value.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_OPENSPEC_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
|
|
23
|
+
/**
|
|
24
|
+
* Explicit safety ceiling for auto-discovered openspec candidates (>=200).
|
|
25
|
+
* Reaching the cap is reported via DagSpec.advisories, never silently dropped.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_OPENSPEC_CANDIDATE_PATHS = 200;
|
|
28
|
+
/**
|
|
29
|
+
* Returns `true` when `filePath` is the governance subtree itself or any path
|
|
30
|
+
* under it. `filePath` must be a repo-relative POSIX path (backslashes are
|
|
31
|
+
* normalised internally). Comparison is case-sensitive, matching the other
|
|
32
|
+
* predicates in this module.
|
|
33
|
+
*/
|
|
34
|
+
export function isGovernanceSubtreePath(filePath, governanceRoot) {
|
|
35
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
36
|
+
const root = governanceRoot.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
37
|
+
if (!root)
|
|
38
|
+
return false;
|
|
39
|
+
return normalized === root || normalized.startsWith(root + "/");
|
|
40
|
+
}
|
|
17
41
|
/**
|
|
18
42
|
* Returns `true` when `filePath` is inside one of the canonical frontend spec
|
|
19
43
|
* directories. `filePath` must be a repo-relative POSIX path (backslashes are
|
|
@@ -47,3 +71,55 @@ export function isOpenspecSpecSearchTarget(value) {
|
|
|
47
71
|
const normalized = value.replaceAll("\\", "/");
|
|
48
72
|
return /(?:^|[^A-Za-z0-9_.-])(?:openspec\/(?:schemas|project-specs)|ai_workspace)(?:\/|$)/.test(normalized);
|
|
49
73
|
}
|
|
74
|
+
/** Normalizes a free-form path token to a repo-relative POSIX path or null. */
|
|
75
|
+
function normalizeCitationToken(raw) {
|
|
76
|
+
let token = raw.trim();
|
|
77
|
+
token = token.replace(/^[<"'`(\[\s]+|[>"'`)\]\s]+$/g, "");
|
|
78
|
+
// Strip trailing punctuation that is not part of a filename.
|
|
79
|
+
token = token.replace(/[),.;:!?]+$/g, "");
|
|
80
|
+
const hashIndex = token.indexOf("#");
|
|
81
|
+
if (hashIndex >= 0)
|
|
82
|
+
token = token.slice(0, hashIndex);
|
|
83
|
+
token = token.replace(/\\/g, "/");
|
|
84
|
+
if (!token)
|
|
85
|
+
return null;
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Deterministically extracts repo-relative openspec spec paths explicitly cited
|
|
90
|
+
* in task-source markdown (requirement + execution constraint). Pure function,
|
|
91
|
+
* no IO.
|
|
92
|
+
*
|
|
93
|
+
* Scanned sources, in order:
|
|
94
|
+
* - backtick inline code spans (`path`)
|
|
95
|
+
* - markdown link targets ([text](path))
|
|
96
|
+
* - bare path tokens under openspec/ or ai_workspace/
|
|
97
|
+
*
|
|
98
|
+
* Every token is normalized to POSIX, stripped of a trailing `#fragment`,
|
|
99
|
+
* filtered through `isOpenspecSpecFilePath`, and excluded when it falls inside
|
|
100
|
+
* the governance subtree (matching `listOpenspecDiscovery`'s exclusion口径).
|
|
101
|
+
* Results are deduped and sorted.
|
|
102
|
+
*/
|
|
103
|
+
export function extractTaskSourceOpenspecPaths(markdown, governanceRoot = DEFAULT_OPENSPEC_GOVERNANCE_ROOT) {
|
|
104
|
+
if (!markdown)
|
|
105
|
+
return [];
|
|
106
|
+
const raw = new Set();
|
|
107
|
+
const collect = (token) => {
|
|
108
|
+
const normalized = normalizeCitationToken(token);
|
|
109
|
+
if (normalized)
|
|
110
|
+
raw.add(normalized);
|
|
111
|
+
};
|
|
112
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
|
|
113
|
+
collect(match[1] ?? "");
|
|
114
|
+
}
|
|
115
|
+
for (const match of markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
|
|
116
|
+
collect(match[1] ?? "");
|
|
117
|
+
}
|
|
118
|
+
for (const match of markdown.matchAll(/(?:^|[\s"'([>])((?:openspec|ai_workspace)\/[A-Za-z0-9._/-]+)/g)) {
|
|
119
|
+
collect(match[1] ?? "");
|
|
120
|
+
}
|
|
121
|
+
return [...raw]
|
|
122
|
+
.filter((token) => isOpenspecSpecFilePath(token) &&
|
|
123
|
+
!isGovernanceSubtreePath(token, governanceRoot))
|
|
124
|
+
.sort();
|
|
125
|
+
}
|