@tea-agent/loop-agent 0.39.0-beta.5 → 0.39.0-beta.7
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 +13 -1
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +138 -36
- package/dist/executors/pi-executor.js +7 -0
- package/dist/executors/pi-playwright-cli-tool.js +23 -16
- package/dist/executors/shell-executor.js +26 -9
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/task/config-types.js +6 -0
- package/dist/task/frontend-project-capability.js +19 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/routes.js +5 -3
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-bPer1waP.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BLdkwucW.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-Bw4wo4I1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-DUUkc0gr.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-BC3O0nob.js} +1 -1
- package/dist/worker/console/static/assets/channel-AbFtVnFm.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-Dd4g2nti.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-s2EDm7-n.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-GqtHM2yQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-BzRFrN8j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-Bw9nFjCJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-B4n-03Qm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-Dt75ZdBD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-B_afMhHB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-Bd8Rqqg9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-9boOMji4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BtCycfCc.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-eTOts9L1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Cibd2mjg.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-CDjDZtDV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-Btexn-JJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-Cyl8sdpM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Ev-83E1t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-41LqE0Mc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-BC39T5R3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DzcNRuri.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-B5Frvvnz.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-BapxROIM.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-Cd4d4wQ0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-CrL6buFW.js} +1 -1
- package/dist/worker/console/static/assets/index-DBh_x_Ek.js +406 -0
- package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BrSiNxyd.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-gvlvY-n9.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-D-YaPzRQ.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-BsmDv9E_.js} +1 -1
- package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-UWaoDN3h.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-BlvpGVG8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-CUCIP5CP.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CB1aQUAK.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-BwehHxhf.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-CFz30APB.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-BwNmw1i6.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-jg-tPXlI.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-BlyReDKl.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DBUzW1f3.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-BYcI2RAr.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-D7AiyJdo.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-BHGt7Kkv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-OPjooWcX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-ggsWDvrO.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DjNe-BjZ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-Byp63Hei.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
- package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -128
- package/dist/workflows/dag/frontend-design-policy.js +31 -7
- package/dist/workflows/dag/frontend-implementation-contract.js +78 -3
- package/dist/workflows/dag/frontend-review-findings.js +238 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +24 -10
- package/dist/workflows/dag/frontend-shape.js +5 -3
- package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
- package/dist/workflows/dag/init-hybrid.js +149 -15
- package/dist/workflows/dag/node-execution.js +18 -1
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +101 -1
- package/dist/workflows/dag/retry-policy.js +31 -3
- package/dist/workflows/dag/runner.js +46 -1
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +32 -22
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/package.json +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +4 -4
- package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +10 -8
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
- package/dist/worker/console/static/assets/index-D5zGX6fG.js +0 -404
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Frontend-test artifact layout (configurable testRoot).
|
|
4
|
+
*
|
|
5
|
+
* Resolved once from `task.frontendTest.testRoot` at DAG compile time and
|
|
6
|
+
* frozen on the spec for the whole run. Writers, gates, Playwright fences
|
|
7
|
+
* and TaskSpec allowedPaths all derive from this single object.
|
|
8
|
+
*
|
|
9
|
+
* Default layout keeps the historical frozen contract:
|
|
10
|
+
* testcase/frontend/rag/**
|
|
11
|
+
* testcase/frontend/cases/**
|
|
12
|
+
* testcase/frontend/evidence/**
|
|
13
|
+
* testcase/frontend/reports/**
|
|
14
|
+
* testcase/frontend/fixtures/**
|
|
15
|
+
*/
|
|
16
|
+
export const FRONTEND_TEST_DEFAULT_TEST_ROOT = "testcase/frontend";
|
|
17
|
+
const FORBIDDEN_TEST_ROOT_PREFIXES = [
|
|
18
|
+
"src",
|
|
19
|
+
"app",
|
|
20
|
+
"lib",
|
|
21
|
+
"apps",
|
|
22
|
+
"packages",
|
|
23
|
+
".harness",
|
|
24
|
+
".git",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
"docs",
|
|
29
|
+
"website",
|
|
30
|
+
"scripts",
|
|
31
|
+
"test",
|
|
32
|
+
"smoke",
|
|
33
|
+
"knowledge",
|
|
34
|
+
"features",
|
|
35
|
+
"dogfood",
|
|
36
|
+
"examples",
|
|
37
|
+
"bin",
|
|
38
|
+
];
|
|
39
|
+
function normalizeRelPosix(value, label) {
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
throw new Error(`frontendTest.${label} must not be empty`);
|
|
43
|
+
if (trimmed.includes("\\")) {
|
|
44
|
+
throw new Error(`frontendTest.${label} must use forward slashes: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
if (path.isAbsolute(trimmed) || /^[A-Za-z]:/.test(trimmed)) {
|
|
47
|
+
throw new Error(`frontendTest.${label} must be a repository-relative path: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
const segments = trimmed.split("/").filter((segment) => segment.length > 0);
|
|
50
|
+
if (segments.some((segment) => segment === ".." || segment === ".")) {
|
|
51
|
+
throw new Error(`frontendTest.${label} must not contain "." or ".." segments: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
if (segments.some((segment) => segment.startsWith("."))) {
|
|
54
|
+
throw new Error(`frontendTest.${label} must not target hidden directories: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
|
|
57
|
+
throw new Error(`frontendTest.${label} contains unsafe path segments: ${value}`);
|
|
58
|
+
}
|
|
59
|
+
return segments.join("/");
|
|
60
|
+
}
|
|
61
|
+
function assertTestRootAllowed(testRoot) {
|
|
62
|
+
const topSegment = testRoot.split("/")[0];
|
|
63
|
+
if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
|
|
64
|
+
throw new Error(`frontendTest.testRoot must not target production/governance trees: ${testRoot}`);
|
|
65
|
+
}
|
|
66
|
+
if (topSegment.startsWith(".")) {
|
|
67
|
+
throw new Error(`frontendTest.testRoot must not target hidden directories: ${testRoot}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the frozen frontend-test artifact layout from task config.
|
|
72
|
+
* Throws on unsafe or ambiguous configuration (fail-closed).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveFrontendTestLayout(config) {
|
|
75
|
+
const testRoot = config?.testRoot === undefined || config?.testRoot === null
|
|
76
|
+
? FRONTEND_TEST_DEFAULT_TEST_ROOT
|
|
77
|
+
: normalizeRelPosix(config.testRoot, "testRoot");
|
|
78
|
+
assertTestRootAllowed(testRoot);
|
|
79
|
+
const isDefault = testRoot === FRONTEND_TEST_DEFAULT_TEST_ROOT;
|
|
80
|
+
return {
|
|
81
|
+
testRoot,
|
|
82
|
+
ragDir: `${testRoot}/rag`,
|
|
83
|
+
casesDir: `${testRoot}/cases`,
|
|
84
|
+
evidenceDir: `${testRoot}/evidence`,
|
|
85
|
+
reportsDir: `${testRoot}/reports`,
|
|
86
|
+
fixturesDir: `${testRoot}/fixtures`,
|
|
87
|
+
isDefault,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Historical default layout. Old DAGs without frontendTestLayout use this. */
|
|
91
|
+
export function defaultFrontendTestLayout() {
|
|
92
|
+
return resolveFrontendTestLayout(undefined);
|
|
93
|
+
}
|
|
94
|
+
export function frontendTestLayoutFromSpec(spec) {
|
|
95
|
+
const frozen = spec?.frontendTestLayout;
|
|
96
|
+
if (!frozen)
|
|
97
|
+
return defaultFrontendTestLayout();
|
|
98
|
+
return resolveFrontendTestLayout({ testRoot: frozen.testRoot });
|
|
99
|
+
}
|
|
100
|
+
const DEFAULT_PREFIX = `${FRONTEND_TEST_DEFAULT_TEST_ROOT}/`;
|
|
101
|
+
/**
|
|
102
|
+
* Longest-prefix rewrite of historical `testcase/frontend/` paths.
|
|
103
|
+
* No-op when layout.isDefault so default DAGs stay byte-identical.
|
|
104
|
+
* Never replaces the short token `testcase/` (would collide with backend-test).
|
|
105
|
+
*/
|
|
106
|
+
export function applyFrontendTestLayoutToText(text, layout) {
|
|
107
|
+
if (layout.isDefault)
|
|
108
|
+
return text;
|
|
109
|
+
const replacement = `${layout.testRoot}/`;
|
|
110
|
+
let output = "";
|
|
111
|
+
for (let i = 0; i < text.length;) {
|
|
112
|
+
if (text.startsWith(replacement, i)) {
|
|
113
|
+
output += replacement;
|
|
114
|
+
i += replacement.length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (text.startsWith(DEFAULT_PREFIX, i)) {
|
|
118
|
+
output += replacement;
|
|
119
|
+
i += DEFAULT_PREFIX.length;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
output += text[i];
|
|
123
|
+
i += 1;
|
|
124
|
+
}
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
export function frontendTestCasePath(layout, caseId) {
|
|
128
|
+
return `${layout.casesDir}/${caseId}.md`;
|
|
129
|
+
}
|
|
130
|
+
export function frontendTestEvidenceDir(layout, caseId) {
|
|
131
|
+
return `${layout.evidenceDir}/${caseId}/`;
|
|
132
|
+
}
|
|
133
|
+
export function frontendTestEvidencePrefix(layout, caseId) {
|
|
134
|
+
return `${layout.evidenceDir}/${caseId}`;
|
|
135
|
+
}
|
|
136
|
+
export function frontendTestManifestPath(layout) {
|
|
137
|
+
return `${layout.casesDir}/manifest.json`;
|
|
138
|
+
}
|
|
139
|
+
export function frontendTestManifestDraftPath(layout) {
|
|
140
|
+
return `${layout.casesDir}/manifest.draft.json`;
|
|
141
|
+
}
|
|
142
|
+
export function frontendTestContextRel(layout) {
|
|
143
|
+
return `${layout.ragDir}/context.md`;
|
|
144
|
+
}
|
|
145
|
+
export function frontendTestCoverageMapRel(layout) {
|
|
146
|
+
return `${layout.ragDir}/coverage-map.md`;
|
|
147
|
+
}
|
|
148
|
+
export function frontendTestProbeRel(layout) {
|
|
149
|
+
return `${layout.ragDir}/environment-probe.json`;
|
|
150
|
+
}
|
|
151
|
+
export function frontendTestStandardScenariosRel(layout) {
|
|
152
|
+
return `${layout.ragDir}/standard-scenarios.v1.json`;
|
|
153
|
+
}
|
|
154
|
+
export function frontendTestUiAnchorsRel(layout) {
|
|
155
|
+
return `${layout.ragDir}/ui-anchors.md`;
|
|
156
|
+
}
|
|
157
|
+
export function escapeRegexLiteral(value) {
|
|
158
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
@@ -6,6 +6,7 @@ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-s
|
|
|
6
6
|
import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
|
|
7
7
|
import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
|
|
8
8
|
import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
|
|
9
|
+
import { defaultFrontendTestLayout, frontendTestEvidencePrefix, frontendTestManifestPath, } from "./frontend-test-layout.js";
|
|
9
10
|
export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
|
|
10
11
|
/**
|
|
11
12
|
* Truncated acceptance-id shapes observed in generator output: a bare
|
|
@@ -124,12 +125,14 @@ function isSafeEvidenceShellPath(value) {
|
|
|
124
125
|
* backslash interpretation for the former long `node -e` command.
|
|
125
126
|
*/
|
|
126
127
|
export async function validateFrontendCaseEvidence(input) {
|
|
127
|
-
const
|
|
128
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
129
|
+
const manifestRel = frontendTestManifestPath(layout);
|
|
130
|
+
const manifestPath = path.join(input.workspaceRoot, manifestRel);
|
|
128
131
|
try {
|
|
129
132
|
await stat(manifestPath);
|
|
130
133
|
}
|
|
131
134
|
catch {
|
|
132
|
-
throw new Error(
|
|
135
|
+
throw new Error(`missing ${manifestRel}`);
|
|
133
136
|
}
|
|
134
137
|
let manifest;
|
|
135
138
|
try {
|
|
@@ -147,7 +150,7 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
147
150
|
const item = rawCase;
|
|
148
151
|
const id = typeof item?.caseId === "string" ? item.caseId : "?";
|
|
149
152
|
const dir = item?.evidenceDir;
|
|
150
|
-
const prefix =
|
|
153
|
+
const prefix = frontendTestEvidencePrefix(layout, id);
|
|
151
154
|
if (!item ||
|
|
152
155
|
typeof item.caseId !== "string" ||
|
|
153
156
|
typeof dir !== "string" ||
|
|
@@ -191,12 +194,15 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
191
194
|
continue;
|
|
192
195
|
}
|
|
193
196
|
const currentPrefix = `${prefix}/`;
|
|
194
|
-
if (evidencePath.startsWith(
|
|
197
|
+
if ((evidencePath.startsWith(`${layout.evidenceDir}/`) ||
|
|
198
|
+
evidencePath.startsWith("testcase/frontend/evidence/")) &&
|
|
199
|
+
!evidencePath.startsWith(currentPrefix)) {
|
|
195
200
|
hardFail = true;
|
|
196
201
|
issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
|
|
197
202
|
continue;
|
|
198
203
|
}
|
|
199
|
-
const target = evidencePath.startsWith(
|
|
204
|
+
const target = evidencePath.startsWith(`${layout.testRoot}/`) ||
|
|
205
|
+
evidencePath.startsWith("testcase/")
|
|
200
206
|
? path.join(input.workspaceRoot, evidencePath)
|
|
201
207
|
: path.join(input.workspaceRoot, dir, evidencePath);
|
|
202
208
|
if (!(await isNonEmptyFile(target))) {
|
|
@@ -343,8 +349,10 @@ function sha256(content) {
|
|
|
343
349
|
* headings. Numbered and bulleted lists are extracted; a missing 测试目的
|
|
344
350
|
* falls back to the case H1 title.
|
|
345
351
|
*/
|
|
346
|
-
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
347
|
-
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
352
|
+
async function readFrontendCaseContent(workspaceRoot, casePath, layout) {
|
|
353
|
+
if (!safeRelativePathSchema.safeParse(casePath).success ||
|
|
354
|
+
!(casePath.startsWith(`${layout.casesDir}/`) ||
|
|
355
|
+
casePath.startsWith("testcase/frontend/cases/"))) {
|
|
348
356
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
349
357
|
}
|
|
350
358
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
@@ -397,7 +405,7 @@ function assertInside(root, candidate, label) {
|
|
|
397
405
|
throw new Error(`${label} escapes its allowed root`);
|
|
398
406
|
}
|
|
399
407
|
}
|
|
400
|
-
async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
408
|
+
async function readEvidenceFile(repoRoot, evidenceRoot, relative, layout) {
|
|
401
409
|
if (!safeRelativePathSchema.safeParse(relative).success) {
|
|
402
410
|
throw new Error(`unsafe evidence path: ${relative}`);
|
|
403
411
|
}
|
|
@@ -409,12 +417,16 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
|
|
|
409
417
|
const normalized = relative;
|
|
410
418
|
const evidenceRootRel = path.relative(repoRootResolved, evidenceRootResolved).replaceAll("\\", "/");
|
|
411
419
|
const isCurrentCaseRepoPath = normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`);
|
|
412
|
-
const isRepoRelative = normalized.startsWith(
|
|
420
|
+
const isRepoRelative = (normalized.startsWith(`${layout.testRoot}/`) ||
|
|
421
|
+
normalized.startsWith("testcase/")) &&
|
|
422
|
+
!isCurrentCaseRepoPath;
|
|
413
423
|
const absolute = isRepoRelative
|
|
414
424
|
? path.resolve(repoRootResolved, normalized)
|
|
415
425
|
: path.resolve(evidenceRootResolved, normalized);
|
|
416
426
|
assertInside(repoRootResolved, absolute, "evidence path");
|
|
417
|
-
if (isRepoRelative &&
|
|
427
|
+
if (isRepoRelative &&
|
|
428
|
+
(normalized.startsWith(`${layout.evidenceDir}/`) ||
|
|
429
|
+
normalized.startsWith("testcase/frontend/evidence/"))) {
|
|
418
430
|
throw new Error(`evidence path escapes evidence root: ${relative}`);
|
|
419
431
|
}
|
|
420
432
|
if (!isRepoRelative)
|
|
@@ -437,7 +449,8 @@ export async function materializeFrontendTestResult(input) {
|
|
|
437
449
|
if (!sourceBinding?.taskId || !sourceBinding.sources?.length) {
|
|
438
450
|
throw new Error("frontend-test result requires frozen sourceBinding");
|
|
439
451
|
}
|
|
440
|
-
const
|
|
452
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
453
|
+
const manifestPath = frontendTestManifestPath(layout);
|
|
441
454
|
const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
|
|
442
455
|
let manifest;
|
|
443
456
|
try {
|
|
@@ -453,6 +466,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
453
466
|
const contentQuality = await validateFrontendCaseContent({
|
|
454
467
|
workspaceRoot: input.workspaceRoot,
|
|
455
468
|
requiredAcIds: sourceBinding.requirementIds ?? [],
|
|
469
|
+
layout,
|
|
456
470
|
});
|
|
457
471
|
advisoryFindings.push(...contentQuality.findings);
|
|
458
472
|
const requiredIds = new Set(sourceBinding.requirementIds ?? []);
|
|
@@ -464,7 +478,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
464
478
|
advisoryFindings.push({ ruleId: "invalid-manifest-case", detail: "manifest case is missing caseId, acIds, or evidenceDir" });
|
|
465
479
|
continue;
|
|
466
480
|
}
|
|
467
|
-
const evidencePrefix =
|
|
481
|
+
const evidencePrefix = frontendTestEvidencePrefix(layout, item.caseId);
|
|
468
482
|
if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
|
|
469
483
|
throw new Error(`unsafe evidenceDir for ${item.caseId}`);
|
|
470
484
|
}
|
|
@@ -495,7 +509,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
495
509
|
if (typeof evidencePath !== "string" || evidencePath === "execution.md" && evidence.some((entry) => entry.path.endsWith("/execution.md")))
|
|
496
510
|
continue;
|
|
497
511
|
try {
|
|
498
|
-
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
|
|
512
|
+
evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath, layout));
|
|
499
513
|
}
|
|
500
514
|
catch (error) {
|
|
501
515
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -541,7 +555,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
541
555
|
}
|
|
542
556
|
}
|
|
543
557
|
}
|
|
544
|
-
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
558
|
+
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath, layout);
|
|
545
559
|
// Parse bounded fixed sections from execution.md. Missing optional
|
|
546
560
|
// summary/steps are advisory only and never downgrade a passed status.
|
|
547
561
|
let executionSummary = "";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
|
|
3
4
|
export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
|
|
4
5
|
export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
|
|
5
6
|
const MINIMAL_STANDARD_SCENARIOS = {
|
|
@@ -47,7 +48,8 @@ async function readGovernanceRoot(workspaceRoot) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
export async function copyFrontendTestStandardScenarios(input) {
|
|
50
|
-
const
|
|
51
|
+
const layout = input.layout ?? defaultFrontendTestLayout();
|
|
52
|
+
const destRel = frontendTestStandardScenariosRel(layout);
|
|
51
53
|
const destAbs = path.join(input.workspaceRoot, destRel);
|
|
52
54
|
const candidates = listFrontendTestStandardScenarioCandidates({
|
|
53
55
|
governanceRoot: await readGovernanceRoot(input.workspaceRoot),
|
|
@@ -28,6 +28,7 @@ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
|
28
28
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
29
29
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
30
30
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
31
|
+
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
31
32
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
32
33
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
33
34
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
@@ -2256,6 +2257,83 @@ function frontendMockStrategyMustBeNotNeeded(sources) {
|
|
|
2256
2257
|
capabilityStatus === "ambiguous" ||
|
|
2257
2258
|
!hasDeterministicMockVerification));
|
|
2258
2259
|
}
|
|
2260
|
+
/**
|
|
2261
|
+
* 候选规范懒加载:prompt 只内联与任务相关的候选,不全部塞给模型。
|
|
2262
|
+
*
|
|
2263
|
+
* - mandatory(任务源显式声明/引用)始终保留——模型必须知道它们存在。
|
|
2264
|
+
* - scan-strict 候选按「路径段是否命中任务源关键词」过滤:列表页任务只
|
|
2265
|
+
* 带出列表相关组件/页面规范,创建页规范不进 prompt。
|
|
2266
|
+
* - runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths(prewrite
|
|
2267
|
+
* gate 用全量);这里只缩小 prompt 体积,不改变门禁语义。未提及的候选
|
|
2268
|
+
* 由 runtime 默认 irrelevant,模型无需枚举。
|
|
2269
|
+
* - 真正读取发生在 plan 侧:模型对 required 选型调用 read 工具,prewrite
|
|
2270
|
+
* 门禁验证 read 事件——按需读取,不预加载正文。
|
|
2271
|
+
*/
|
|
2272
|
+
export function filterRelevantOpenspecCandidates(input) {
|
|
2273
|
+
const mandatory = new Set(input.mandatoryPaths);
|
|
2274
|
+
const relevant = [];
|
|
2275
|
+
for (const candidate of input.candidates) {
|
|
2276
|
+
if (mandatory.has(candidate)) {
|
|
2277
|
+
relevant.push(candidate);
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (openspecCandidateMatchesSource(candidate, input.sourceMarkdown)) {
|
|
2281
|
+
relevant.push(candidate);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
// 保持候选发现顺序(确定性);mandatory 与相关候选都按原始顺序出现。
|
|
2285
|
+
return relevant;
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* 候选路径是否与任务源相关:从任务源提取文件名/路径 token(去扩展名、
|
|
2289
|
+
* 去连字符/下划线),若候选的路径段包含任一 token 即视为相关。纯字符串
|
|
2290
|
+
* 判定,无 IO;找不到 token 时保守保留(避免漏掉模型可能需要的规范)。
|
|
2291
|
+
*/
|
|
2292
|
+
function openspecCandidateMatchesSource(candidate, sourceMarkdown) {
|
|
2293
|
+
const sourceTokens = extractOpenspecSourceTokens(sourceMarkdown);
|
|
2294
|
+
if (sourceTokens.size === 0)
|
|
2295
|
+
return true;
|
|
2296
|
+
const candidateLower = candidate.toLowerCase().replace(/\\/g, "/");
|
|
2297
|
+
const candidateSegments = candidateLower.split("/");
|
|
2298
|
+
const candidateBasename = candidateSegments[candidateSegments.length - 1] ?? "";
|
|
2299
|
+
for (const token of sourceTokens) {
|
|
2300
|
+
if (candidateBasename.includes(token) ||
|
|
2301
|
+
candidateLower.includes(`/${token}/`) ||
|
|
2302
|
+
candidateLower.includes(`${token}.`)) {
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return false;
|
|
2307
|
+
}
|
|
2308
|
+
/** 从任务源 markdown 提取显著 token:反引号代码段、路径、组件/页面名。 */
|
|
2309
|
+
function extractOpenspecSourceTokens(markdown) {
|
|
2310
|
+
const tokens = new Set();
|
|
2311
|
+
const push = (raw) => {
|
|
2312
|
+
const cleaned = raw
|
|
2313
|
+
.trim()
|
|
2314
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "")
|
|
2315
|
+
.toLowerCase();
|
|
2316
|
+
if (cleaned.length >= 2 && cleaned.length <= 40)
|
|
2317
|
+
tokens.add(cleaned);
|
|
2318
|
+
};
|
|
2319
|
+
// 反引号内联代码(文件名/组件名)
|
|
2320
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
|
|
2321
|
+
push(match[1] ?? "");
|
|
2322
|
+
}
|
|
2323
|
+
// markdown 链接文本
|
|
2324
|
+
for (const match of markdown.matchAll(/\[([^\]]+)\]\([^)]+\)/g)) {
|
|
2325
|
+
push(match[1] ?? "");
|
|
2326
|
+
}
|
|
2327
|
+
// 路径 token(openspec/ 或 xxx/xxx.md)
|
|
2328
|
+
for (const match of markdown.matchAll(/(?:[A-Za-z0-9_-]+\/)+[A-Za-z0-9_.-]+/g)) {
|
|
2329
|
+
const segments = (match[0] ?? "").split("/");
|
|
2330
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2331
|
+
push(basename.replace(/\.[a-z0-9]+$/i, ""));
|
|
2332
|
+
for (const segment of segments)
|
|
2333
|
+
push(segment);
|
|
2334
|
+
}
|
|
2335
|
+
return tokens;
|
|
2336
|
+
}
|
|
2259
2337
|
async function resolveFrontendOpenspecGateConfig(sources) {
|
|
2260
2338
|
const taskConfig = sources.taskConfig;
|
|
2261
2339
|
const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
|
|
@@ -2869,10 +2947,22 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2869
2947
|
const openspecGate = await resolveFrontendOpenspecGateConfig(sources);
|
|
2870
2948
|
const requiresOpenspecClassification = openspecGate.openspecPolicy === "cited" &&
|
|
2871
2949
|
openspecGate.openspecCandidatePaths.length > 0;
|
|
2950
|
+
// Prompt 内联的候选只保留与任务相关的子集:mandatory(任务显式声明/
|
|
2951
|
+
// 引用)始终保留;scan-strict 候选按路径段是否命中任务源关键词过滤。
|
|
2952
|
+
// runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths——这里只
|
|
2953
|
+
// 减小 prompt 体积,不改变门禁语义;未提及的候选 runtime 默认 irrelevant。
|
|
2954
|
+
const promptCandidates = filterRelevantOpenspecCandidates({
|
|
2955
|
+
candidates: openspecGate.openspecCandidatePaths,
|
|
2956
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
2957
|
+
sourceMarkdown: [
|
|
2958
|
+
sources.requirementMarkdown,
|
|
2959
|
+
sources.constraintMarkdown ?? "",
|
|
2960
|
+
].join("\n"),
|
|
2961
|
+
});
|
|
2872
2962
|
const openspecSelectionContext = JSON.stringify({
|
|
2873
2963
|
schemaVersion: 1,
|
|
2874
2964
|
schemaId: "frontend-openspec-selection-v1",
|
|
2875
|
-
candidates:
|
|
2965
|
+
candidates: promptCandidates,
|
|
2876
2966
|
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
2877
2967
|
});
|
|
2878
2968
|
// Generation-frozen component/theme specification bucket (ADR 0016). Derived
|
|
@@ -2985,7 +3075,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2985
3075
|
outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency / record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from the committed ledger facts (requirements / verificationTargets / evidenceGaps / residualRisks / realIntegrationGap). Commit requirements and verification targets incrementally — one entry per record call — so the plan never needs a single large output; evidence gaps are optional and only needed for genuine gaps. Implementation steps are NOT part of the plan — the implementer designs its own ordering. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime compiles facts ⊕ skeleton into canonical full-contract JSON for downstream review. No file writes.",
|
|
2986
3076
|
subtask_prompt: [
|
|
2987
3077
|
"Use frontend-contract-pi typed requirement facts (stable REQ/BR/AC identifiers, dispositions, evidence expectations, and frontend-test handoff intents), frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Record the plan decision ledger through the incremental record_* tools, then call finalize_plan exactly once. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2988
|
-
"Commit requirements with record_plan_requirement (one entry per call) and verification targets with record_plan_verification_target (one entry per call). Evidence gaps are optional — call record_plan_evidence_gap only for genuine gaps. Never bundle these into finalize_plan arguments — finalize_plan only closes the plan.
|
|
3078
|
+
"Commit requirements with record_plan_requirement (one entry per call) and verification targets with record_plan_verification_target (one entry per call). Omit expectedOutcome in requirements — the runtime derives it from the contract; keep each entry to id + targets + verificationTargetIds. Evidence gaps are optional — call record_plan_evidence_gap only for genuine gaps. Never bundle these into finalize_plan arguments — finalize_plan only closes the plan. CRITICAL: emit exactly ONE record tool call per assistant message — never batch multiple record_* calls in the same message, even though they look independent. Parallel-batching them makes a single message as large as the old full-contract output and re-introduces the truncation bug. One call per message, many messages, then finalize_plan exactly once at the end. Do NOT plan implementation steps: the implementer decides ordering itself.",
|
|
2989
3079
|
...(requiresOpenspecClassification ? ["Additionally consume the frontend-contract-pi openspec classification. Successfully read every required selection (including mandatory paths) and declare any used component specReference in the typed decision ledger; relevant/irrelevant selections are not forced into the ledger unless the plan actually uses them."] : []),
|
|
2990
3080
|
"The incremental record_* facts become the complete implementation plan after deterministic merge with the protected skeleton. Do not treat leftover JSON in the narrative as the compile authority.",
|
|
2991
3081
|
"Select the Mock / API strategy only through record_mock_api. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
|
|
@@ -4862,11 +4952,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
|
4862
4952
|
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4863
4953
|
spec.backendTestLayout = layout;
|
|
4864
4954
|
}
|
|
4955
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
4956
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
4957
|
+
if (layout.isDefault) {
|
|
4958
|
+
spec.frontendTestLayout = layout;
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
4962
|
+
for (const task of spec.tasks) {
|
|
4963
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4964
|
+
if (task.outputContract)
|
|
4965
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4966
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4967
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4968
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4969
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4970
|
+
const child = task.dynamicExpansion.childTask;
|
|
4971
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4972
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4973
|
+
}
|
|
4974
|
+
if (typeof child.outputContract === "string") {
|
|
4975
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4976
|
+
}
|
|
4977
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4978
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4979
|
+
}
|
|
4980
|
+
if (Array.isArray(child.writeSet)) {
|
|
4981
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4982
|
+
}
|
|
4983
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4984
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
if (task.shell?.commands) {
|
|
4988
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4992
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4993
|
+
spec.frontendTestLayout = layout;
|
|
4994
|
+
}
|
|
4995
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
4996
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
4997
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
4998
|
+
pathMatchesPattern(probe, pattern));
|
|
4999
|
+
}
|
|
4865
5000
|
// ---------------------------------------------------------------------------
|
|
4866
5001
|
// Frontend browser-test RAG DAG template
|
|
4867
5002
|
// ---------------------------------------------------------------------------
|
|
4868
5003
|
function buildFrontendTestHybridDag(sources) {
|
|
4869
5004
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
5005
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4870
5006
|
const config = {
|
|
4871
5007
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4872
5008
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4891,9 +5027,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4891
5027
|
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4892
5028
|
// exists so ledger-less projects keep generating without noise.
|
|
4893
5029
|
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4894
|
-
existsSync(path.join(sources.repoRoot, "
|
|
5030
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
4895
5031
|
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4896
|
-
?
|
|
5032
|
+
? `Also read ${layout.ragDir}/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly.`
|
|
4897
5033
|
: "";
|
|
4898
5034
|
const reviewMode = config.reviewMode;
|
|
4899
5035
|
const blockingReview = reviewMode === "blocking";
|
|
@@ -4901,22 +5037,19 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4901
5037
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4902
5038
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4903
5039
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
pattern === "**");
|
|
4907
|
-
if (!hasFrontendTestWriteScope) {
|
|
4908
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
5040
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
5041
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4909
5042
|
}
|
|
4910
5043
|
const forbidden = commonForbiddenPaths(sources);
|
|
4911
|
-
const ragWriteSet = [
|
|
5044
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4912
5045
|
const caseDraftWriteSet = [
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
5046
|
+
`${layout.casesDir}/FE-*.md`,
|
|
5047
|
+
`${layout.casesDir}/index.md`,
|
|
5048
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4916
5049
|
];
|
|
4917
5050
|
// The shell materializer alone owns the final manifest boundary.
|
|
4918
|
-
const casesWriteSet = [
|
|
4919
|
-
const evidenceRoot =
|
|
5051
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
5052
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4920
5053
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4921
5054
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4922
5055
|
const checklistScript = [
|
|
@@ -5508,6 +5641,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5508
5641
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5509
5642
|
tasks,
|
|
5510
5643
|
};
|
|
5644
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
5511
5645
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
5512
5646
|
parseDagSpec(spec);
|
|
5513
5647
|
assertValidDagSpec(spec);
|
|
@@ -238,6 +238,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
238
238
|
"</retry_instruction>",
|
|
239
239
|
].join("\n");
|
|
240
240
|
}
|
|
241
|
+
if (previousFailureCategory === "read-burst") {
|
|
242
|
+
return [
|
|
243
|
+
basePrompt,
|
|
244
|
+
"",
|
|
245
|
+
"<retry_instruction>",
|
|
246
|
+
"Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
|
|
247
|
+
"Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
|
|
248
|
+
"</retry_instruction>",
|
|
249
|
+
].join("\n");
|
|
250
|
+
}
|
|
241
251
|
if (previousFailureCategory === "invalid-output" &&
|
|
242
252
|
task.structuredContractOutput &&
|
|
243
253
|
previousProtocolReason) {
|
|
@@ -248,6 +258,12 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
248
258
|
// only to nodes whose authority is still a text contract artifact.
|
|
249
259
|
if (task.structuredContractOutput.schemaId ===
|
|
250
260
|
"frontend-implementation-contract-plan-patch-v1") {
|
|
261
|
+
const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
|
|
262
|
+
? [
|
|
263
|
+
"",
|
|
264
|
+
"The writeSet is too large for one implement node. Do NOT delete implementation files to squeeze under the limit — that drops required work. Split the task via record_split_proposal (or narrow targets.files to a genuine subset) so each implement node stays bounded; the full file set must remain covered across the split.",
|
|
265
|
+
]
|
|
266
|
+
: [];
|
|
251
267
|
return [
|
|
252
268
|
basePrompt,
|
|
253
269
|
"",
|
|
@@ -256,6 +272,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
256
272
|
previousProtocolReason,
|
|
257
273
|
"Fix the reported violations by re-committing corrected record_* facts and calling finalize_plan exactly once. The committed typed ledger is the only compile authority.",
|
|
258
274
|
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
275
|
+
...splitGuidance,
|
|
259
276
|
"</retry_instruction>",
|
|
260
277
|
].join("\n");
|
|
261
278
|
}
|
|
@@ -279,7 +296,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
279
296
|
"",
|
|
280
297
|
"<retry_instruction>",
|
|
281
298
|
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
282
|
-
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
|
|
299
|
+
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority. Do NOT re-read contract/scout outputs or source files — use the facts already in context. Commit record_* facts one tool call per message, then finalize_plan immediately.",
|
|
283
300
|
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
284
301
|
"</retry_instruction>",
|
|
285
302
|
].join("\n");
|