@tea-agent/loop-agent 0.39.0-next.30 → 0.39.0-next.32
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 +5 -0
- package/dist/build-stamp.json +2 -2
- package/dist/executors/dag-pi-executor.js +7 -4
- package/dist/executors/pi-playwright-cli-tool.js +66 -27
- package/dist/executors/shell-executor.js +25 -8
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/task/config-types.js +6 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CiGOV5eG.js → abnfDiagram-N423BO3Z-B_Eawmjz.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Ds9Gyfd8.js → arc-CAX7-_9s.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C6V6uC98.js → architectureDiagram-T3A2C74G-kSHUPU8D.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DezF7Ol9.js → blockDiagram-VBNYF7ZC-7uygSzId.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCWk3gCT.js → c4Diagram-5PPSVZJV-B7VBFqY8.js} +1 -1
- package/dist/worker/console/static/assets/channel-BqOf3G6O.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DbYr-Cxy.js → chunk-2GRJ4B5K-CToVTe0G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-XSlusW4G.js → chunk-2Q5K7J3B-C3Y6NsZJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BtcKAj9i.js → chunk-5RXB4S5H-D__FOJmB.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BF57lhXj.js → chunk-5VM5RSS4-DnbdnPQh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DJIlR25o.js → chunk-6Q2QTUOP-fxcnbtmR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DklNc7dm.js → chunk-GF5L2VYU-DkWP4DCN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-fZNtEdQZ.js → chunk-JWPE2WC7-3crbQ-b9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BBgLqV4h.js → chunk-KBJHAD2P-AW5fUBHx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Dl9e5n_J.js → chunk-RYQCIY6F-BsiQcbN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-B0JK51NT.js → chunk-XXDRQBXY-CuRhHBel.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BGuxW-g8.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BGuxW-g8.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Ch-ajv4O.js → cose-bilkent-JH36ORCC-VNMSbhR1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-xn_NxcfB.js → cynefin-VYW2F7L2-9VXNKhPA.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DRD0fyga.js → cynefinDiagram-MW4NZA55-DnA3i2aF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-COtnKp4B.js → dagre-VZM6K2ZE-CXf1ON53.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BQp_CAqG.js → diagram-7IWD3JNH-DkHu5QHM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-x0y-vNgq.js → diagram-B4RE2ZJO-b3DVvxzf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-N2ezwL31.js → diagram-LBJQPF4R-CMtGrmDr.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CSaxRH6Q.js → diagram-Q27KOJAE-CTUKQdiN.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-6Ro7Kpbd.js → diagram-UB23O5K3-XwXry4X0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CrIIQEAI.js → ebnfDiagram-BXEA7PRR-Dn6cLJ4_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DivKcq8g.js → erDiagram-JOGREHBK-MLibOYKC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BXrfDo8y.js → flowDiagram-UKHOOZJN-C3ErZkth.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-LTTQmMRJ.js → ganttDiagram-PKOTCBZU-CN8OlZId.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DP3o_5qd.js → gitGraphDiagram-DS77QQ5N-Cq4huIY9.js} +1 -1
- package/dist/worker/console/static/assets/{index-C2jmYBfu.js → index-DSWsdkbR.js} +52 -52
- package/dist/worker/console/static/assets/index-XZm1O8NF.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-wVfSY1WH.js → infoDiagram-6WML65LV-BG_CYrDS.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BRKfFL0I.js → ishikawaDiagram-WSZJBQD7-DCWDQKJF.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DEml3jLb.js → journeyDiagram-NVQOT4AX-DG_VS4XD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DTzBmOqN.js → kanban-definition-27J2QSJJ-DkmTDctP.js} +1 -1
- package/dist/worker/console/static/assets/{linear-Cpy1d873.js → linear-C1PpUkLo.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C1cSavOy.js → mermaid.core-BXExQJbm.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BEBSKvYM.js → mindmap-definition-FAOFIHXS-gUlCo9JV.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BrLs4Oac.js → pegDiagram-VL7TDLO6-JoegdCzT.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B47AZcLa.js → pieDiagram-7S7Q4E2Y-CrJnvJe_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-s0dVeYR2.js → quadrantDiagram-CIZ2JOQS-BHZqrxkB.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-lW8aJ7Q6.js → railroadDiagram-AXF67PYL-D2uwo4iG.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BcHJPf6Q.js → requirementDiagram-LRYGKXZP-Cjg0rXAB.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DdegpUUf.js → sankeyDiagram-W5VNT64P-0VbDOPz_.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BxFWqNv8.js → sequenceDiagram-SI44F4Z6-S2UAiI-8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-cOdUDPbR.js → sizeCapture-X5ZJPWSS-DQYI8pSg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DpxlYl8P.js → stateDiagram-OKZ733FA-EIDdI0hz.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BF-RJ1_H.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DQOfurE0.js → swimlanes-SLNWSIFB-CfukQalg.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-NswIVCtn.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CIp3tC6V.js → timeline-definition-Z64GVDOM-BOQ3dOWp.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C-adIPKn.js → vennDiagram-T6HMQDX7-ChhOpX0T.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CiYrar9M.js → wardleyDiagram-T6FBY63Y-BVYvTZDn.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WHrdTV0-.js → xychartDiagram-ELKLHX3M-CPRbN4R2.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observe/static/views/failures.js +5 -2
- 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/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -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 +179 -25
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +146 -25
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +23 -19
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-DMvu1nmi.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +0 -1
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +0 -8
|
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
4
4
|
export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
|
|
5
|
+
/**
|
|
6
|
+
* Shared marker for a project that exposes no usable verification command at
|
|
7
|
+
* generation time. The marker command runs (and logs not-run honestly), but it
|
|
8
|
+
* must never count as successful verification evidence: labels carrying this
|
|
9
|
+
* text are excluded from successful evidence and targets bound to it fail the
|
|
10
|
+
* trace gate instead of passing it.
|
|
11
|
+
*/
|
|
12
|
+
export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
|
|
13
|
+
export function isFrontendNoVerificationMarkerLabel(label) {
|
|
14
|
+
return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
|
|
15
|
+
}
|
|
16
|
+
export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
|
|
5
17
|
async function readJsonFile(filePath) {
|
|
6
18
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
7
19
|
}
|
|
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
|
|
|
52
64
|
const hardIssues = [];
|
|
53
65
|
for (const target of input.contract.verificationTargets) {
|
|
54
66
|
const issues = [];
|
|
55
|
-
|
|
67
|
+
if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
|
|
68
|
+
// A target bound to the not-run marker has no verification behind it;
|
|
69
|
+
// passing it would let the run finish green without any check.
|
|
70
|
+
issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
|
|
71
|
+
}
|
|
72
|
+
const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
|
|
73
|
+
? []
|
|
74
|
+
: (input.labelOwners.get(target.commandLabel) ?? []);
|
|
56
75
|
if (!owners?.length) {
|
|
57
76
|
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
58
77
|
}
|
|
@@ -189,11 +208,17 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
189
208
|
throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
|
|
190
209
|
}
|
|
191
210
|
if (input.evidence) {
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
// The no-op not-run marker never counts as successful verification
|
|
212
|
+
// evidence, even if it executed with exit code 0.
|
|
213
|
+
const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
214
|
+
const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
215
|
+
if (effectiveStaticLabels.length === 0) {
|
|
216
|
+
throw new Error(input.evidence.static.commandLabels.length > 0
|
|
217
|
+
? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
|
|
218
|
+
: "trace: no successful static verification commands in current run");
|
|
194
219
|
}
|
|
195
220
|
if (requiresBehaviorVerification &&
|
|
196
|
-
|
|
221
|
+
effectiveBehaviorLabels.length === 0) {
|
|
197
222
|
throw new Error("trace: no successful behavior verification commands in current run");
|
|
198
223
|
}
|
|
199
224
|
}
|
|
@@ -28,12 +28,14 @@ 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";
|
|
34
35
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
35
36
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
36
37
|
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
38
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
37
39
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
38
40
|
const REQUIREMENT_FILE = "需求.md";
|
|
39
41
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -1281,11 +1283,15 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
1281
1283
|
: allowed,
|
|
1282
1284
|
};
|
|
1283
1285
|
}
|
|
1286
|
+
const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
|
|
1284
1287
|
/**
|
|
1285
1288
|
* Resolve frontend verification fallbacks from the target project's own
|
|
1286
1289
|
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1290
|
+
* repoRoot, so those fixtures retain the historical generic fallback. A real
|
|
1291
|
+
* project (repoRoot set) without a readable package.json has no scripts to
|
|
1292
|
+
* invoke; the generic fallback would only inject commands guaranteed to fail
|
|
1293
|
+
* at verify time, so such projects fall through to the tsc probe and may end
|
|
1294
|
+
* up with no fallback commands plus a generation-time advisory.
|
|
1289
1295
|
*/
|
|
1290
1296
|
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
1291
1297
|
const genericFallback = {
|
|
@@ -1302,10 +1308,8 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1302
1308
|
}
|
|
1303
1309
|
}
|
|
1304
1310
|
catch {
|
|
1305
|
-
|
|
1311
|
+
// No readable manifest: leave scripts unset and keep probing.
|
|
1306
1312
|
}
|
|
1307
|
-
if (!scripts)
|
|
1308
|
-
return genericFallback;
|
|
1309
1313
|
let packageManager = "npm";
|
|
1310
1314
|
for (const [lockfile, manager] of [
|
|
1311
1315
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -1382,19 +1386,66 @@ function toDagSourcePath(sources, absolutePath) {
|
|
|
1382
1386
|
absolutePath,
|
|
1383
1387
|
});
|
|
1384
1388
|
}
|
|
1385
|
-
|
|
1389
|
+
/**
|
|
1390
|
+
* Declaration-line shapes that assert a requirement id: list items
|
|
1391
|
+
* ("- AC-1: ..."), numbered/ordered acceptance entries ("1. AC-1 ...",
|
|
1392
|
+
* "1、AC-1"), table rows ("| AC-1 | ..."), and explicit key/value or
|
|
1393
|
+
* heading declarations ("AC-1:...", "id: AC-1"). Ids that merely appear
|
|
1394
|
+
* inside narrative prose (e.g. a fixture run id mentioned in background:
|
|
1395
|
+
* "夹具 run 2026-08-23-req-f03535aa 已存在") are incidental mentions, not
|
|
1396
|
+
* declared requirements, and must not pollute sourceBinding.requirementIds.
|
|
1397
|
+
*/
|
|
1398
|
+
const REQUIREMENT_ID_DECLARATION_LINE = /^\s*(?:[-+*]\s+|\d+[.、))]\s*|\|\s*)?(?:[-A-Z0-9]+\s*[::]\s*)?(?:\*\*)?(?:REQ|BR|AC)-/i;
|
|
1399
|
+
function isRequirementIdDeclarationLine(line) {
|
|
1400
|
+
return REQUIREMENT_ID_DECLARATION_LINE.test(line);
|
|
1401
|
+
}
|
|
1402
|
+
function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
|
|
1386
1403
|
const ids = [];
|
|
1387
1404
|
const seen = new Set();
|
|
1388
|
-
|
|
1405
|
+
const collect = (markdown, declarationsOnly) => {
|
|
1389
1406
|
if (!markdown)
|
|
1390
|
-
|
|
1391
|
-
|
|
1407
|
+
return;
|
|
1408
|
+
const source = declarationsOnly
|
|
1409
|
+
? markdown
|
|
1410
|
+
.split(/\r?\n/)
|
|
1411
|
+
.filter((line) => isRequirementIdDeclarationLine(line))
|
|
1412
|
+
.join("\n")
|
|
1413
|
+
: markdown;
|
|
1414
|
+
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1392
1415
|
const id = match[0].toUpperCase();
|
|
1393
1416
|
if (!seen.has(id)) {
|
|
1394
1417
|
seen.add(id);
|
|
1395
1418
|
ids.push(id);
|
|
1396
1419
|
}
|
|
1397
1420
|
}
|
|
1421
|
+
};
|
|
1422
|
+
// The requirement document itself is human-authored acceptance prose:
|
|
1423
|
+
// only declaration lines declare requirements there. If the document has
|
|
1424
|
+
// no declaration-shaped lines at all (minimal free-form tasks like
|
|
1425
|
+
// "covers AC-001"), fall back to its full text so bindings never go
|
|
1426
|
+
// missing for unstructured input. References and constraints are bound
|
|
1427
|
+
// documents (acceptance yaml, analysis docs) whose ids are authoritative
|
|
1428
|
+
// wherever they appear — keep full scan for them.
|
|
1429
|
+
const declarationLines = requirementMarkdown
|
|
1430
|
+
.split(/\r?\n/)
|
|
1431
|
+
.filter((line) => isRequirementIdDeclarationLine(line));
|
|
1432
|
+
collect(requirementMarkdown, declarationLines.length > 0);
|
|
1433
|
+
for (const markdown of fallbackMarkdown) {
|
|
1434
|
+
if (markdown)
|
|
1435
|
+
collect(markdown, false);
|
|
1436
|
+
}
|
|
1437
|
+
return ids;
|
|
1438
|
+
}
|
|
1439
|
+
/** Extract ids from an explicit authoritative section (full scan inside it). */
|
|
1440
|
+
function extractSectionRequirementIds(sectionMarkdown) {
|
|
1441
|
+
const ids = [];
|
|
1442
|
+
const seen = new Set();
|
|
1443
|
+
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1444
|
+
const id = match[0].toUpperCase();
|
|
1445
|
+
if (!seen.has(id)) {
|
|
1446
|
+
seen.add(id);
|
|
1447
|
+
ids.push(id);
|
|
1448
|
+
}
|
|
1398
1449
|
}
|
|
1399
1450
|
return ids;
|
|
1400
1451
|
}
|
|
@@ -1408,7 +1459,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1408
1459
|
// Without it, preserve legacy full-scan behavior (requirement + references).
|
|
1409
1460
|
const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
|
|
1410
1461
|
if (sectionMatch?.[1]) {
|
|
1411
|
-
const fromSection =
|
|
1462
|
+
const fromSection = extractSectionRequirementIds(sectionMatch[1]);
|
|
1412
1463
|
if (fromSection.length > 0)
|
|
1413
1464
|
return fromSection;
|
|
1414
1465
|
}
|
|
@@ -2530,7 +2581,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2530
2581
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2531
2582
|
}
|
|
2532
2583
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2533
|
-
|
|
2584
|
+
// The verification bundle schema requires at least one static command. When
|
|
2585
|
+
// a real project exposes no usable verification command at all, run an
|
|
2586
|
+
// explicit no-op marker instead of a command that is guaranteed to fail:
|
|
2587
|
+
// the trace then records not-run honestly and the advisory asks the task
|
|
2588
|
+
// to declare verification commands and regenerate.
|
|
2589
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
|
|
2590
|
+
? fallbackVerifyCommands.staticCommands
|
|
2591
|
+
: [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
|
|
2534
2592
|
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
2535
2593
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
2536
2594
|
repoRoot: sources.repoRoot,
|
|
@@ -2629,6 +2687,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2629
2687
|
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2630
2688
|
].join("\n");
|
|
2631
2689
|
const advisories = [];
|
|
2690
|
+
if (!hasDeclaredFrontendVerification &&
|
|
2691
|
+
fallbackVerifyCommands.staticCommands.length === 0 &&
|
|
2692
|
+
fallbackVerifyCommands.behaviorCommands.length === 0) {
|
|
2693
|
+
advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
|
|
2694
|
+
}
|
|
2632
2695
|
if (frontendSourceMentionsMock(frontendSources) &&
|
|
2633
2696
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2634
2697
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
@@ -4736,11 +4799,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
|
4736
4799
|
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4737
4800
|
spec.backendTestLayout = layout;
|
|
4738
4801
|
}
|
|
4802
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
4803
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
4804
|
+
if (layout.isDefault) {
|
|
4805
|
+
spec.frontendTestLayout = layout;
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
4809
|
+
for (const task of spec.tasks) {
|
|
4810
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4811
|
+
if (task.outputContract)
|
|
4812
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4813
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4814
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4815
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4816
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4817
|
+
const child = task.dynamicExpansion.childTask;
|
|
4818
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4819
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4820
|
+
}
|
|
4821
|
+
if (typeof child.outputContract === "string") {
|
|
4822
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4823
|
+
}
|
|
4824
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4825
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4826
|
+
}
|
|
4827
|
+
if (Array.isArray(child.writeSet)) {
|
|
4828
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4829
|
+
}
|
|
4830
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4831
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
if (task.shell?.commands) {
|
|
4835
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4839
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4840
|
+
spec.frontendTestLayout = layout;
|
|
4841
|
+
}
|
|
4842
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
4843
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
4844
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
4845
|
+
pathMatchesPattern(probe, pattern));
|
|
4846
|
+
}
|
|
4739
4847
|
// ---------------------------------------------------------------------------
|
|
4740
4848
|
// Frontend browser-test RAG DAG template
|
|
4741
4849
|
// ---------------------------------------------------------------------------
|
|
4742
4850
|
function buildFrontendTestHybridDag(sources) {
|
|
4743
4851
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
4852
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4744
4853
|
const config = {
|
|
4745
4854
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4746
4855
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4762,28 +4871,32 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4762
4871
|
};
|
|
4763
4872
|
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
4764
4873
|
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
4874
|
+
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4875
|
+
// exists so ledger-less projects keep generating without noise.
|
|
4876
|
+
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4877
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
4878
|
+
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4879
|
+
? `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.`
|
|
4880
|
+
: "";
|
|
4765
4881
|
const reviewMode = config.reviewMode;
|
|
4766
4882
|
const blockingReview = reviewMode === "blocking";
|
|
4767
4883
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
4768
4884
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4769
4885
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4770
4886
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
pattern === "**");
|
|
4774
|
-
if (!hasFrontendTestWriteScope) {
|
|
4775
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
4887
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
4888
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4776
4889
|
}
|
|
4777
4890
|
const forbidden = commonForbiddenPaths(sources);
|
|
4778
|
-
const ragWriteSet = [
|
|
4891
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4779
4892
|
const caseDraftWriteSet = [
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4893
|
+
`${layout.casesDir}/FE-*.md`,
|
|
4894
|
+
`${layout.casesDir}/index.md`,
|
|
4895
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4783
4896
|
];
|
|
4784
4897
|
// The shell materializer alone owns the final manifest boundary.
|
|
4785
|
-
const casesWriteSet = [
|
|
4786
|
-
const evidenceRoot =
|
|
4898
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
4899
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4787
4900
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4788
4901
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4789
4902
|
const checklistScript = [
|
|
@@ -4802,6 +4915,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4802
4915
|
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
4803
4916
|
,
|
|
4804
4917
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4918
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4919
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4805
4920
|
"for(const c of manifest.cases){",
|
|
4806
4921
|
" const id=c&&c.caseId||'?';",
|
|
4807
4922
|
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
@@ -4820,6 +4935,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4820
4935
|
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
4821
4936
|
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
4822
4937
|
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
4938
|
+
" if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))){issues.push({ruleId:'ac-id-truncated',caseId:id,detail:ac+' is a truncated acceptance id (missing feature prefix or sequence number)'});continue;}",
|
|
4823
4939
|
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
4824
4940
|
" }",
|
|
4825
4941
|
" }",
|
|
@@ -4851,6 +4967,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4851
4967
|
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
4852
4968
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
4853
4969
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4970
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4971
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4854
4972
|
"for(const c of manifest.cases){",
|
|
4855
4973
|
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)) fail('case-id-shape','caseId must be FE-*, never AC-FE-*: '+String(c&&c.caseId));",
|
|
4856
4974
|
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
@@ -4858,7 +4976,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4858
4976
|
" seen.add(c.caseId);",
|
|
4859
4977
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
4860
4978
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
4861
|
-
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4979
|
+
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4862
4980
|
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
4863
4981
|
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
4864
4982
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
@@ -4983,6 +5101,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4983
5101
|
subtask_prompt: [
|
|
4984
5102
|
"Use skill playwright-cli-case-generator.",
|
|
4985
5103
|
"Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
|
|
5104
|
+
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
4986
5105
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
4987
5106
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
4988
5107
|
"HARD ID CONTRACT (do not confuse these):",
|
|
@@ -5142,7 +5261,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5142
5261
|
subtaskPromptTemplate: [
|
|
5143
5262
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
5144
5263
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
5145
|
-
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5264
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5146
5265
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
5147
5266
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
5148
5267
|
].join("\n\n"),
|
|
@@ -5165,7 +5284,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5165
5284
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
5166
5285
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
5167
5286
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
5168
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5287
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5169
5288
|
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
5170
5289
|
].join("");
|
|
5171
5290
|
tasks.push({
|
|
@@ -5233,6 +5352,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5233
5352
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
5234
5353
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
5235
5354
|
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
5355
|
+
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
5236
5356
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
5237
5357
|
].join("\n\n"),
|
|
5238
5358
|
},
|
|
@@ -5368,6 +5488,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5368
5488
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5369
5489
|
tasks,
|
|
5370
5490
|
};
|
|
5491
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
5371
5492
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
5372
5493
|
parseDagSpec(spec);
|
|
5373
5494
|
assertValidDagSpec(spec);
|
|
@@ -978,6 +978,18 @@ export const dagSpecSchema = z
|
|
|
978
978
|
isDefault: z.boolean(),
|
|
979
979
|
})
|
|
980
980
|
.optional(),
|
|
981
|
+
/** Frozen frontend-test artifact layout; absent = historical testcase/frontend/ layout. */
|
|
982
|
+
frontendTestLayout: z
|
|
983
|
+
.object({
|
|
984
|
+
testRoot: z.string(),
|
|
985
|
+
ragDir: z.string(),
|
|
986
|
+
casesDir: z.string(),
|
|
987
|
+
evidenceDir: z.string(),
|
|
988
|
+
reportsDir: z.string(),
|
|
989
|
+
fixturesDir: z.string(),
|
|
990
|
+
isDefault: z.boolean(),
|
|
991
|
+
})
|
|
992
|
+
.optional(),
|
|
981
993
|
/** Bound user shared-setup document (plan B); absent = no shared setup references expected. */
|
|
982
994
|
backendTestSharedSetup: z
|
|
983
995
|
.object({
|
|
@@ -5,6 +5,7 @@ import { resolveRepairTaskForGate } from "./repair-artifact.js";
|
|
|
5
5
|
import { topoSortToRanks } from "./topo.js";
|
|
6
6
|
import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./retry-policy.js";
|
|
7
7
|
import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
|
|
8
|
+
import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
|
|
8
9
|
const GOVERNANCE_WARNING_TYPES = new Set([
|
|
9
10
|
"read-only-missing-artifacts-forbidden",
|
|
10
11
|
"read-only-prompt-mentions-artifact-writes",
|
|
@@ -680,22 +681,23 @@ function validateWriterOutcomePolicyTaskConfig(task, issues) {
|
|
|
680
681
|
});
|
|
681
682
|
}
|
|
682
683
|
}
|
|
683
|
-
function isFrontendEvidenceWriteSet(writeSet) {
|
|
684
|
+
function isFrontendEvidenceWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
|
|
684
685
|
const entries = writeSet ?? [];
|
|
685
686
|
if (entries.length === 0)
|
|
686
687
|
return false;
|
|
688
|
+
const prefix = `${layout.evidenceDir}/`;
|
|
689
|
+
const glob = `${layout.evidenceDir}/**`;
|
|
687
690
|
return entries.every((entry) => {
|
|
688
691
|
const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
689
|
-
return
|
|
690
|
-
normalized.startsWith("testcase/frontend/evidence/"));
|
|
692
|
+
return normalized === glob || normalized.startsWith(prefix);
|
|
691
693
|
});
|
|
692
694
|
}
|
|
693
|
-
function isFrontendBrowserExecutorTask(task) {
|
|
695
|
+
function isFrontendBrowserExecutorTask(task, layout = defaultFrontendTestLayout()) {
|
|
694
696
|
const skills = task.skills ?? [];
|
|
695
697
|
return (task.executor === "pi" &&
|
|
696
698
|
task.toolProfile === "write" &&
|
|
697
699
|
skills.includes("playwright-cli") &&
|
|
698
|
-
isFrontendEvidenceWriteSet(task.writeSet));
|
|
700
|
+
isFrontendEvidenceWriteSet(task.writeSet, layout));
|
|
699
701
|
}
|
|
700
702
|
function taskMentionsPlaywrightCliContract(task) {
|
|
701
703
|
const prompt = `${task.subtask_prompt}\n${task.outputContract ?? ""}`;
|
|
@@ -707,10 +709,11 @@ function taskMentionsPlaywrightCliContract(task) {
|
|
|
707
709
|
* - capability is granted without skill/prompt contract or outside evidence writeSet
|
|
708
710
|
* - non-browser nodes receive command capability
|
|
709
711
|
*/
|
|
710
|
-
function validateCommandPolicyTaskConfig(task, issues) {
|
|
712
|
+
function validateCommandPolicyTaskConfig(task, issues, spec) {
|
|
713
|
+
const layout = frontendTestLayoutFromSpec(spec);
|
|
711
714
|
const policy = resolveDagCommandPolicy(task.commandPolicy);
|
|
712
715
|
const allowsPlaywright = dagCommandPolicyAllows(task.commandPolicy, "playwright-cli");
|
|
713
|
-
const isBrowserExecutor = isFrontendBrowserExecutorTask(task);
|
|
716
|
+
const isBrowserExecutor = isFrontendBrowserExecutorTask(task, layout);
|
|
714
717
|
if (isBrowserExecutor && !allowsPlaywright) {
|
|
715
718
|
issues.push({
|
|
716
719
|
type: "invalid-command-policy",
|
|
@@ -741,10 +744,10 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
741
744
|
message: `task ${task.id} playwright-cli command capability requires prompt/outputContract to reference playwright_cli`,
|
|
742
745
|
});
|
|
743
746
|
}
|
|
744
|
-
if (!isFrontendEvidenceWriteSet(task.writeSet)) {
|
|
747
|
+
if (!isFrontendEvidenceWriteSet(task.writeSet, layout)) {
|
|
745
748
|
issues.push({
|
|
746
749
|
type: "invalid-command-policy",
|
|
747
|
-
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under
|
|
750
|
+
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under ${layout.evidenceDir}/`,
|
|
748
751
|
});
|
|
749
752
|
}
|
|
750
753
|
if (task.toolProfile !== "write") {
|
|
@@ -755,7 +758,7 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
755
758
|
}
|
|
756
759
|
}
|
|
757
760
|
}
|
|
758
|
-
function validateDynamicChildCommandPolicy(task, issues) {
|
|
761
|
+
function validateDynamicChildCommandPolicy(task, issues, spec) {
|
|
759
762
|
const expansion = task.dynamicExpansion;
|
|
760
763
|
if (!expansion)
|
|
761
764
|
return;
|
|
@@ -776,7 +779,7 @@ function validateDynamicChildCommandPolicy(task, issues) {
|
|
|
776
779
|
writeSet: child.writeSet,
|
|
777
780
|
outputContract: child.outputContract,
|
|
778
781
|
};
|
|
779
|
-
validateCommandPolicyTaskConfig(synthetic, issues);
|
|
782
|
+
validateCommandPolicyTaskConfig(synthetic, issues, spec);
|
|
780
783
|
}
|
|
781
784
|
function validateDecisionGateTaskConfig(task, issues) {
|
|
782
785
|
if (!task.decisionGate?.enabled) {
|
|
@@ -883,12 +886,13 @@ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
|
|
|
883
886
|
}));
|
|
884
887
|
}
|
|
885
888
|
/** Revalidate a rendered dynamic child before it enters state or executes. */
|
|
886
|
-
export function assertValidMaterializedDagTask(task) {
|
|
889
|
+
export function assertValidMaterializedDagTask(task, parentSpec) {
|
|
887
890
|
const issues = [];
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
891
|
+
const spec = {
|
|
892
|
+
version: 2,
|
|
893
|
+
tasks: [task],
|
|
894
|
+
frontendTestLayout: parentSpec?.frontendTestLayout,
|
|
895
|
+
};
|
|
892
896
|
validateTaskWritePolicy(task, spec, issues);
|
|
893
897
|
validateShellTaskConfig(task, spec, issues);
|
|
894
898
|
validateStaticTaskConfig(task, issues);
|
|
@@ -896,7 +900,7 @@ export function assertValidMaterializedDagTask(task) {
|
|
|
896
900
|
validateRetryPolicyTaskConfig(task, issues);
|
|
897
901
|
validateOutputProtocolTaskConfig(task, issues);
|
|
898
902
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
899
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
903
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
900
904
|
if (issues.length > 0) {
|
|
901
905
|
throw new Error(`invalid materialized dynamic child ${task.id}: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
902
906
|
}
|
|
@@ -952,8 +956,8 @@ export function validateDagSpec(spec) {
|
|
|
952
956
|
validateRetryPolicyTaskConfig(task, issues);
|
|
953
957
|
validateOutputProtocolTaskConfig(task, issues);
|
|
954
958
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
955
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
956
|
-
validateDynamicChildCommandPolicy(task, issues);
|
|
959
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
960
|
+
validateDynamicChildCommandPolicy(task, issues, spec);
|
|
957
961
|
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
958
962
|
validatePiExtensionsTaskConfig(task, issues);
|
|
959
963
|
validateFailureAwareDependsOn(task, spec, issues);
|
|
@@ -15,7 +15,7 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
|
|
|
15
15
|
| `unknown-ac` | When task `sourceBinding.requirementIds` lists ACs, every `acIds` entry must be in that set |
|
|
16
16
|
| `case-id-shape` | `caseId` matches `FE-<FEATURE>-<NNN>-...` (never `AC-FE-*`) |
|
|
17
17
|
| `case-id-is-ac` | Do not use acceptance id as `caseId` / filename |
|
|
18
|
-
| `case-path-mismatch` | `casePath === testcase/frontend/cases/{caseId}.md
|
|
18
|
+
| `case-path-mismatch` | `casePath === {casesDir}/{caseId}.md`(默认 `testcase/frontend/cases/{caseId}.md`) |
|
|
19
19
|
| `case-file-missing` | `casePath` exists |
|
|
20
20
|
|
|
21
21
|
## Tool guidance (non-blocking)
|
|
@@ -40,5 +40,5 @@ LLM review (when `frontendTest.reviewMode=blocking`) must not invent blocking ru
|
|
|
40
40
|
|
|
41
41
|
## Pipeline vs quality
|
|
42
42
|
|
|
43
|
-
- **Pipeline acceptance**: final `
|
|
43
|
+
- **Pipeline acceptance**: final `{reportsDir}/frontend-test-retrospect-*.md` exists after result materialize (default `testcase/frontend/reports/`)
|
|
44
44
|
- **Quality**: `frontend-test-result-v1.outcome=passed` with 0 blocked/failed (opt-in via `frontendTest.strictOutcomeGate`)
|
package/harness.json
CHANGED
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
"executors": {
|
|
83
83
|
"pi": {
|
|
84
84
|
"description": "Pi 负责规划、评审、诊断;当 DAG toolProfile=write 时也可做有界写入。模型按复杂度三档配置,支持 provider/model 字符串或带 thinking 的对象;斜杠前为 Pi provider,后为 modelId,勿只写裸 modelId。",
|
|
85
|
-
"LOW": "
|
|
86
|
-
"MED": "
|
|
87
|
-
"HIGH": "
|
|
85
|
+
"LOW": "deepseek/deepseek-v4-flash",
|
|
86
|
+
"MED": "deepseek/deepseek-v4-flash",
|
|
87
|
+
"HIGH": "deepseek/deepseek-v4-flash"
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|