@tea-agent/loop-agent 0.42.0-next.4 → 0.42.0-next.6
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 +24 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/worker/console/chat/pi-runtime.js +26 -0
- package/dist/worker/console/chat/routes.js +60 -5
- package/dist/worker/console/chat/sdd-data-alignment.js +219 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +26 -0
- package/dist/worker/console/chat/shortcuts.js +18 -2
- package/dist/worker/console/server.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CeznJ4Iw.js → abnfDiagram-N423BO3Z-BJpsGgAv.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4u9u7SX.js → arc-DjMFPsEd.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-oghpT81p.js → architectureDiagram-T3A2C74G-DqdZlr1O.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BPJ5Kvcj.js → blockDiagram-VBNYF7ZC-BmZnfFEO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CoIu69pd.js → c4Diagram-5PPSVZJV-3cKberpL.js} +1 -1
- package/dist/worker/console/static/assets/channel-DWYwciiv.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DtgiAsSc.js → chunk-2GRJ4B5K-q-0yjQ1h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-YSu_EVSD.js → chunk-2Q5K7J3B-DnONBaDW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DaRO-NB6.js → chunk-5RXB4S5H-v3a6PCOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Dh0ZKiGA.js → chunk-5VM5RSS4-D1kLrlU_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DOrvbR-H.js → chunk-6Q2QTUOP-BPelzihi.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3b22LN2.js → chunk-GF5L2VYU-DauNE69H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CXlqO8l6.js → chunk-JWPE2WC7-RIg5_bFJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BblTBrDu.js → chunk-KBJHAD2P-BuILuoxs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DswqPALe.js → chunk-RYQCIY6F-VHORV5oW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CfTwVOlc.js → chunk-XXDRQBXY-BJYr4xHC.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-bLD8jVFo.js → cose-bilkent-JH36ORCC-BS6nuqQn.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvoG1fh5.js → cynefin-VYW2F7L2-BBhF0NAT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DEPAdJu3.js → cynefinDiagram-MW4NZA55-D1135Ifg.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmrNvBLy.js → dagre-VZM6K2ZE-vAOYPUWJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdwzIwvs.js → diagram-7IWD3JNH-DuaSPkfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CJ3hj-md.js → diagram-B4RE2ZJO-mMvXvGU6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C6YJT2a4.js → diagram-LBJQPF4R-CDIEJtlL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C_LPfjhX.js → diagram-Q27KOJAE-Bi_6JvCW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-xvsSb5Ie.js → diagram-UB23O5K3-DAqj57yN.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Rxp2tKcD.js → ebnfDiagram-BXEA7PRR-DfWaxNEo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DRyLYmZh.js → erDiagram-JOGREHBK-Bt7QqkNG.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-TLSU_D2B.js → flowDiagram-UKHOOZJN-CRRhQwT2.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGqeTvHC.js → ganttDiagram-PKOTCBZU-DAV1O5B6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BZxyH2wh.js → gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js} +1 -1
- package/dist/worker/console/static/assets/index-CVdddQsA.js +451 -0
- package/dist/worker/console/static/assets/index-DDQc5a50.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BsYtzx01.js → infoDiagram-6WML65LV-C5W5eILC.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js → ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DYbB3ibo.js → journeyDiagram-NVQOT4AX-CKDRcaKf.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BHJHewZO.js → kanban-definition-27J2QSJJ-Cc7_TuSb.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CmPIPmCG.js → linear-BcQYYmeT.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-F--C1uV0.js → mermaid.core-CwLZPtQo.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-YTXfL1st.js → mindmap-definition-FAOFIHXS-D29M025z.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DdDMYXP9.js → pegDiagram-VL7TDLO6-CV-ZAWYe.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C3nH4KVd.js → pieDiagram-7S7Q4E2Y-C6iPS_o5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js → quadrantDiagram-CIZ2JOQS-BFCD1sTg.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-KNzbl-7w.js → railroadDiagram-AXF67PYL-CztNj4O9.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CSwI6dG0.js → requirementDiagram-LRYGKXZP-BWIELSNd.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bl1AyuXy.js → sankeyDiagram-W5VNT64P-CPtxrjJQ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2y4EmXQa.js → sequenceDiagram-SI44F4Z6-BEWVSFsu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-E8v8YrMg.js → sizeCapture-X5ZJPWSS-DyhzoX_J.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eGGlR856.js → stateDiagram-OKZ733FA-xLox0nu7.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-B7g7xc07.js → swimlanes-SLNWSIFB-CLC_ow9A.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DR3VCHBG.js → timeline-definition-Z64GVDOM-DmJZ4uyY.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LUvapZIy.js → vennDiagram-T6HMQDX7-DD7GGi9Z.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C7GDcaXq.js → wardleyDiagram-T6FBY63Y-Bi8PQoGt.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CbNAqoGK.js → xychartDiagram-ELKLHX3M-uKdAY8Dp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +225 -21
- package/dist/worker/console/static-src/operator-chat/useComposer.js +5 -1
- package/dist/worker/console/workspace-context.js +4 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +58 -15
- package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +16 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +42 -6
- package/dist/workflows/dag/init-hybrid.js +62 -22
- package/dist/workflows/dag/prompt.js +36 -2
- package/docs/templates/backend-test-dag.json +5 -5
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +0 -451
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +0 -8
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic backend-test plan protocol helpers.
|
|
3
|
+
*
|
|
4
|
+
* English canonical `##` headings remain the parse authority. Known Chinese
|
|
5
|
+
* aliases are rewritten only as an exact whole-line `##` heading, never as
|
|
6
|
+
* prose. Partition slot coverage follows Scheme A: Case count may be smaller
|
|
7
|
+
* than the enum count, but every exact `TP-SP-...` slot must appear as an
|
|
8
|
+
* independent variant Test Point.
|
|
9
|
+
*/
|
|
10
|
+
export const BACKEND_TEST_PLAN_HEADING_ALIASES = {
|
|
11
|
+
"模块索引": "Module Index",
|
|
12
|
+
"覆盖范围": "Coverage Scope",
|
|
13
|
+
"覆盖矩阵": "Coverage Matrix",
|
|
14
|
+
"场景分区": "Scenario Partitions",
|
|
15
|
+
};
|
|
16
|
+
export const BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID = "backend-test-scenario-partition-slots-v1";
|
|
17
|
+
const CANONICAL_HEADING_LINE = /^##\s+(Module Index|Coverage Scope|Coverage Matrix|Scenario Partitions)\s*$/;
|
|
18
|
+
const ALIAS_HEADING_LINE = /^##\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\s*$/;
|
|
19
|
+
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{2,3})\b/;
|
|
20
|
+
const SECTION_HEADING = /^###\s+/;
|
|
21
|
+
const TEST_POINT_RE = /\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
|
|
22
|
+
const VARIANT_LABEL = /变体测试点|Variant Test Points/i;
|
|
23
|
+
const FORBIDDEN_SLOT_ALIASES = new Set(["SINGLE", "MULTIPLE"]);
|
|
24
|
+
export function canonicalizeBackendTestPlanProtocolHeadings(markdown) {
|
|
25
|
+
const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
26
|
+
const rewrittenHeadings = [];
|
|
27
|
+
const next = lines.map((line) => {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
const alias = ALIAS_HEADING_LINE.exec(trimmed);
|
|
30
|
+
if (!alias)
|
|
31
|
+
return line;
|
|
32
|
+
const canonical = BACKEND_TEST_PLAN_HEADING_ALIASES[alias[1]];
|
|
33
|
+
rewrittenHeadings.push(canonical);
|
|
34
|
+
return `## ${canonical}`;
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
markdown: next.join("\n"),
|
|
38
|
+
changed: rewrittenHeadings.length > 0,
|
|
39
|
+
rewrittenHeadings,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function countCanonicalPlanHeading(markdown, heading) {
|
|
43
|
+
const canonical = canonicalizeBackendTestPlanProtocolHeadings(markdown).markdown;
|
|
44
|
+
return canonical
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter((line) => line.trim() === `## ${heading}`).length;
|
|
47
|
+
}
|
|
48
|
+
function orderedUnique(values) {
|
|
49
|
+
return [...new Set(values.filter(Boolean))];
|
|
50
|
+
}
|
|
51
|
+
function collectSectionTokens(body, headingNeedle) {
|
|
52
|
+
const lines = body.split("\n");
|
|
53
|
+
const start = lines.findIndex((line) => line.trim() === `### ${headingNeedle}`);
|
|
54
|
+
if (start < 0)
|
|
55
|
+
return [];
|
|
56
|
+
const tokens = [];
|
|
57
|
+
for (const line of lines.slice(start + 1)) {
|
|
58
|
+
if (SECTION_HEADING.test(line) || CASE_HEADING.test(line.trim()))
|
|
59
|
+
break;
|
|
60
|
+
tokens.push(...(line.match(TEST_POINT_RE) ?? []));
|
|
61
|
+
}
|
|
62
|
+
return tokens;
|
|
63
|
+
}
|
|
64
|
+
function collectLabeledTokens(body, label) {
|
|
65
|
+
return body
|
|
66
|
+
.split("\n")
|
|
67
|
+
.filter((line) => label.test(line))
|
|
68
|
+
.flatMap((line) => line.match(TEST_POINT_RE) ?? []);
|
|
69
|
+
}
|
|
70
|
+
export function collectMarkdownPartitionSlotCoverage(markdown, requiredVariantSlots) {
|
|
71
|
+
const declaredTestPoints = [];
|
|
72
|
+
const variantTestPoints = [];
|
|
73
|
+
const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
74
|
+
const headingIndexes = lines.flatMap((line, index) => CASE_HEADING.test(line.trim()) ? [index] : []);
|
|
75
|
+
for (let i = 0; i < headingIndexes.length; i += 1) {
|
|
76
|
+
const start = headingIndexes[i];
|
|
77
|
+
const end = headingIndexes[i + 1] ?? lines.length;
|
|
78
|
+
const body = lines.slice(start, end).join("\n");
|
|
79
|
+
declaredTestPoints.push(...collectSectionTokens(body, "测试点"));
|
|
80
|
+
variantTestPoints.push(...collectLabeledTokens(body, VARIANT_LABEL));
|
|
81
|
+
}
|
|
82
|
+
const declaredSet = new Set(orderedUnique(declaredTestPoints));
|
|
83
|
+
const variantSet = new Set(orderedUnique(variantTestPoints));
|
|
84
|
+
const required = requiredVariantSlots.filter((slotId) => !FORBIDDEN_SLOT_ALIASES.has(slotId));
|
|
85
|
+
return {
|
|
86
|
+
declaredTestPoints: orderedUnique(declaredTestPoints),
|
|
87
|
+
variantTestPoints: orderedUnique(variantTestPoints),
|
|
88
|
+
missingFromDeclared: required.filter((slotId) => !declaredSet.has(slotId)),
|
|
89
|
+
missingFromVariant: required.filter((slotId) => !variantSet.has(slotId)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function isForbiddenPartitionSlotAlias(token) {
|
|
93
|
+
return FORBIDDEN_SLOT_ALIASES.has(token.trim().toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
export function lookupRequiredVariantSlotsForMarkdownPath(inventory, markdownPath) {
|
|
96
|
+
if (!inventory)
|
|
97
|
+
return [];
|
|
98
|
+
const stem = markdownPath
|
|
99
|
+
.replace(/\\/g, "/")
|
|
100
|
+
.replace(/^.*\//, "")
|
|
101
|
+
.replace(/\.md$/i, "");
|
|
102
|
+
return inventory.modules.find((item) => item.stem === stem)?.requiredVariantSlots ?? [];
|
|
103
|
+
}
|
|
104
|
+
export function isCanonicalPlanHeadingLine(line) {
|
|
105
|
+
return CANONICAL_HEADING_LINE.test(line.trim());
|
|
106
|
+
}
|
|
@@ -1116,7 +1116,11 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
1116
1116
|
? "UNDETERMINED"
|
|
1117
1117
|
: "MISMATCH";
|
|
1118
1118
|
}
|
|
1119
|
-
|
|
1119
|
+
const comparableObserved = observed.kind === "boolean"
|
|
1120
|
+
? (observed.literal === "True" ? "true" : observed.literal === "False" ? "false" : literal)
|
|
1121
|
+
: literal;
|
|
1122
|
+
const comparableExpected = expected === "True" ? "true" : expected === "False" ? "false" : expected;
|
|
1123
|
+
return comparableObserved === comparableExpected
|
|
1120
1124
|
? "MATCH"
|
|
1121
1125
|
: observed.kind === "unknown"
|
|
1122
1126
|
? "UNDETERMINED"
|
|
@@ -1207,6 +1211,12 @@ function boundedScenarioText(value, logicalLength) {
|
|
|
1207
1211
|
const suffix = `<truncated length=${logicalLength ?? value.length}>`;
|
|
1208
1212
|
return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
|
|
1209
1213
|
}
|
|
1214
|
+
/** Facts schema forbids negative lengths. Internal helper sentinels stay in-memory only. */
|
|
1215
|
+
function persistedObservedLength(length) {
|
|
1216
|
+
return typeof length === "number" && Number.isFinite(length) && length >= 0
|
|
1217
|
+
? Math.trunc(length)
|
|
1218
|
+
: undefined;
|
|
1219
|
+
}
|
|
1210
1220
|
function localPythonFunctionRegion(source, functionName) {
|
|
1211
1221
|
const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1212
1222
|
const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
|
|
@@ -1449,11 +1459,12 @@ function scenarioParamDiagnosticValues(intent, observed) {
|
|
|
1449
1459
|
const expectedRaw = intent.startsWith("custom-literal:")
|
|
1450
1460
|
? intent.slice("custom-literal:".length)
|
|
1451
1461
|
: intent === "unknown" ? undefined : intent;
|
|
1462
|
+
const persistedLength = persistedObservedLength(observed.length);
|
|
1452
1463
|
return {
|
|
1453
1464
|
expectedRaw,
|
|
1454
|
-
observedRaw: boundedScenarioText(observed.text,
|
|
1465
|
+
observedRaw: boundedScenarioText(observed.text, persistedLength),
|
|
1455
1466
|
expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
|
|
1456
|
-
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal),
|
|
1467
|
+
observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), persistedLength),
|
|
1457
1468
|
};
|
|
1458
1469
|
}
|
|
1459
1470
|
export async function assessBackendScenarioParamConsistency(input) {
|
|
@@ -1539,8 +1550,8 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
1539
1550
|
tpId,
|
|
1540
1551
|
field: inferred.field,
|
|
1541
1552
|
intent: inferred.intent,
|
|
1542
|
-
observed: boundedScenarioText(observed.text, observed.length),
|
|
1543
|
-
observedLength: observed.length,
|
|
1553
|
+
observed: boundedScenarioText(observed.text, persistedObservedLength(observed.length)),
|
|
1554
|
+
observedLength: persistedObservedLength(observed.length),
|
|
1544
1555
|
status,
|
|
1545
1556
|
repairability,
|
|
1546
1557
|
reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
|
|
6
6
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
7
7
|
import { canonicalizeBackendTestMarkdownTestPointBindings, inspectBackendTestMarkdownTestPointBindings, } from "./backend-test-case-coverage-analysis.js";
|
|
8
|
+
import { BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID, canonicalizeBackendTestPlanProtocolHeadings, collectMarkdownPartitionSlotCoverage, lookupRequiredVariantSlotsForMarkdownPath, } from "./backend-test-plan-protocol.js";
|
|
8
9
|
/**
|
|
9
10
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
10
11
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -127,10 +128,8 @@ function looksLikeValidModuleStem(raw) {
|
|
|
127
128
|
return moduleStemInvalidReason(raw) === undefined;
|
|
128
129
|
}
|
|
129
130
|
function extractCanonicalModuleIndexSection(readme) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
.replaceAll("\r", "\n")
|
|
133
|
-
.split("\n");
|
|
131
|
+
const canonical = canonicalizeBackendTestPlanProtocolHeadings(readme).markdown;
|
|
132
|
+
const lines = canonical.split("\n");
|
|
134
133
|
const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
|
|
135
134
|
if (headings.length === 0) {
|
|
136
135
|
return { section: "", structureIssues: ["missing-module-index"] };
|
|
@@ -519,6 +518,19 @@ export async function loadBackendTestWriterProgressForRetry(runDir, taskId) {
|
|
|
519
518
|
* child's own target file(s): md children are checked for structural
|
|
520
519
|
* completeness, pytest children for parseability. Sibling modules are ignored.
|
|
521
520
|
*/
|
|
521
|
+
async function loadScenarioPartitionSlotInventory(runDir) {
|
|
522
|
+
if (!runDir)
|
|
523
|
+
return undefined;
|
|
524
|
+
try {
|
|
525
|
+
const raw = JSON.parse(await readFile(path.join(runDir, "contracts", "backend-test-scenario-partition-slots.json"), "utf8"));
|
|
526
|
+
if (raw?.schemaId !== BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID)
|
|
527
|
+
return undefined;
|
|
528
|
+
return raw;
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
522
534
|
export async function assessBackendTestShardChildCompleteness(input) {
|
|
523
535
|
const role = input.role;
|
|
524
536
|
const targetPaths = input.writeSet
|
|
@@ -531,6 +543,9 @@ export async function assessBackendTestShardChildCompleteness(input) {
|
|
|
531
543
|
let canonicalizationOutcome = role === "md-generate" ? "already-canonical" : undefined;
|
|
532
544
|
let canonicalizedTestPointCount = 0;
|
|
533
545
|
let lightweightRepairEligible = false;
|
|
546
|
+
const slotInventory = role === "md-generate"
|
|
547
|
+
? await loadScenarioPartitionSlotInventory(input.runDir)
|
|
548
|
+
: undefined;
|
|
534
549
|
for (const target of targetPaths) {
|
|
535
550
|
// Rendered map-child writeSet entries are concrete (e.g.
|
|
536
551
|
// testcase/md/health.md / testcase/test_orders.py); glob-only entries are
|
|
@@ -565,6 +580,27 @@ export async function assessBackendTestShardChildCompleteness(input) {
|
|
|
565
580
|
.filter((entry) => entry.unclassifiedTestPoints.length > 0 || entry.duplicateBindingTestPoints.length > 0)
|
|
566
581
|
.map((entry) => entry.caseId);
|
|
567
582
|
const structural = moduleStructurallyComplete(body);
|
|
583
|
+
const requiredSlots = lookupRequiredVariantSlotsForMarkdownPath(slotInventory, target);
|
|
584
|
+
const slotCoverage = requiredSlots.length > 0
|
|
585
|
+
? collectMarkdownPartitionSlotCoverage(body, requiredSlots)
|
|
586
|
+
: undefined;
|
|
587
|
+
const missingSlots = orderedUnique([
|
|
588
|
+
...(slotCoverage?.missingFromDeclared ?? []),
|
|
589
|
+
...(slotCoverage?.missingFromVariant ?? []),
|
|
590
|
+
]);
|
|
591
|
+
if (missingSlots.length > 0) {
|
|
592
|
+
lightweightRepairEligible = true;
|
|
593
|
+
if (!brokenPaths.includes(target))
|
|
594
|
+
brokenPaths.push(target);
|
|
595
|
+
issues.push({
|
|
596
|
+
code: "T5",
|
|
597
|
+
path: target,
|
|
598
|
+
detail: `incomplete-partition-slot-set: missing ${missingSlots.join(", ")}; Scheme A requires each exact slot as an independent variant Test Point, not Case count or SINGLE/MULTIPLE aliases`,
|
|
599
|
+
recoverable: true,
|
|
600
|
+
affectedCases: afterBindings.map((entry) => entry.caseId),
|
|
601
|
+
testPointIds: missingSlots,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
568
604
|
if (!structural.ok) {
|
|
569
605
|
lightweightRepairEligible = bindingCases.length > 0;
|
|
570
606
|
if (bindingCases.length > 0)
|
|
@@ -651,7 +687,7 @@ export async function assessBackendTestMdWriterCompleteness(workspaceRoot, layou
|
|
|
651
687
|
}
|
|
652
688
|
else {
|
|
653
689
|
actualPaths.push(readmeRel);
|
|
654
|
-
readme = await readFile(readmePath, "utf8");
|
|
690
|
+
readme = canonicalizeBackendTestPlanProtocolHeadings(await readFile(readmePath, "utf8")).markdown;
|
|
655
691
|
if (!readme.includes("## Coverage Scope")) {
|
|
656
692
|
brokenPaths.push(readmeRel);
|
|
657
693
|
issues.push({
|
|
@@ -985,7 +1021,7 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout)
|
|
|
985
1021
|
}
|
|
986
1022
|
else {
|
|
987
1023
|
actualPaths.push(planReadmeRel);
|
|
988
|
-
const readme = await readFile(readmePath, "utf8");
|
|
1024
|
+
const readme = canonicalizeBackendTestPlanProtocolHeadings(await readFile(readmePath, "utf8")).markdown;
|
|
989
1025
|
if (!readme.includes("## Coverage Scope")) {
|
|
990
1026
|
brokenPaths.push(planReadmeRel);
|
|
991
1027
|
issues.push({
|
|
@@ -4243,7 +4243,11 @@ const stripBackticks=s=>s.split(bt).join('');
|
|
|
4243
4243
|
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(/^[a-f][a-f0-9]{6,63}$/.test(st))return 'opaque-hash-module-stem';if(st==='readme')return 'reserved-module-stem';if(!/^[a-z][a-z0-9_]*$/.test(st))return 'invalid-syntax';if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return 'case-like-module-stem';return null;};
|
|
4244
4244
|
const valid=raw=>invalidReason(raw)===null;
|
|
4245
4245
|
const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
|
|
4246
|
+
const headingAlias={'\u6a21\u5757\u7d22\u5f15':'Module Index','\u8986\u76d6\u8303\u56f4':'Coverage Scope','\u8986\u76d6\u77e9\u9635':'Coverage Matrix','\u573a\u666f\u5206\u533a':'Scenario Partitions'};
|
|
4246
4247
|
const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
|
|
4248
|
+
let headingRepair=false;
|
|
4249
|
+
for(let i=0;i<allLines.length;i++){const alias=/^##\\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\\s*$/.exec(allLines[i].trim());if(alias){allLines[i]='## '+headingAlias[alias[1]];headingRepair=true;}}
|
|
4250
|
+
if(headingRepair)readme=allLines.join('\\n');
|
|
4247
4251
|
const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
|
|
4248
4252
|
if(headings.length!==1){process.stderr.write((headings.length===0?'missing-module-index':'duplicate-module-index')+'; require exactly one exact ## Module Index section\\n');process.exit(2);}
|
|
4249
4253
|
const start=headings[0]+1;let end=allLines.length;for(let i=start;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){end=i;break;}}
|
|
@@ -4256,21 +4260,26 @@ const headerIndex=lines.findIndex(line=>{const row=cells(line);return expectedHe
|
|
|
4256
4260
|
if(headerIndex<0){process.stderr.write('invalid-module-index-header: require exact business ownership and path columns\\n');process.exit(2);}
|
|
4257
4261
|
const dataRows=lines.slice(headerIndex+2).map(cells).filter(row=>row.length>=8&&row[0]&&row[0]!=='Module Stem');
|
|
4258
4262
|
const allowedSplit=new Set(['explicit-user-layout','primary-business-resource','independent-business-resource','output-budget']);
|
|
4259
|
-
const
|
|
4260
|
-
const operationOwners=new Map();const declaredModules=[];let planRepairApplied=false;
|
|
4263
|
+
const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=headingRepair;
|
|
4261
4264
|
for(const row of dataRows){
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4265
|
+
const rawStem=String(row[0]||'').trim(),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
|
|
4266
|
+
const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];
|
|
4267
|
+
if(mdMatches.length!==1){process.stderr.write('module-markdown-path-mismatch: '+(rawStem||'unknown')+'; require exactly one canonical Markdown Path\\n');process.exit(2);}
|
|
4268
|
+
let stem=norm(mdMatches[0][1]),markdownPath=mdMatches[0][0];
|
|
4269
|
+
if(strictLayout&&!strictLayout.modules.some(item=>item.markdownPath===markdownPath)){planRepairApplied=true;continue;}
|
|
4264
4270
|
if(!resource){process.stderr.write('missing-business-resource: '+stem+'\\n');process.exit(2);}
|
|
4265
4271
|
if(/^(?:response|resp|regression|positive|negative|boundary|error|combo|filter)(?:[_ -]|$)/i.test(resource)){process.stderr.write('test-purpose-business-resource: '+stem+' -> '+resource+'\\n');process.exit(2);}
|
|
4266
4272
|
if(!allowedSplit.has(split)){process.stderr.write('invalid-module-split-reason: '+stem+' -> '+split+'\\n');process.exit(2);}
|
|
4267
4273
|
if(operations.length===0){process.stderr.write('missing-owned-operation: '+stem+'\\n');process.exit(2);}
|
|
4268
|
-
const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];let markdownPath=mdMatches[0]&&mdMatches[0][0];
|
|
4269
4274
|
let pytestPath=String(row[7]||'').replace(/^\\.\\//,'').replace(/\\\\/g,'/');
|
|
4270
|
-
if(mdMatches.length!==1||norm(mdMatches[0][1])!==stem){process.stderr.write('module-markdown-path-mismatch: '+stem+'\\n');process.exit(2);}
|
|
4271
4275
|
if(!/^[A-Za-z0-9_./-]+\\.py$/.test(pytestPath)||pytestPath.includes('..')){process.stderr.write('invalid-module-pytest-path: '+stem+' -> '+pytestPath+'\\n');process.exit(2);}
|
|
4272
|
-
const
|
|
4276
|
+
const requiredCandidates=strictLayout?strictLayout.modules.filter(item=>item.markdownPath===markdownPath):[];
|
|
4277
|
+
if(strictLayout&&requiredCandidates.length!==1){process.stderr.write('MODULE_LAYOUT_CONFLICT: no unique required module for Markdown Path '+markdownPath+'\\n');process.exit(2);}
|
|
4278
|
+
const required=requiredCandidates[0];
|
|
4279
|
+
if(!required&&norm(rawStem)!==stem)planRepairApplied=true;
|
|
4273
4280
|
if(required){
|
|
4281
|
+
stem=required.stem;
|
|
4282
|
+
if(norm(rawStem)!==stem)planRepairApplied=true;
|
|
4274
4283
|
if(!required.markdownPath.startsWith(mdDir+'/')||!required.pytestPath.startsWith(scriptDir+'/')||required.markdownPath.includes('..')||required.pytestPath.includes('..')){process.stderr.write('MODULE_LAYOUT_CONFLICT: strict paths outside frozen roots for '+stem+'\\n');process.exit(2);}
|
|
4275
4284
|
if(markdownPath!==required.markdownPath||pytestPath!==required.pytestPath||resource!==required.businessResource||JSON.stringify(operations)!==JSON.stringify(required.ownedOperations)||split!==required.splitReason){planRepairApplied=true;}
|
|
4276
4285
|
markdownPath=required.markdownPath;pytestPath=required.pytestPath;
|
|
@@ -4280,17 +4289,21 @@ for(const row of dataRows){
|
|
|
4280
4289
|
}
|
|
4281
4290
|
}
|
|
4282
4291
|
const item={stem,markdownPath,pytestPath,businessResource:required?required.businessResource:resource,ownedOperations:required?required.ownedOperations:operations,ownedRuleKeys:String(row[3]||'').split(';').map(value=>value.trim()).filter(Boolean),caseIds:String(row[4]||'').split(';').map(value=>value.trim()).filter(Boolean),splitReason:required?required.splitReason:split,...(required&&required.budgetProof?{budgetProof:required.budgetProof}:{})};
|
|
4292
|
+
if(canonicalSeen.has(item.stem)){process.stderr.write('duplicate-canonical-module-stem: '+item.stem+'; module identity must be unique\\n');process.exit(2);}
|
|
4293
|
+
canonicalSeen.add(item.stem);
|
|
4283
4294
|
declaredModules.push(item);
|
|
4284
4295
|
for(const operation of item.ownedOperations){const owners=operationOwners.get(operation)||[];owners.push({stem,split:item.splitReason});operationOwners.set(operation,owners);}
|
|
4285
4296
|
}
|
|
4286
4297
|
if(strictLayout){
|
|
4287
4298
|
const missing=strictLayout.modules.filter(item=>!declaredModules.some(actual=>actual.stem===item.stem));
|
|
4288
4299
|
if(missing.length){process.stderr.write('MODULE_LAYOUT_CONFLICT: plan missing required modules '+missing.map(item=>item.stem).join(',')+'; deterministic Plan-only repair cannot invent Rule/Case ownership\\n');process.exit(2);}
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4300
|
+
}
|
|
4301
|
+
if(planRepairApplied){
|
|
4302
|
+
const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
|
|
4303
|
+
readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
|
|
4304
|
+
fs.writeFileSync(planPath,readme,'utf8');
|
|
4305
|
+
} else if(headingRepair){
|
|
4306
|
+
fs.writeFileSync(planPath,readme,'utf8');
|
|
4294
4307
|
}
|
|
4295
4308
|
for(const [operation,owners] of operationOwners){if(owners.length>1&&!owners.every(owner=>owner.split==='explicit-user-layout'||owner.split==='output-budget')){process.stderr.write('overlapping-operation-modules: '+operation+' -> '+owners.map(owner=>owner.stem).join(',')+'; merge by business resource or use an authoritative explicit-user-layout\\n');process.exit(2);}}
|
|
4296
4309
|
if(!strictLayout){
|
|
@@ -4302,13 +4315,40 @@ if(!strictLayout){
|
|
|
4302
4315
|
}
|
|
4303
4316
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
4304
4317
|
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
4305
|
-
const
|
|
4306
|
-
for(const item of declaredModules){if(valid(item.stem)
|
|
4318
|
+
const modules=[];
|
|
4319
|
+
for(const item of declaredModules){if(valid(item.stem))modules.push(item);}
|
|
4307
4320
|
if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
|
|
4308
4321
|
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
4309
4322
|
const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
|
|
4310
4323
|
const planSha256=crypto.createHash('sha256').update(readme).digest('hex');
|
|
4311
|
-
|
|
4324
|
+
const slotTok=v=>String(v).trim().toUpperCase().replace(/[^A-Z0-9]+/g,'-').replace(/^-+|-+$/g,'');
|
|
4325
|
+
const partHead=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Scenario Partitions')partHead.push(i);}
|
|
4326
|
+
if(partHead.length>1){process.stderr.write('duplicate-scenario-partitions; require at most one exact ## Scenario Partitions section\\n');process.exit(2);}
|
|
4327
|
+
const slotByModule=new Map(modules.map(m=>[m.stem,[]]));const unassigned=[];
|
|
4328
|
+
if(partHead.length===1){
|
|
4329
|
+
const pStart=partHead[0]+1;let pEnd=allLines.length;for(let i=pStart;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){pEnd=i;break;}}
|
|
4330
|
+
const pLines=allLines.slice(pStart,pEnd).filter(l=>l.includes('|'));
|
|
4331
|
+
const pCells=line=>line.split('|').slice(1,-1).map(v=>stripBackticks(v).trim());
|
|
4332
|
+
const pHeader=['Partition ID','Operation','Axis','Domain','Required Slots','Expected by Slot','Bind Rule'];
|
|
4333
|
+
const pH=pLines.findIndex(line=>pHeader.every((v,i)=>pCells(line)[i]===v));
|
|
4334
|
+
if(pH<0){process.stderr.write('MISSING_PARTITION_TABLE: Scenario Partitions section has no canonical header row\\n');process.exit(2);}
|
|
4335
|
+
const pRows=pLines.slice(pH+2).map(pCells).filter(r=>r.length>=7&&r[0]&&r[0]!=='Partition ID');
|
|
4336
|
+
for(const row of pRows){
|
|
4337
|
+
const pid=String(row[0]||'').trim(),op=String(row[1]||'').trim(),domain=String(row[3]||'').split(/[;,,;]/).map(v=>v.trim()).filter(Boolean),optional=/omit/i.test(String(row[4]||'')),bind=String(row[6]||'').trim();
|
|
4338
|
+
const prefix=/^SP-[A-Z0-9][A-Z0-9._-]*$/i.test(pid)?('TP-'+pid.toUpperCase()):('TP-SP-'+slotTok(op)+'-'+slotTok(String(row[2]||'')));
|
|
4339
|
+
const slotIds=[...domain.map(v=>prefix+'-'+slotTok(v)),...(optional?[prefix+'-OMITTED']:[]),prefix+'-NOT-IN-SET'];
|
|
4340
|
+
const owners=modules.filter(m=>(m.ownedRuleKeys||[]).includes(bind)||(m.ownedOperations||[]).some(o=>String(o).replace(/\\s+/g,' ').toUpperCase()===op.replace(/\\s+/g,' ').toUpperCase()));
|
|
4341
|
+
const uniqueOwners=[...new Set(owners.map(o=>o.stem))];
|
|
4342
|
+
if(uniqueOwners.length===1){for(const id of slotIds)slotByModule.get(uniqueOwners[0]).push(id);}
|
|
4343
|
+
else if(modules.length===1){for(const id of slotIds)slotByModule.get(modules[0].stem).push(id);}
|
|
4344
|
+
else unassigned.push(...slotIds);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
if(unassigned.length){process.stderr.write('unassigned-partition-slots: '+unassigned.join(',')+'\\n');process.exit(2);}
|
|
4348
|
+
const slotInventory={schemaId:'backend-test-scenario-partition-slots-v1',planSha256,modules:modules.map(m=>({stem:m.stem,requiredVariantSlots:[...new Set(slotByModule.get(m.stem)||[])]})),unassignedSlotIds:[]};
|
|
4349
|
+
fs.mkdirSync(path.join(runDir,'contracts'),{recursive:true});
|
|
4350
|
+
fs.writeFileSync(path.join(runDir,'contracts','backend-test-scenario-partition-slots.json'),JSON.stringify(slotInventory,null,2));
|
|
4351
|
+
process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath,requiredVariantSlots:(slotInventory.modules.find(m=>m.stem===item.stem)||{requiredVariantSlots:[]}).requiredVariantSlots})),planReadPath,planSha256,moduleLayout:{schemaId:'backend-test-module-layout-facts-v1',source:strictLayout?'task-contract':'plan-derived',contractSha256:strictLayoutSha256,mode:strictLayout&&strictLayout.mode||'business-resource-layout',planRepairAttemptCount:planRepairApplied?1:0,planRepairOutcome:planRepairApplied?'changed':'not-required'},partitionSlots:slotInventory}));
|
|
4312
4352
|
`;
|
|
4313
4353
|
const encoded = deflateRawSync(Buffer.from(script, "utf8")).toString("base64");
|
|
4314
4354
|
return `node -e "eval(require('zlib').inflateRawSync(Buffer.from('${encoded}','base64')).toString('utf8'))"`;
|
|
@@ -4889,9 +4929,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4889
4929
|
subtask_prompt: [
|
|
4890
4930
|
"This is a required plan-generation node. Read only the strict read set and return the complete Markdown plan in the assistant response. The runtime persists the response as a run-owned Harness artifact named generate-backend-md-plan-pi/plan.md. Do not write testcase/md/README.md or any project file; module case cards are written by downstream sharded nodes.",
|
|
4891
4931
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
4892
|
-
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact ## Coverage Scope, ## Coverage Matrix and ## Module Index
|
|
4932
|
+
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact English protocol headings ## Coverage Scope, ## Coverage Matrix, ## Scenario Partitions when applicable, and ## Module Index. Never translate those headings into 覆盖范围/覆盖矩阵/场景分区/模块索引.",
|
|
4893
4933
|
"Read the upstream environment report only through the strict read set. Generate the Markdown-first backend test plan; it will be persisted under the current DAG run's Harness artifacts, not under testcase/md/.",
|
|
4894
|
-
"Write human-readable content in Simplified Chinese by default. Keep English
|
|
4934
|
+
"Write human-readable content in Simplified Chinese by default. Keep English protocol literals exact: section headings, table headers, Partition IDs, TP IDs, Case IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and source citations. Never translate ## Module Index into ## 模块索引.",
|
|
4895
4935
|
"Create the concise plan entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in the plan artifact.",
|
|
4896
4936
|
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in the plan artifact using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.",
|
|
4897
4937
|
"Coverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.",
|
|
@@ -4899,7 +4939,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4899
4939
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
4900
4940
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
4901
4941
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
4902
|
-
"Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden.",
|
|
4942
|
+
"Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden. The Module Stem cell must contain only the plain filename stem; never put Markdown link syntax or a path in that cell.",
|
|
4903
4943
|
...(taskConfig.backendTest?.moduleLayout
|
|
4904
4944
|
? [
|
|
4905
4945
|
"A strict task-contract `backendTest.moduleLayout` is bound and is authoritative over model-derived layout. Reproduce every stem, businessResource, ownedOperations, splitReason, markdownPath and pytestPath exactly; do not add, omit, rename or reorder physical modules. The downstream preflight compares exact path sets and may perform at most one deterministic Plan-only pruning/path normalization; it cannot invent missing Rule/Case ownership.",
|
|
@@ -4907,7 +4947,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4907
4947
|
]
|
|
4908
4948
|
: []),
|
|
4909
4949
|
"Include exactly one `## Module Index` table with this exact header: `| Module Stem | Business Resource | Owned Operations | Owned Rule Keys | Case IDs | Split Reason | Markdown Path | Pytest Path |`. Use canonical relative links `[label](./<stem>.md)` inside the Markdown Path cell followed by the resolved `${layout.markdownDir}/<stem>.md` path. Split Reason is exactly one of `explicit-user-layout`, `primary-business-resource`, `independent-business-resource`, or `output-budget`. Group by stable business resource/domain, not by CRUD operation, AC, parameter/field axis, scenario type or regression purpose: one resource's list/detail/create/update/delete and its filters/response assertions/regression floor belong in one module. Multiple modules owning the same exact `METHOD /path` are forbidden unless every such row is `explicit-user-layout` from primary-requirement path pairs or has a documented `output-budget` proof. Keep the total module count at the smallest safe value and never exceed 8 modules. Name model-derived modules with stable lowercase business stems such as `health` or `resource_notes`; explicit-user-layout preserves the primary requirement filename stem even when it is more specific. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix. Pure hexadecimal/hash-like opaque stems and test-purpose-only stems are forbidden. Do not use Case-ID-like module filenames. The relative link target, Markdown Path, Pytest Path and downstream automation mapping must be one-to-one and exact; for model-derived modules the default pair remains `testcase/md/<module>.md` and `testcase/test_<module>.py`, while explicit-user-layout preserves the primary requirement paths. Do not hand-write a conflicting module count in prose; the Module Index row count is the only count truth.",
|
|
4910
|
-
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4950
|
+
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. Scheme A: Case count may be smaller than the enum count, but every exact slot still needs an independent variant Test Point and pytest.param id; never use SINGLE/MULTIPLE aliases as coverage. Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4911
4951
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
4912
4952
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4913
4953
|
intake.boundedSourceContext,
|
|
@@ -4991,7 +5031,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4991
5031
|
'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol. Evidence-only meta cases that exist solely for non-executable assertion/cross-cutting process evidence may declare `脚本:无` and `primary symbol:无` with empty `变体测试点`, and must not invent a business pytest item.',
|
|
4992
5032
|
"Name this module file with the exact frozen Module Index stem `{{item.stem}}` (filename `{{item.markdownPath}}`). Never reinterpret or rename an explicit-user-layout stem. Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `{{item.pytestPath}}`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
4993
5033
|
"AUTOMATION_BINDING_FORMAT_V1 is a literal machine contract. Under every Case's `### 自动化映射`, write these independent lines exactly: `- 脚本:<path|无>`, `- primary symbol:<symbol|无>`, `- 变体测试点:<semicolon-separated TP IDs|无>`, `- 场景断言测试点:<semicolon-separated TP IDs|无>`, `- 横切证据测试点:<semicolon-separated TP IDs|无>`. TP IDs must be on the same line after the colon. Forbidden classification forms include `TP-X(变体测试点)`, `[变体测试点] TP-X`, `【变体测试点】:TP-X`, pipe-delimited annotations, tables, or nested TP lists. Before returning, verify that the Case `### 测试点` exact set equals the pairwise-disjoint union of the three canonical binding lines; do not add, remove, rename or duplicate a TP to make the format pass.",
|
|
4994
|
-
"Scenario Partition slots:
|
|
5034
|
+
"Scenario Partition slots: requiredVariantSlots for this module are `{{item.requiredVariantSlots}}`. Every listed ID MUST appear in this module's Cases as exactly one variant Test Point in both `### 测试点` and `变体测试点`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. Ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: one Case may carry many slots; do not create one Case per enum value just to match Case count. Prefer ONE Case per partition with a parameter table over duplicated Cases per value. The not-in-set slot value must be a concrete literal absent from the Domain (e.g. `UNKNOWN_TYPE`) and its expected result must come from the bound source — when Expected by Slot is GAP, the Case states the expectation as GAP evidence, never a guessed 空列表/400. Never create cross-axis combination variants beyond the single documented nominal.",
|
|
4995
5035
|
"For every variant Test Point, write its machine-checkable `场景意图: <TP-ID>; operation=...; target=...; intent=...` line inside that same Case body/自动化映射. Never collect Scenario Intent lines in a file-level appendix, implementation-details block, or another Case; local TP ownership is mandatory.",
|
|
4996
5036
|
"Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
4997
5037
|
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`, plus a deterministic payload contract. For operations without a request body write `Payload Contract: none`. Otherwise write `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum` (write `none` when there is no enum); nested fields use dot paths such as `approver.name`. Each Case describes exactly one target request payload contract: put every payload label on its own list line, never concatenate multiple operations or setup POST/PUT contracts into one label line, and never repeat a `Payload Contract:` token inside explanatory prose/details after the machine-readable line. Values must come only from bound API/DTO evidence, never guesses. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For evidence-only meta Cases with no executable business journey, write `脚本:无` and `primary symbol:无`, keep `变体测试点:无`, and place process evidence only in assertion/cross-cutting lists. If the bound contract only says an identifier is returned/present, do not declare a concrete identifier type. If a 404 Case needs a nonexistent path identifier but its syntax/type is unspecified, define a create-delete-derived valid identifier journey instead of an arbitrary UUID/text placeholder. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
@@ -5036,7 +5076,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
5036
5076
|
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, preserve every frozen Module Index filename exactly (never rename an explicit-user-layout module; model-derived invalid stems must have been rejected before map expansion), normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
5037
5077
|
"This is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; every label must occupy its own machine-readable list line, and a Case must never concatenate target/setup operations or multiple `Payload Contract` tokens onto one line, and explanatory prose/details must not repeat any `Payload Contract:` token; never infer missing keys or enum values. A target GET/DELETE operation with no request body must remain `Payload Contract: none` even when its setup journey performs POST/PUT with a DTO; setup payloads never redefine the target Case payload contract. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.",
|
|
5038
5078
|
"For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints. Do not invent a concrete identifier type when the source only requires presence; for a missing-resource 404 path with unspecified identifier syntax/type, synchronize the Case to a create-delete-derived valid identifier journey rather than an arbitrary UUID/text placeholder.",
|
|
5039
|
-
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
5079
|
+
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: keep existing Case structure and add missing exact variant Test Points to already-assigned Cases instead of creating one Case per enum value. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
5040
5080
|
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in the run-owned plan or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
5041
5081
|
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
5042
5082
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
@@ -5146,7 +5186,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
5146
5186
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly the frozen `{{item.pytestPath}}`. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
5147
5187
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize. Hard contract: request payload dicts may only contain DTO field keys from Payload Allowed Paths; never put expect/expected/echo_* helper keys inside the JSON body dict. Path/query/header identifiers and scenario-control metadata (including `id`, expected codes, and selector labels) must stay in separate pytest parameters and helper arguments; never merge them into a DTO patch or JSON body unless that exact path is allowed by the Markdown payload contract. Normalize the configured API base URL with `rstrip(\"/\")` (or equivalently join exactly one slash) before appending endpoint paths; generated requests must never contain a `//api/...` path. When the bound source documents a concrete non-secret local API URL, generated clients must use it as the fallback in `os.environ.get(\"API_BASE_URL\", \"<documented-url>\")`; do not require an otherwise-uninjected environment variable or fail setup solely because it is absent. Missing-field helpers must remove keys idempotently with `payload.pop(field, None)`, never `del payload[field]`, because optional fields may already be absent.",
|
|
5148
5188
|
"For every response contract that requires an object or pagination envelope, first assert that each envelope/data value is a dict and that required keys exist, then index fields and assert values. Never let an incidental KeyError or list/string TypeError stand in for the explicit response-shape contract failure.",
|
|
5149
|
-
'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use a literal direct `pytest.param(..., id=...)` expression for every row; never hide or wrap it behind `_post_case`, `_put_case`, row-factory functions, comprehensions, generators, or dynamically returned parameter lists; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. GET/DELETE setup journeys may create resources, but their setup DTO must not change the target operation\'s no-body payload contract. No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
5189
|
+
'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. A Case with exactly one variant Test Point still needs one literal `pytest.param(..., id="TP-...")` row; never leave a single-variant Case as a bare function with the TP only in the docstring. Use a literal direct `pytest.param(..., id=...)` expression for every row; never hide or wrap it behind `_post_case`, `_put_case`, row-factory functions, comprehensions, generators, or dynamically returned parameter lists; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. GET/list filters still declare query fields in those payload labels when the Case varies `params=`/`query=` keys. Python `True`/`False` may implement JSON/OpenAPI `true`/`false` query or body booleans. GET/DELETE setup journeys may create resources, but their setup DTO must not change the target operation\'s no-body payload contract. No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
5150
5190
|
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly the frozen `{{item.pytestPath}}`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.py`. If Markdown automation mapping names a different path than this module stem path, still write the frozen manifest pytest path and do not invent prefixes. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
5151
5191
|
"Scenario Partition slots: every `TP-<Partition ID>-...` variant Test Point declared by this module's Markdown MUST become exactly one literal direct `pytest.param(..., id=\"TP-<Partition ID>-...\")` row with the exact slot ID; the not-in-set slot passes a concrete literal absent from the documented Domain (e.g. `UNKNOWN_TYPE`) — never `_OMIT`, never a descriptive token. Never split one slot into multiple params or merge several slots under a family TP id. Slot filtering requests hit the documented list endpoint with the slot value as the query/path filter.",
|
|
5152
5192
|
"Keep this module self-contained: define module-local fixtures and helpers directly in `{{item.pytestPath}}`, so pytest discovers every fixture dependency without external plugin registration. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions. Recursively redact sensitive values and apply bounded truncation before logging.",
|
|
@@ -19,6 +19,40 @@ const RERUN_FEEDBACK_ROLES = new Set([
|
|
|
19
19
|
"verifier",
|
|
20
20
|
"reviewer",
|
|
21
21
|
]);
|
|
22
|
+
function globPatternToRegExp(pattern) {
|
|
23
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
24
|
+
let source = "";
|
|
25
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
26
|
+
const char = normalized[index];
|
|
27
|
+
if (char === "*" && normalized[index + 1] === "*") {
|
|
28
|
+
source += ".*";
|
|
29
|
+
index += 1;
|
|
30
|
+
}
|
|
31
|
+
else if (char === "*") {
|
|
32
|
+
source += "[^/]*";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
source += char.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return new RegExp(`^${source}$`);
|
|
39
|
+
}
|
|
40
|
+
function readSetAllowsArtifactPath(task, artifactPath) {
|
|
41
|
+
const readSet = task.readSet ?? [];
|
|
42
|
+
if (readSet.length === 0)
|
|
43
|
+
return true;
|
|
44
|
+
const candidate = artifactPath.replace(/\\/g, "/");
|
|
45
|
+
const candidates = [candidate];
|
|
46
|
+
for (const marker of ["/.harness/", "/source/", "/testcase/"]) {
|
|
47
|
+
const index = candidate.indexOf(marker);
|
|
48
|
+
if (index >= 0)
|
|
49
|
+
candidates.push(candidate.slice(index + 1));
|
|
50
|
+
}
|
|
51
|
+
return readSet.some((pattern) => {
|
|
52
|
+
const regex = globPatternToRegExp(pattern);
|
|
53
|
+
return candidates.some((value) => regex.test(value));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
22
56
|
function formatBulletList(items, emptyLabel) {
|
|
23
57
|
if (!items || items.length === 0)
|
|
24
58
|
return emptyLabel;
|
|
@@ -129,7 +163,7 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
|
|
|
129
163
|
: artifactKind === "assistant"
|
|
130
164
|
? record.assistantArtifactPath
|
|
131
165
|
: undefined;
|
|
132
|
-
if (truncated && artifactPath) {
|
|
166
|
+
if (truncated && artifactPath && readSetAllowsArtifactPath(task, artifactPath)) {
|
|
133
167
|
section += formatUpstreamArtifactPointerMap(record, artifactKind === "stdout" ? "stdout" : "assistant");
|
|
134
168
|
}
|
|
135
169
|
else if (truncated && record.nodeRecordPath) {
|
|
@@ -205,7 +239,7 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
|
|
|
205
239
|
if (consumedArtifactSection) {
|
|
206
240
|
section += `\n\n${consumedArtifactSection}`;
|
|
207
241
|
}
|
|
208
|
-
if (truncated && artifactPath) {
|
|
242
|
+
if (truncated && artifactPath && readSetAllowsArtifactPath(task, artifactPath)) {
|
|
209
243
|
section += formatUpstreamArtifactPointerMap(record, artifactKind);
|
|
210
244
|
}
|
|
211
245
|
sections.push(section);
|