@tea-agent/loop-agent 0.38.1 → 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 +109 -13
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +2 -14
- package/dist/executors/dag-pi-executor.js +41 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/governance/manifest-types.js +0 -2
- package/dist/shared/operator/capabilities.js +87 -22
- package/dist/task/config-types.js +22 -0
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/pi-runtime.js +10 -5
- package/dist/worker/console/chat/routes.js +458 -47
- package/dist/worker/console/chat/semantic-activity.js +13 -13
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/harness-pi-model-matrix.js +2 -11
- package/dist/worker/console/operator-actions.js +5 -4
- package/dist/worker/console/pi-readiness.js +1 -0
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.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-DvBN4JZc.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.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-CUZgsiRC.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.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-DzqWXNO_.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.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-CcfkA-Dq.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-DH8FjnOK.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.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 +0 -12
- 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 +38 -1
- 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 +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -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 +2 -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/frontend-implementation-contract.js +236 -8
- package/dist/workflows/dag/frontend-prewrite-gate.js +95 -35
- package/dist/workflows/dag/init-hybrid.js +652 -87
- package/dist/workflows/dag/node-execution.js +45 -18
- package/dist/workflows/dag/prompt.js +45 -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 +18 -0
- package/dist/workflows/dag/types.js +98 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/harness.schema.json +0 -8
- package/harness.json +1 -8
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Plan C: deterministic query/filter axis scenario partitions.
|
|
4
|
+
*
|
|
5
|
+
* A README `## Scenario Partitions` table declares one partition per
|
|
6
|
+
* filterable enum/classification axis on an affected list/query operation.
|
|
7
|
+
* The deterministic expander turns each declared partition into machine
|
|
8
|
+
* slots — the model never invents slot IDs:
|
|
9
|
+
*
|
|
10
|
+
* each-value one slot per legal domain value (intent=nominal-filter)
|
|
11
|
+
* omitted only when the contract marks the parameter optional
|
|
12
|
+
* not-in-set exactly one complement slot (intent=enum-invalid)
|
|
13
|
+
*
|
|
14
|
+
* `not-in-set` expected semantics must come from the bound source; when the
|
|
15
|
+
* source does not document the complement expectation, the partition row
|
|
16
|
+
* keeps the slot but the expected column stays GAP so N6 reports it instead
|
|
17
|
+
* of guessing "empty list" or "400".
|
|
18
|
+
*/
|
|
19
|
+
export const scenarioPartitionRowSchema = z.object({
|
|
20
|
+
partitionId: z.string().min(1),
|
|
21
|
+
operation: z.string().min(1),
|
|
22
|
+
axis: z.string().min(1),
|
|
23
|
+
/** Legal values copied verbatim from the bound OpenAPI enum / requirement sentence. */
|
|
24
|
+
domain: z.array(z.string().min(1)).min(1),
|
|
25
|
+
/** true when the parameter is optional and the omitted slot is required. */
|
|
26
|
+
optional: z.boolean(),
|
|
27
|
+
/**
|
|
28
|
+
* Complement expectation sourced from the contract. `unknown` keeps the
|
|
29
|
+
* not-in-set slot but marks expected as GAP (never guessed).
|
|
30
|
+
*/
|
|
31
|
+
complementExpected: z.enum(["empty-result", "excluded-result", "unknown"]),
|
|
32
|
+
/** Which bind rule this partition enforces (advisory provenance). */
|
|
33
|
+
bindRule: z.string().min(1),
|
|
34
|
+
}).strict();
|
|
35
|
+
const PARTITION_HEADING = /^##\s+Scenario Partitions\s*$/;
|
|
36
|
+
const PARTITION_TABLE_HEADER = /^\|\s*Partition ID\s*\|\s*Operation\s*\|\s*Axis\s*\|\s*Domain\s*\|\s*Required Slots\s*\|\s*Expected by Slot\s*\|\s*Bind Rule\s*\|\s*$/;
|
|
37
|
+
const TABLE_ROW = /^\|(.+)\|$/;
|
|
38
|
+
function splitCells(raw) {
|
|
39
|
+
return raw
|
|
40
|
+
.split("|")
|
|
41
|
+
.map((cell) => cell.trim())
|
|
42
|
+
.filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse the `## Scenario Partitions` section of the README into declared
|
|
46
|
+
* partitions. Returns { partitions, issues }; issues are fail-closed parse
|
|
47
|
+
* problems (malformed table), while semantic gaps surface in expansion.
|
|
48
|
+
*/
|
|
49
|
+
export function parseScenarioPartitions(readmeMarkdown) {
|
|
50
|
+
const issues = [];
|
|
51
|
+
const partitions = [];
|
|
52
|
+
const lines = readmeMarkdown.split("\n");
|
|
53
|
+
const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line));
|
|
54
|
+
if (headingIndex === -1)
|
|
55
|
+
return { partitions, issues };
|
|
56
|
+
// find table header within the section (before the next ## heading)
|
|
57
|
+
let headerIndex = -1;
|
|
58
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
59
|
+
if (/^##\s+/.test(lines[i]) && !PARTITION_HEADING.test(lines[i]))
|
|
60
|
+
break;
|
|
61
|
+
if (PARTITION_TABLE_HEADER.test(lines[i])) {
|
|
62
|
+
headerIndex = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (headerIndex === -1) {
|
|
67
|
+
issues.push("MISSING_PARTITION_TABLE: Scenario Partitions section has no canonical header row");
|
|
68
|
+
return { partitions, issues };
|
|
69
|
+
}
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (let i = headerIndex + 2; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
if (!TABLE_ROW.test(line)) {
|
|
74
|
+
if (/^##\s+/.test(line) || /^#\s+/.test(line))
|
|
75
|
+
break;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const cells = splitCells(line);
|
|
79
|
+
if (cells.length !== 7) {
|
|
80
|
+
issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [partitionId, operation, axis, domainRaw, requiredSlotsRaw, expectedBySlotRaw, bindRule,] = cells;
|
|
84
|
+
const domain = domainRaw
|
|
85
|
+
.split(/[;,,;]/)
|
|
86
|
+
.map((value) => value.trim())
|
|
87
|
+
.filter((value) => value.length > 0);
|
|
88
|
+
if (domain.length === 0) {
|
|
89
|
+
issues.push(`MISSING_PARTITION: empty domain: ${partitionId}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const optional = /omit/i.test(requiredSlotsRaw);
|
|
93
|
+
const complementExpected = /empty[- ]?result/i.test(expectedBySlotRaw)
|
|
94
|
+
? "empty-result"
|
|
95
|
+
: /excluded[- ]?result/i.test(expectedBySlotRaw)
|
|
96
|
+
? "excluded-result"
|
|
97
|
+
: "unknown";
|
|
98
|
+
if (seen.has(partitionId)) {
|
|
99
|
+
issues.push(`MISSING_PARTITION: duplicate partition id ${partitionId}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
seen.add(partitionId);
|
|
103
|
+
partitions.push({
|
|
104
|
+
partitionId,
|
|
105
|
+
operation,
|
|
106
|
+
axis,
|
|
107
|
+
domain,
|
|
108
|
+
optional,
|
|
109
|
+
complementExpected,
|
|
110
|
+
bindRule,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { partitions, issues };
|
|
114
|
+
}
|
|
115
|
+
function slotToken(value) {
|
|
116
|
+
return value
|
|
117
|
+
.trim()
|
|
118
|
+
.toUpperCase()
|
|
119
|
+
.replace(/[^A-Z0-9]+/g, "-")
|
|
120
|
+
.replace(/^-+|-+$/g, "");
|
|
121
|
+
}
|
|
122
|
+
const STABLE_PARTITION_ID = /^SP-[A-Z0-9][A-Z0-9._-]*$/i;
|
|
123
|
+
/**
|
|
124
|
+
* Slot IDs are `TP-<Partition ID>-<VALUE|OMITTED|NOT-IN-SET>`.
|
|
125
|
+
* The declared Partition ID is the single source of truth so model-written
|
|
126
|
+
* `TP-SP-GET-API-...` IDs match the expander (never drop the HTTP method).
|
|
127
|
+
*/
|
|
128
|
+
function partitionToken(row) {
|
|
129
|
+
const id = row.partitionId.trim();
|
|
130
|
+
if (STABLE_PARTITION_ID.test(id))
|
|
131
|
+
return `TP-${id.toUpperCase()}`;
|
|
132
|
+
return `TP-SP-${slotToken(row.operation)}-${slotToken(row.axis)}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Deterministically expand declared partitions into slots. Pure function:
|
|
136
|
+
* same input rows always produce the same slot IDs.
|
|
137
|
+
*/
|
|
138
|
+
export function expandScenarioPartitions(partitions) {
|
|
139
|
+
const slots = [];
|
|
140
|
+
for (const row of partitions) {
|
|
141
|
+
const prefix = partitionToken(row);
|
|
142
|
+
for (const value of row.domain) {
|
|
143
|
+
slots.push({
|
|
144
|
+
partitionId: row.partitionId,
|
|
145
|
+
operation: row.operation,
|
|
146
|
+
axis: row.axis,
|
|
147
|
+
slotId: `${prefix}-${slotToken(value)}`,
|
|
148
|
+
kind: "each-value",
|
|
149
|
+
value,
|
|
150
|
+
intent: "nominal-filter",
|
|
151
|
+
expected: "domain-value",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (row.optional) {
|
|
155
|
+
slots.push({
|
|
156
|
+
partitionId: row.partitionId,
|
|
157
|
+
operation: row.operation,
|
|
158
|
+
axis: row.axis,
|
|
159
|
+
slotId: `${prefix}-OMITTED`,
|
|
160
|
+
kind: "omitted",
|
|
161
|
+
value: "",
|
|
162
|
+
intent: "omitted",
|
|
163
|
+
expected: "default-behavior",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
slots.push({
|
|
167
|
+
partitionId: row.partitionId,
|
|
168
|
+
operation: row.operation,
|
|
169
|
+
axis: row.axis,
|
|
170
|
+
slotId: `${prefix}-NOT-IN-SET`,
|
|
171
|
+
kind: "not-in-set",
|
|
172
|
+
value: "",
|
|
173
|
+
intent: "enum-invalid",
|
|
174
|
+
expected: row.complementExpected === "unknown" ? "GAP" : "complement",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return slots;
|
|
178
|
+
}
|
|
179
|
+
/** Facts projection consumed by N6 and plan D gap-fill. */
|
|
180
|
+
export const scenarioPartitionFactsSchema = z.object({
|
|
181
|
+
partitions: z.array(z.object({
|
|
182
|
+
partitionId: z.string(),
|
|
183
|
+
operation: z.string(),
|
|
184
|
+
axis: z.string(),
|
|
185
|
+
domain: z.array(z.string()),
|
|
186
|
+
optional: z.boolean(),
|
|
187
|
+
complementExpected: z.string(),
|
|
188
|
+
bindRule: z.string(),
|
|
189
|
+
slotIds: z.array(z.string()),
|
|
190
|
+
})),
|
|
191
|
+
slots: z.array(z.object({
|
|
192
|
+
slotId: z.string(),
|
|
193
|
+
partitionId: z.string(),
|
|
194
|
+
kind: z.string(),
|
|
195
|
+
intent: z.string(),
|
|
196
|
+
expected: z.string(),
|
|
197
|
+
})),
|
|
198
|
+
missingSlots: z.array(z.string()),
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
201
|
+
* Compare expanded slots against the Case test-point universe. A slot is
|
|
202
|
+
* covered only when its exact slot ID is declared in some Case's 测试点 or
|
|
203
|
+
* binding lists. Missing each-value/omitted slots and the complement slot
|
|
204
|
+
* produce fail-closed finding codes; a GAP-expected complement that IS
|
|
205
|
+
* covered still reports a coverage gap (expected semantics undocumented).
|
|
206
|
+
*/
|
|
207
|
+
export function assessScenarioPartitionCoverage(partitions, declaredTestPointIds) {
|
|
208
|
+
const slots = expandScenarioPartitions(partitions);
|
|
209
|
+
const findings = [];
|
|
210
|
+
const missingSlots = [];
|
|
211
|
+
for (const row of partitions) {
|
|
212
|
+
const rowSlots = slots.filter((slot) => slot.partitionId === row.partitionId);
|
|
213
|
+
for (const slot of rowSlots) {
|
|
214
|
+
if (!declaredTestPointIds.has(slot.slotId)) {
|
|
215
|
+
missingSlots.push(slot.slotId);
|
|
216
|
+
const code = slot.kind === "not-in-set" ? "MISSING_COMPLEMENT_SLOT" : "MISSING_PARTITION_SLOT";
|
|
217
|
+
findings.push(`${code}: partition ${row.partitionId} slot ${slot.slotId} (${slot.kind}${slot.value ? `=${slot.value}` : ""}) is not covered by any Case test point`);
|
|
218
|
+
}
|
|
219
|
+
else if (slot.kind === "not-in-set" && slot.expected === "GAP") {
|
|
220
|
+
findings.push(`PARTITION_COMPLEMENT_EXPECTED_GAP: partition ${row.partitionId} complement slot ${slot.slotId} has no documented expected semantics in the bound source; expected stays GAP instead of guessing`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
facts: {
|
|
226
|
+
partitions: partitions.map((row) => ({
|
|
227
|
+
partitionId: row.partitionId,
|
|
228
|
+
operation: row.operation,
|
|
229
|
+
axis: row.axis,
|
|
230
|
+
domain: row.domain,
|
|
231
|
+
optional: row.optional,
|
|
232
|
+
complementExpected: row.complementExpected,
|
|
233
|
+
bindRule: row.bindRule,
|
|
234
|
+
slotIds: slots
|
|
235
|
+
.filter((slot) => slot.partitionId === row.partitionId)
|
|
236
|
+
.map((slot) => slot.slotId),
|
|
237
|
+
})),
|
|
238
|
+
slots: slots.map((slot) => ({
|
|
239
|
+
slotId: slot.slotId,
|
|
240
|
+
partitionId: slot.partitionId,
|
|
241
|
+
kind: slot.kind,
|
|
242
|
+
intent: slot.intent,
|
|
243
|
+
expected: slot.expected,
|
|
244
|
+
})),
|
|
245
|
+
missingSlots,
|
|
246
|
+
},
|
|
247
|
+
findings,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
|
|
5
|
+
import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
|
|
6
|
+
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
6
7
|
/**
|
|
7
8
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
8
9
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -103,6 +104,8 @@ function moduleStemInvalidReason(raw) {
|
|
|
103
104
|
const stem = normalizeBackendTestModuleStem(raw);
|
|
104
105
|
if (isPriorityOnlyBackendTestModuleStem(stem))
|
|
105
106
|
return "priority-only-module-stem";
|
|
107
|
+
if (isOpaqueHashBackendTestModuleStem(stem))
|
|
108
|
+
return "opaque-hash-module-stem";
|
|
106
109
|
if (stem.toLowerCase() === "readme")
|
|
107
110
|
return "reserved-module-stem";
|
|
108
111
|
if (!/^[a-z][a-z0-9_]*$/.test(stem))
|
|
@@ -114,28 +117,40 @@ function moduleStemInvalidReason(raw) {
|
|
|
114
117
|
function looksLikeValidModuleStem(raw) {
|
|
115
118
|
return moduleStemInvalidReason(raw) === undefined;
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const candidates = [];
|
|
120
|
-
const tableRowLines = readme
|
|
120
|
+
function extractCanonicalModuleIndexSection(readme) {
|
|
121
|
+
const lines = readme
|
|
121
122
|
.replaceAll("\r\n", "\n")
|
|
122
123
|
.replaceAll("\r", "\n")
|
|
123
|
-
.split("\n")
|
|
124
|
-
|
|
124
|
+
.split("\n");
|
|
125
|
+
const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
|
|
126
|
+
if (headings.length === 0) {
|
|
127
|
+
return { section: "", structureIssues: ["missing-module-index"] };
|
|
128
|
+
}
|
|
129
|
+
if (headings.length > 1) {
|
|
130
|
+
return { section: "", structureIssues: ["duplicate-module-index"] };
|
|
131
|
+
}
|
|
132
|
+
const start = headings[0] + 1;
|
|
133
|
+
const relativeEnd = lines.slice(start).findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
134
|
+
const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
|
|
135
|
+
return { section: lines.slice(start, end).join("\n"), structureIssues: [] };
|
|
136
|
+
}
|
|
137
|
+
/** Inspect only candidates inside the unique exact `## Module Index` section. */
|
|
138
|
+
export function inspectModuleStemsFromReadme(readme) {
|
|
139
|
+
const extracted = extractCanonicalModuleIndexSection(readme);
|
|
140
|
+
const candidates = [];
|
|
141
|
+
const tableRowLines = extracted.section.split("\n").filter((line) => line.includes("|"));
|
|
125
142
|
for (const line of tableRowLines) {
|
|
126
143
|
for (const match of line.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
|
|
127
144
|
candidates.push(match[1]);
|
|
128
145
|
}
|
|
129
146
|
for (const match of line.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (looksLikeValidModuleStem(match[1]) || isPriorityOnlyBackendTestModuleStem(match[1])) {
|
|
134
|
-
candidates.push(match[1]);
|
|
147
|
+
const raw = match[1];
|
|
148
|
+
if (looksLikeValidModuleStem(raw) || moduleStemInvalidReason(raw) !== "invalid-syntax") {
|
|
149
|
+
candidates.push(raw);
|
|
135
150
|
}
|
|
136
151
|
}
|
|
137
152
|
}
|
|
138
|
-
for (const match of
|
|
153
|
+
for (const match of extracted.section.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
|
|
139
154
|
candidates.push(match[1]);
|
|
140
155
|
}
|
|
141
156
|
const uniqueCandidates = orderedUnique(candidates);
|
|
@@ -145,12 +160,18 @@ export function inspectModuleStemsFromReadme(readme) {
|
|
|
145
160
|
? [{ raw, normalized: normalizeBackendTestModuleStem(raw), reasonCode }]
|
|
146
161
|
: [];
|
|
147
162
|
});
|
|
163
|
+
const validStems = orderedUnique(uniqueCandidates
|
|
164
|
+
.filter(looksLikeValidModuleStem)
|
|
165
|
+
.map((stem) => normalizeBackendTestModuleStem(stem)));
|
|
166
|
+
const structureIssues = [...extracted.structureIssues];
|
|
167
|
+
if (validStems.length > BACKEND_TEST_MAX_MODULE_COUNT) {
|
|
168
|
+
structureIssues.push(`excessive-module-count: ${validStems.length} > ${BACKEND_TEST_MAX_MODULE_COUNT}`);
|
|
169
|
+
}
|
|
148
170
|
return {
|
|
149
171
|
candidates: uniqueCandidates,
|
|
150
|
-
validStems
|
|
151
|
-
.filter(looksLikeValidModuleStem)
|
|
152
|
-
.map((stem) => normalizeBackendTestModuleStem(stem))),
|
|
172
|
+
validStems,
|
|
153
173
|
invalid,
|
|
174
|
+
structureIssues,
|
|
154
175
|
};
|
|
155
176
|
}
|
|
156
177
|
/**
|
|
@@ -161,8 +182,9 @@ export function inspectModuleStemsFromReadme(readme) {
|
|
|
161
182
|
export function extractModuleStemsFromReadme(readme) {
|
|
162
183
|
return inspectModuleStemsFromReadme(readme).validStems;
|
|
163
184
|
}
|
|
164
|
-
async function listMarkdownModules(workspaceRoot) {
|
|
165
|
-
const
|
|
185
|
+
async function listMarkdownModules(workspaceRoot, layout) {
|
|
186
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
187
|
+
const root = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
|
|
166
188
|
if (!(await exists(root)))
|
|
167
189
|
return [];
|
|
168
190
|
const entries = await readdir(root, { withFileTypes: true });
|
|
@@ -170,7 +192,7 @@ async function listMarkdownModules(workspaceRoot) {
|
|
|
170
192
|
.filter((entry) => entry.isFile() &&
|
|
171
193
|
entry.name.toLowerCase().endsWith(".md") &&
|
|
172
194
|
entry.name.toLowerCase() !== "readme.md")
|
|
173
|
-
.map((entry) => path.posix.join(
|
|
195
|
+
.map((entry) => path.posix.join(resolved.markdownDir, entry.name.replaceAll("\\", "/"))));
|
|
174
196
|
}
|
|
175
197
|
/**
|
|
176
198
|
* Recursively list `.py` file paths relative to `dirRoot` (forward slashes).
|
|
@@ -550,56 +572,58 @@ export async function assessBackendTestShardChildCompleteness(input) {
|
|
|
550
572
|
issues,
|
|
551
573
|
};
|
|
552
574
|
}
|
|
553
|
-
export async function assessBackendTestMdWriterCompleteness(workspaceRoot) {
|
|
575
|
+
export async function assessBackendTestMdWriterCompleteness(workspaceRoot, layout) {
|
|
576
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
577
|
+
const readmeRel = resolved.readmePath;
|
|
554
578
|
const issues = [];
|
|
555
|
-
const expectedPaths = [
|
|
579
|
+
const expectedPaths = [readmeRel];
|
|
556
580
|
const actualPaths = [];
|
|
557
581
|
const missingPaths = [];
|
|
558
582
|
const brokenPaths = [];
|
|
559
|
-
const readmePath = path.join(workspaceRoot, "
|
|
583
|
+
const readmePath = path.join(workspaceRoot, ...readmeRel.split("/"));
|
|
560
584
|
let readme = "";
|
|
561
585
|
if (!(await exists(readmePath))) {
|
|
562
|
-
missingPaths.push(
|
|
586
|
+
missingPaths.push(readmeRel);
|
|
563
587
|
issues.push({
|
|
564
588
|
code: "T3",
|
|
565
|
-
path:
|
|
589
|
+
path: readmeRel,
|
|
566
590
|
detail: "README.md is missing",
|
|
567
591
|
recoverable: true,
|
|
568
592
|
});
|
|
569
593
|
}
|
|
570
594
|
else {
|
|
571
|
-
actualPaths.push(
|
|
595
|
+
actualPaths.push(readmeRel);
|
|
572
596
|
readme = await readFile(readmePath, "utf8");
|
|
573
597
|
if (!readme.includes("## Coverage Scope")) {
|
|
574
|
-
brokenPaths.push(
|
|
598
|
+
brokenPaths.push(readmeRel);
|
|
575
599
|
issues.push({
|
|
576
600
|
code: "T5",
|
|
577
|
-
path:
|
|
601
|
+
path: readmeRel,
|
|
578
602
|
detail: "README.md is missing ## Coverage Scope",
|
|
579
603
|
recoverable: true,
|
|
580
604
|
});
|
|
581
605
|
}
|
|
582
606
|
if (!readme.includes("## Coverage Matrix") ||
|
|
583
607
|
!hasMarkdownTable(readme, "Rule Key")) {
|
|
584
|
-
if (!brokenPaths.includes(
|
|
585
|
-
brokenPaths.push(
|
|
608
|
+
if (!brokenPaths.includes(readmeRel)) {
|
|
609
|
+
brokenPaths.push(readmeRel);
|
|
586
610
|
}
|
|
587
611
|
issues.push({
|
|
588
612
|
code: "T5",
|
|
589
|
-
path:
|
|
613
|
+
path: readmeRel,
|
|
590
614
|
detail: "README.md Coverage Matrix table is missing or broken",
|
|
591
615
|
recoverable: true,
|
|
592
616
|
});
|
|
593
617
|
}
|
|
594
618
|
}
|
|
595
619
|
const indexedStems = extractModuleStemsFromReadme(readme);
|
|
596
|
-
const existingModules = await listMarkdownModules(workspaceRoot);
|
|
620
|
+
const existingModules = await listMarkdownModules(workspaceRoot, resolved);
|
|
597
621
|
const existingByStem = new Map(existingModules.map((rel) => [
|
|
598
622
|
normalizeBackendTestModuleStem(path.posix.basename(rel)),
|
|
599
623
|
rel,
|
|
600
624
|
]));
|
|
601
625
|
for (const stem of indexedStems) {
|
|
602
|
-
const rel =
|
|
626
|
+
const rel = `${resolved.markdownDir}/${stem}.md`;
|
|
603
627
|
expectedPaths.push(rel);
|
|
604
628
|
const existing = existingByStem.get(stem);
|
|
605
629
|
if (!existing) {
|
|
@@ -668,15 +692,16 @@ export async function assessBackendTestMdWriterCompleteness(workspaceRoot) {
|
|
|
668
692
|
issues,
|
|
669
693
|
};
|
|
670
694
|
}
|
|
671
|
-
export async function assessBackendTestPytestWriterCompleteness(workspaceRoot) {
|
|
695
|
+
export async function assessBackendTestPytestWriterCompleteness(workspaceRoot, layout) {
|
|
696
|
+
const resolved = layout ?? resolveBackendTestLayout(undefined);
|
|
672
697
|
const issues = [];
|
|
673
698
|
const expectedPaths = [];
|
|
674
699
|
const actualPaths = [];
|
|
675
700
|
const missingPaths = [];
|
|
676
701
|
const brokenPaths = [];
|
|
677
|
-
const modules = await listMarkdownModules(workspaceRoot);
|
|
702
|
+
const modules = await listMarkdownModules(workspaceRoot, resolved);
|
|
678
703
|
for (const moduleRel of modules) {
|
|
679
|
-
const expected = expectedBackendTestPytestScriptForMarkdownModule(path.posix.basename(moduleRel));
|
|
704
|
+
const expected = expectedBackendTestPytestScriptForMarkdownModule(path.posix.basename(moduleRel), resolved);
|
|
680
705
|
expectedPaths.push(expected);
|
|
681
706
|
const absolute = path.join(workspaceRoot, expected);
|
|
682
707
|
if (!(await exists(absolute))) {
|
|
@@ -836,6 +861,11 @@ export function isBackendTestCompletenessRetryCandidate(task) {
|
|
|
836
861
|
task.id === "generate-backend-pytest-pi" ||
|
|
837
862
|
task.id === "generate-backend-md-plan-pi" ||
|
|
838
863
|
task.id === "generate-backend-pytest-plan-pi" ||
|
|
864
|
+
// Plan D gap-fill patch writers share the same Completeness Gate retry
|
|
865
|
+
// contract as full-chain writers.
|
|
866
|
+
task.id === "patch-backend-readme-matrix-pi" ||
|
|
867
|
+
(task.id?.startsWith("patch-backend-md-case-") ?? false) ||
|
|
868
|
+
(task.id?.startsWith("patch-backend-pytest-case-") ?? false) ||
|
|
839
869
|
(task.id?.startsWith("generate-backend-md-case-") ?? false) ||
|
|
840
870
|
(task.id?.startsWith("generate-backend-pytest-case-") ?? false));
|
|
841
871
|
}
|
|
@@ -872,42 +902,44 @@ export function isBackendTestPytestPlanTask(task) {
|
|
|
872
902
|
* index), WITHOUT requiring the indexed module files to exist yet — those are
|
|
873
903
|
* written by the downstream map_agent shard children.
|
|
874
904
|
*/
|
|
875
|
-
export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
|
|
905
|
+
export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
|
|
906
|
+
const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
|
|
907
|
+
const planReadmeRel = resolvedPlan.readmePath;
|
|
876
908
|
const issues = [];
|
|
877
|
-
const expectedPaths = [
|
|
909
|
+
const expectedPaths = [planReadmeRel];
|
|
878
910
|
const actualPaths = [];
|
|
879
911
|
const missingPaths = [];
|
|
880
912
|
const brokenPaths = [];
|
|
881
|
-
const readmePath = path.join(workspaceRoot, "
|
|
913
|
+
const readmePath = path.join(workspaceRoot, ...planReadmeRel.split("/"));
|
|
882
914
|
if (!(await exists(readmePath))) {
|
|
883
|
-
missingPaths.push(
|
|
915
|
+
missingPaths.push(planReadmeRel);
|
|
884
916
|
issues.push({
|
|
885
917
|
code: "T3",
|
|
886
|
-
path:
|
|
918
|
+
path: planReadmeRel,
|
|
887
919
|
detail: "README.md is missing",
|
|
888
920
|
recoverable: true,
|
|
889
921
|
});
|
|
890
922
|
}
|
|
891
923
|
else {
|
|
892
|
-
actualPaths.push(
|
|
924
|
+
actualPaths.push(planReadmeRel);
|
|
893
925
|
const readme = await readFile(readmePath, "utf8");
|
|
894
926
|
if (!readme.includes("## Coverage Scope")) {
|
|
895
|
-
brokenPaths.push(
|
|
927
|
+
brokenPaths.push(planReadmeRel);
|
|
896
928
|
issues.push({
|
|
897
929
|
code: "T5",
|
|
898
|
-
path:
|
|
930
|
+
path: planReadmeRel,
|
|
899
931
|
detail: "README.md is missing ## Coverage Scope",
|
|
900
932
|
recoverable: true,
|
|
901
933
|
});
|
|
902
934
|
}
|
|
903
935
|
if (!readme.includes("## Coverage Matrix") ||
|
|
904
936
|
!hasMarkdownTable(readme, "Rule Key")) {
|
|
905
|
-
if (!brokenPaths.includes(
|
|
906
|
-
brokenPaths.push(
|
|
937
|
+
if (!brokenPaths.includes(planReadmeRel)) {
|
|
938
|
+
brokenPaths.push(planReadmeRel);
|
|
907
939
|
}
|
|
908
940
|
issues.push({
|
|
909
941
|
code: "T5",
|
|
910
|
-
path:
|
|
942
|
+
path: planReadmeRel,
|
|
911
943
|
detail: "README.md Coverage Matrix table is missing or broken",
|
|
912
944
|
recoverable: true,
|
|
913
945
|
});
|
|
@@ -917,24 +949,35 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
|
|
|
917
949
|
// are produced by map_agent children and are NOT required here.
|
|
918
950
|
const stemInspection = inspectModuleStemsFromReadme(readme);
|
|
919
951
|
const indexedStems = stemInspection.validStems;
|
|
952
|
+
for (const structureIssue of stemInspection.structureIssues) {
|
|
953
|
+
if (!brokenPaths.includes(planReadmeRel)) {
|
|
954
|
+
brokenPaths.push(planReadmeRel);
|
|
955
|
+
}
|
|
956
|
+
issues.push({
|
|
957
|
+
code: "T5",
|
|
958
|
+
path: planReadmeRel,
|
|
959
|
+
detail: structureIssue,
|
|
960
|
+
recoverable: true,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
920
963
|
for (const invalid of stemInspection.invalid) {
|
|
921
|
-
if (!brokenPaths.includes(
|
|
922
|
-
brokenPaths.push(
|
|
964
|
+
if (!brokenPaths.includes(planReadmeRel)) {
|
|
965
|
+
brokenPaths.push(planReadmeRel);
|
|
923
966
|
}
|
|
924
967
|
issues.push({
|
|
925
968
|
code: "T5",
|
|
926
|
-
path:
|
|
969
|
+
path: planReadmeRel,
|
|
927
970
|
detail: `${invalid.reasonCode}: ${invalid.normalized}; use a stable lowercase business resource/domain stem`,
|
|
928
971
|
recoverable: true,
|
|
929
972
|
});
|
|
930
973
|
}
|
|
931
974
|
if (indexedStems.length === 0) {
|
|
932
|
-
if (!brokenPaths.includes(
|
|
933
|
-
brokenPaths.push(
|
|
975
|
+
if (!brokenPaths.includes(planReadmeRel)) {
|
|
976
|
+
brokenPaths.push(planReadmeRel);
|
|
934
977
|
}
|
|
935
978
|
issues.push({
|
|
936
979
|
code: "T5",
|
|
937
|
-
path:
|
|
980
|
+
path: planReadmeRel,
|
|
938
981
|
detail: "README.md module index is empty; downstream map_agent has no stems to shard",
|
|
939
982
|
recoverable: true,
|
|
940
983
|
});
|
|
@@ -960,16 +1003,17 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
|
|
|
960
1003
|
* requiring per-module pytest scripts to exist yet — those are written by the
|
|
961
1004
|
* downstream map_agent shard children.
|
|
962
1005
|
*/
|
|
963
|
-
export async function assessBackendTestPytestPlanCompleteness(workspaceRoot) {
|
|
1006
|
+
export async function assessBackendTestPytestPlanCompleteness(workspaceRoot, layout) {
|
|
1007
|
+
const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
|
|
964
1008
|
const issues = [];
|
|
965
1009
|
const expectedPaths = [];
|
|
966
1010
|
const actualPaths = [];
|
|
967
1011
|
const missingPaths = [];
|
|
968
1012
|
const brokenPaths = [];
|
|
969
|
-
// Discover shared pytest resources under
|
|
970
|
-
//
|
|
1013
|
+
// Discover shared pytest resources under <testRoot>/**/helpers/** and
|
|
1014
|
+
// <testRoot>/**/factories/** (the plan node writeSet globs).
|
|
971
1015
|
const candidates = [];
|
|
972
|
-
for (const dir of [
|
|
1016
|
+
for (const dir of [`${resolvedPlan.testRoot}/helpers`, `${resolvedPlan.testRoot}/factories`]) {
|
|
973
1017
|
const absolute = path.join(workspaceRoot, dir);
|
|
974
1018
|
if (await exists(absolute)) {
|
|
975
1019
|
for (const file of await listPythonFiles(absolute)) {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* its schemaId in the contract output registry. Imported once by the node
|
|
4
4
|
* executor; keeps per-contract imports out of the executor.
|
|
5
5
|
*/
|
|
6
|
-
import { validateFrontendContractNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
|
|
6
|
+
import { validateFrontendContractNodeOutput, validateFrontendPlanPatchNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
|
|
7
7
|
import { registerStructuredContractValidator } from "./contract-output-registry.js";
|
|
8
8
|
registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
|
|
9
|
+
registerStructuredContractValidator("frontend-implementation-contract-plan-patch-v1", validateFrontendPlanPatchNodeOutput);
|
|
9
10
|
registerStructuredContractValidator("frontend-implementation-contract-revision-patch-v1", validateFrontendRevisionPatchNodeOutput);
|