@tea-agent/loop-agent 0.39.0-beta.4 → 0.39.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +167 -31
- package/dist/executors/pi-playwright-cli-tool.js +23 -16
- package/dist/executors/shell-executor.js +25 -8
- package/dist/shared/dag-failure-category.js +138 -0
- package/dist/task/config-types.js +6 -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-CfDSkLtp.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BlefM6I5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-C4tHfzHn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-G-G77Rh1.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-DWgxmY72.js} +1 -1
- package/dist/worker/console/static/assets/channel-DsdX3ZUX.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-CJQcfbCY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-vUPviH9y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-DzRXSTeh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-CWfgqm07.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-BtTDauXA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-Bc-NNt84.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-B_f-emL4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-DV-jgJ66.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-DMqzn4RZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-CVNtlAm4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CrvCyJ-8.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CrvCyJ-8.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BBZBhVTd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-DbMU2PZ2.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Dj0v8rjc.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-BW8R4WvJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-DEgWz3ld.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-BAA01rlR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Cau6qqpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-8HiPsLos.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-CmkXuTYL.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DCV_0seW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-gckfWESj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-D2mT2BUu.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-B_HR5qEw.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-BUw4fpSa.js} +1 -1
- package/dist/worker/console/static/assets/{index-D5zGX6fG.js → index-DPbsruV5.js} +81 -79
- package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BSRAqEj0.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-CLxE8hUp.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-juFzVXIj.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-CwLHVhOU.js} +1 -1
- package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-9LfUyPAs.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-TPPHfQbe.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-Dn_Z9RxF.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CiEMKTgm.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-JASaGxjZ.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-ChmH0ply.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-CtyM-HGQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-BN9y_Bvs.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-IyS0GQ1-.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DjxGXurN.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-Cr3Z0S8y.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-BjJFC6pW.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bq44O9Nt.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-DeMUJQiW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BT4tWyW3.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-Bc2Rv2Oo.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-CDhRUeqG.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DaN_z3Xy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-C1vTLBDG.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/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 +12 -7
- package/dist/workflows/dag/frontend-implementation-contract.js +10 -5
- package/dist/workflows/dag/frontend-plan-render.js +0 -3
- package/dist/workflows/dag/frontend-shadow-dual-write.js +51 -10
- 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/frontend-typed-event-store.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +66 -19
- package/dist/workflows/dag/node-execution.js +21 -1
- package/dist/workflows/dag/retry-policy.js +32 -0
- 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/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-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
|
@@ -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),
|
|
@@ -113,6 +113,18 @@ export const contractBlockingOwnerSchema = z.enum([
|
|
|
113
113
|
* `finalize_plan` terminal fact.
|
|
114
114
|
*/
|
|
115
115
|
export const PLAN_TERMINAL_FACT_KINDS = ["finalize_plan"];
|
|
116
|
+
/**
|
|
117
|
+
* A+B: incremental payload records for `frontend-plan-pi` (requirements,
|
|
118
|
+
* verification targets, evidence gaps). Each is committed by one
|
|
119
|
+
* `record_plan_*` call so a large plan never exceeds a single model output
|
|
120
|
+
* budget; `assemblePlanPatchFromCommittedFacts` aggregates them from the
|
|
121
|
+
* ledger in commit order.
|
|
122
|
+
*/
|
|
123
|
+
export const PLAN_RECORD_FACT_KINDS = [
|
|
124
|
+
"plan-requirement",
|
|
125
|
+
"plan-verification-target",
|
|
126
|
+
"plan-evidence-gap",
|
|
127
|
+
];
|
|
116
128
|
/**
|
|
117
129
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
118
130
|
* requests. The five-value enum replaces free-form issueCategory strings.
|
|
@@ -143,6 +155,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
|
|
|
143
155
|
...FRONTEND_SHAPE_FACT_KINDS,
|
|
144
156
|
...CONTRACT_FACT_KINDS,
|
|
145
157
|
...PLAN_TERMINAL_FACT_KINDS,
|
|
158
|
+
...PLAN_RECORD_FACT_KINDS,
|
|
146
159
|
]),
|
|
147
160
|
];
|
|
148
161
|
export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
|
|
@@ -11,7 +11,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
11
11
|
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
|
|
12
12
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
13
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
14
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
15
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
16
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -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";
|
|
@@ -2651,7 +2652,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2651
2652
|
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2652
2653
|
"",
|
|
2653
2654
|
"### optional plan fields - GOOD (all optional; omit when absent):",
|
|
2654
|
-
'{"
|
|
2655
|
+
'{"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
|
|
2655
2656
|
"",
|
|
2656
2657
|
"### optional plan fields - BAD (present-but-empty strings are rejected):",
|
|
2657
2658
|
'{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
|
|
@@ -2676,8 +2677,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2676
2677
|
"- targets: files, routes, publicApiChanges",
|
|
2677
2678
|
"- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
|
|
2678
2679
|
"- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
|
|
2679
|
-
"- designEvidence: source, paths, conflicts; evidenceGaps[]",
|
|
2680
|
-
"- optional:
|
|
2680
|
+
"- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
|
|
2681
|
+
"- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
|
|
2681
2682
|
"- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
|
|
2682
2683
|
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2683
2684
|
].join("\n");
|
|
@@ -2982,13 +2983,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2982
2983
|
retryOnInvalid: true,
|
|
2983
2984
|
skeleton: frontendContractSkeleton,
|
|
2984
2985
|
},
|
|
2985
|
-
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) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from committed facts (requirements / verificationTargets / evidenceGaps /
|
|
2986
|
+
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
2987
|
subtask_prompt: [
|
|
2987
2988
|
"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.",
|
|
2989
|
+
"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.",
|
|
2988
2990
|
...(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."] : []),
|
|
2989
2991
|
"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.",
|
|
2990
2992
|
"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.",
|
|
2991
|
-
"Encode
|
|
2993
|
+
"Encode target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the ledger payload. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2992
2994
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2993
2995
|
"Consume the Scout target surface and design evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2994
2996
|
"Output a short Markdown narrative, record the incremental facts, then call finalize_plan exactly once. Do NOT emit protected skeleton fields or a full contract as the authority.",
|
|
@@ -3234,6 +3236,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3234
3236
|
executor: "pi",
|
|
3235
3237
|
complexity: "HIGH",
|
|
3236
3238
|
writePolicy: "read-only",
|
|
3239
|
+
retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
|
|
3237
3240
|
allowedPaths: readOnlyPaths,
|
|
3238
3241
|
forbiddenPaths,
|
|
3239
3242
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
@@ -4860,11 +4863,57 @@ function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
|
4860
4863
|
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4861
4864
|
spec.backendTestLayout = layout;
|
|
4862
4865
|
}
|
|
4866
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
4867
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
4868
|
+
if (layout.isDefault) {
|
|
4869
|
+
spec.frontendTestLayout = layout;
|
|
4870
|
+
return;
|
|
4871
|
+
}
|
|
4872
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
4873
|
+
for (const task of spec.tasks) {
|
|
4874
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4875
|
+
if (task.outputContract)
|
|
4876
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4877
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4878
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4879
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4880
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4881
|
+
const child = task.dynamicExpansion.childTask;
|
|
4882
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4883
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4884
|
+
}
|
|
4885
|
+
if (typeof child.outputContract === "string") {
|
|
4886
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4887
|
+
}
|
|
4888
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4889
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4890
|
+
}
|
|
4891
|
+
if (Array.isArray(child.writeSet)) {
|
|
4892
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4893
|
+
}
|
|
4894
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4895
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
if (task.shell?.commands) {
|
|
4899
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4903
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4904
|
+
spec.frontendTestLayout = layout;
|
|
4905
|
+
}
|
|
4906
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
4907
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
4908
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
4909
|
+
pathMatchesPattern(probe, pattern));
|
|
4910
|
+
}
|
|
4863
4911
|
// ---------------------------------------------------------------------------
|
|
4864
4912
|
// Frontend browser-test RAG DAG template
|
|
4865
4913
|
// ---------------------------------------------------------------------------
|
|
4866
4914
|
function buildFrontendTestHybridDag(sources) {
|
|
4867
4915
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
4916
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4868
4917
|
const config = {
|
|
4869
4918
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4870
4919
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4889,9 +4938,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4889
4938
|
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4890
4939
|
// exists so ledger-less projects keep generating without noise.
|
|
4891
4940
|
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4892
|
-
existsSync(path.join(sources.repoRoot, "
|
|
4941
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
4893
4942
|
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4894
|
-
?
|
|
4943
|
+
? `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.`
|
|
4895
4944
|
: "";
|
|
4896
4945
|
const reviewMode = config.reviewMode;
|
|
4897
4946
|
const blockingReview = reviewMode === "blocking";
|
|
@@ -4899,22 +4948,19 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4899
4948
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4900
4949
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4901
4950
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
pattern === "**");
|
|
4905
|
-
if (!hasFrontendTestWriteScope) {
|
|
4906
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
4951
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
4952
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4907
4953
|
}
|
|
4908
4954
|
const forbidden = commonForbiddenPaths(sources);
|
|
4909
|
-
const ragWriteSet = [
|
|
4955
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4910
4956
|
const caseDraftWriteSet = [
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4957
|
+
`${layout.casesDir}/FE-*.md`,
|
|
4958
|
+
`${layout.casesDir}/index.md`,
|
|
4959
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4914
4960
|
];
|
|
4915
4961
|
// The shell materializer alone owns the final manifest boundary.
|
|
4916
|
-
const casesWriteSet = [
|
|
4917
|
-
const evidenceRoot =
|
|
4962
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
4963
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4918
4964
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4919
4965
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4920
4966
|
const checklistScript = [
|
|
@@ -5506,6 +5552,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5506
5552
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5507
5553
|
tasks,
|
|
5508
5554
|
};
|
|
5555
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
5509
5556
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
5510
5557
|
parseDagSpec(spec);
|
|
5511
5558
|
assertValidDagSpec(spec);
|
|
@@ -228,6 +228,26 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
228
228
|
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
229
229
|
].join("\n");
|
|
230
230
|
}
|
|
231
|
+
if (previousFailureCategory === "review-terminal-missing") {
|
|
232
|
+
return [
|
|
233
|
+
basePrompt,
|
|
234
|
+
"",
|
|
235
|
+
"<retry_instruction>",
|
|
236
|
+
"The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
|
|
237
|
+
"Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
|
|
238
|
+
"</retry_instruction>",
|
|
239
|
+
].join("\n");
|
|
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
|
+
}
|
|
231
251
|
if (previousFailureCategory === "invalid-output" &&
|
|
232
252
|
task.structuredContractOutput &&
|
|
233
253
|
previousProtocolReason) {
|
|
@@ -269,7 +289,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
269
289
|
"",
|
|
270
290
|
"<retry_instruction>",
|
|
271
291
|
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
272
|
-
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
|
|
292
|
+
"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.",
|
|
273
293
|
"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.",
|
|
274
294
|
"</retry_instruction>",
|
|
275
295
|
].join("\n");
|
|
@@ -25,6 +25,21 @@ export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
|
|
|
25
25
|
export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
|
|
26
26
|
/** Provider stopReason=length truncated the response before the JSON contract completed. */
|
|
27
27
|
export const STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY = "structured-output-truncated";
|
|
28
|
+
/**
|
|
29
|
+
* The review node emitted a verdict in response text (e.g. "VERDICT: pass")
|
|
30
|
+
* but never committed the authoritative typed terminal tool call
|
|
31
|
+
* (approve_review / request_review_changes). Read-only and safe to retry with
|
|
32
|
+
* a corrected instruction; the typed terminal is the only authority.
|
|
33
|
+
*/
|
|
34
|
+
export const REVIEW_TERMINAL_MISSING_RETRY_CATEGORY = "review-terminal-missing";
|
|
35
|
+
/**
|
|
36
|
+
* The plan node issued too many read-only tool calls (read/grep/ls/find),
|
|
37
|
+
* blowing up the context window and eventually a 400 request-too-large. The
|
|
38
|
+
* plan should trust upstream typed facts instead of re-reading contract/scout
|
|
39
|
+
* outputs and source files repeatedly. Read-only and safe to retry with a
|
|
40
|
+
* reduced-reading instruction.
|
|
41
|
+
*/
|
|
42
|
+
export const READ_BURST_RETRY_CATEGORY = "read-burst";
|
|
28
43
|
/** Retry only a proven no-op from an explicitly opt-in bounded Pi writer. */
|
|
29
44
|
export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
|
|
30
45
|
/** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
|
|
@@ -57,6 +72,8 @@ export const ALL_DAG_RETRY_CATEGORIES = [
|
|
|
57
72
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
58
73
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
59
74
|
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
75
|
+
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
76
|
+
READ_BURST_RETRY_CATEGORY,
|
|
60
77
|
];
|
|
61
78
|
const RETRY_SAFE_PI_ROLES = new Set([
|
|
62
79
|
"planner",
|
|
@@ -143,6 +160,20 @@ export const PROTOCOL_AWARE_PI_RETRY_POLICY = {
|
|
|
143
160
|
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
144
161
|
retryCategories: [...PROTOCOL_AWARE_DAG_RETRY_CATEGORIES],
|
|
145
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* frontend-review-pi retry policy. The review verdict must be committed
|
|
165
|
+
* through the typed terminal tools (approve_review / request_review_changes);
|
|
166
|
+
* a model that only echoes "VERDICT: pass" text fails as
|
|
167
|
+
* review-terminal-missing. That is read-only and safe to retry with a
|
|
168
|
+
* corrected instruction so a single model slip does not burn the whole run.
|
|
169
|
+
*/
|
|
170
|
+
export const FRONTEND_REVIEW_TERMINAL_RETRY_POLICY = {
|
|
171
|
+
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
172
|
+
retryCategories: [
|
|
173
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
174
|
+
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
175
|
+
],
|
|
176
|
+
};
|
|
146
177
|
/**
|
|
147
178
|
* The sole writer retry policy for requireChangedFiles writers. It is
|
|
148
179
|
* intentionally not included in any read-only default: a writer may retry only
|
|
@@ -501,5 +532,6 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
|
|
|
501
532
|
retryCategories: [
|
|
502
533
|
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
503
534
|
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
535
|
+
READ_BURST_RETRY_CATEGORY,
|
|
504
536
|
],
|
|
505
537
|
};
|
|
@@ -1159,6 +1159,18 @@ export const dagSpecSchema = z
|
|
|
1159
1159
|
isDefault: z.boolean(),
|
|
1160
1160
|
})
|
|
1161
1161
|
.optional(),
|
|
1162
|
+
/** Frozen frontend-test artifact layout; absent = historical testcase/frontend/ layout. */
|
|
1163
|
+
frontendTestLayout: z
|
|
1164
|
+
.object({
|
|
1165
|
+
testRoot: z.string(),
|
|
1166
|
+
ragDir: z.string(),
|
|
1167
|
+
casesDir: z.string(),
|
|
1168
|
+
evidenceDir: z.string(),
|
|
1169
|
+
reportsDir: z.string(),
|
|
1170
|
+
fixturesDir: z.string(),
|
|
1171
|
+
isDefault: z.boolean(),
|
|
1172
|
+
})
|
|
1173
|
+
.optional(),
|
|
1162
1174
|
/** Bound user shared-setup document (plan B); absent = no shared setup references expected. */
|
|
1163
1175
|
backendTestSharedSetup: z
|
|
1164
1176
|
.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",
|
|
@@ -690,22 +691,23 @@ function validateWriterOutcomePolicyTaskConfig(task, issues) {
|
|
|
690
691
|
});
|
|
691
692
|
}
|
|
692
693
|
}
|
|
693
|
-
function isFrontendEvidenceWriteSet(writeSet) {
|
|
694
|
+
function isFrontendEvidenceWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
|
|
694
695
|
const entries = writeSet ?? [];
|
|
695
696
|
if (entries.length === 0)
|
|
696
697
|
return false;
|
|
698
|
+
const prefix = `${layout.evidenceDir}/`;
|
|
699
|
+
const glob = `${layout.evidenceDir}/**`;
|
|
697
700
|
return entries.every((entry) => {
|
|
698
701
|
const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
699
|
-
return
|
|
700
|
-
normalized.startsWith("testcase/frontend/evidence/"));
|
|
702
|
+
return normalized === glob || normalized.startsWith(prefix);
|
|
701
703
|
});
|
|
702
704
|
}
|
|
703
|
-
function isFrontendBrowserExecutorTask(task) {
|
|
705
|
+
function isFrontendBrowserExecutorTask(task, layout = defaultFrontendTestLayout()) {
|
|
704
706
|
const skills = task.skills ?? [];
|
|
705
707
|
return (task.executor === "pi" &&
|
|
706
708
|
task.toolProfile === "write" &&
|
|
707
709
|
skills.includes("playwright-cli") &&
|
|
708
|
-
isFrontendEvidenceWriteSet(task.writeSet));
|
|
710
|
+
isFrontendEvidenceWriteSet(task.writeSet, layout));
|
|
709
711
|
}
|
|
710
712
|
function taskMentionsPlaywrightCliContract(task) {
|
|
711
713
|
const prompt = `${task.subtask_prompt}\n${task.outputContract ?? ""}`;
|
|
@@ -717,10 +719,11 @@ function taskMentionsPlaywrightCliContract(task) {
|
|
|
717
719
|
* - capability is granted without skill/prompt contract or outside evidence writeSet
|
|
718
720
|
* - non-browser nodes receive command capability
|
|
719
721
|
*/
|
|
720
|
-
function validateCommandPolicyTaskConfig(task, issues) {
|
|
722
|
+
function validateCommandPolicyTaskConfig(task, issues, spec) {
|
|
723
|
+
const layout = frontendTestLayoutFromSpec(spec);
|
|
721
724
|
const policy = resolveDagCommandPolicy(task.commandPolicy);
|
|
722
725
|
const allowsPlaywright = dagCommandPolicyAllows(task.commandPolicy, "playwright-cli");
|
|
723
|
-
const isBrowserExecutor = isFrontendBrowserExecutorTask(task);
|
|
726
|
+
const isBrowserExecutor = isFrontendBrowserExecutorTask(task, layout);
|
|
724
727
|
if (isBrowserExecutor && !allowsPlaywright) {
|
|
725
728
|
issues.push({
|
|
726
729
|
type: "invalid-command-policy",
|
|
@@ -751,10 +754,10 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
751
754
|
message: `task ${task.id} playwright-cli command capability requires prompt/outputContract to reference playwright_cli`,
|
|
752
755
|
});
|
|
753
756
|
}
|
|
754
|
-
if (!isFrontendEvidenceWriteSet(task.writeSet)) {
|
|
757
|
+
if (!isFrontendEvidenceWriteSet(task.writeSet, layout)) {
|
|
755
758
|
issues.push({
|
|
756
759
|
type: "invalid-command-policy",
|
|
757
|
-
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under
|
|
760
|
+
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under ${layout.evidenceDir}/`,
|
|
758
761
|
});
|
|
759
762
|
}
|
|
760
763
|
if (task.toolProfile !== "write") {
|
|
@@ -765,7 +768,7 @@ function validateCommandPolicyTaskConfig(task, issues) {
|
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
}
|
|
768
|
-
function validateDynamicChildCommandPolicy(task, issues) {
|
|
771
|
+
function validateDynamicChildCommandPolicy(task, issues, spec) {
|
|
769
772
|
const expansion = task.dynamicExpansion;
|
|
770
773
|
if (!expansion)
|
|
771
774
|
return;
|
|
@@ -786,7 +789,7 @@ function validateDynamicChildCommandPolicy(task, issues) {
|
|
|
786
789
|
writeSet: child.writeSet,
|
|
787
790
|
outputContract: child.outputContract,
|
|
788
791
|
};
|
|
789
|
-
validateCommandPolicyTaskConfig(synthetic, issues);
|
|
792
|
+
validateCommandPolicyTaskConfig(synthetic, issues, spec);
|
|
790
793
|
}
|
|
791
794
|
function validateDecisionGateTaskConfig(task, issues) {
|
|
792
795
|
if (!task.decisionGate?.enabled) {
|
|
@@ -893,12 +896,13 @@ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
|
|
|
893
896
|
}));
|
|
894
897
|
}
|
|
895
898
|
/** Revalidate a rendered dynamic child before it enters state or executes. */
|
|
896
|
-
export function assertValidMaterializedDagTask(task) {
|
|
899
|
+
export function assertValidMaterializedDagTask(task, parentSpec) {
|
|
897
900
|
const issues = [];
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
901
|
+
const spec = {
|
|
902
|
+
version: 2,
|
|
903
|
+
tasks: [task],
|
|
904
|
+
frontendTestLayout: parentSpec?.frontendTestLayout,
|
|
905
|
+
};
|
|
902
906
|
validateTaskWritePolicy(task, spec, issues);
|
|
903
907
|
validateShellTaskConfig(task, spec, issues);
|
|
904
908
|
validateStaticTaskConfig(task, issues);
|
|
@@ -906,7 +910,7 @@ export function assertValidMaterializedDagTask(task) {
|
|
|
906
910
|
validateRetryPolicyTaskConfig(task, issues);
|
|
907
911
|
validateOutputProtocolTaskConfig(task, issues);
|
|
908
912
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
909
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
913
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
910
914
|
if (issues.length > 0) {
|
|
911
915
|
throw new Error(`invalid materialized dynamic child ${task.id}: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
912
916
|
}
|
|
@@ -962,8 +966,8 @@ export function validateDagSpec(spec) {
|
|
|
962
966
|
validateRetryPolicyTaskConfig(task, issues);
|
|
963
967
|
validateOutputProtocolTaskConfig(task, issues);
|
|
964
968
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
965
|
-
validateCommandPolicyTaskConfig(task, issues);
|
|
966
|
-
validateDynamicChildCommandPolicy(task, issues);
|
|
969
|
+
validateCommandPolicyTaskConfig(task, issues, spec);
|
|
970
|
+
validateDynamicChildCommandPolicy(task, issues, spec);
|
|
967
971
|
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
968
972
|
validatePiExtensionsTaskConfig(task, issues);
|
|
969
973
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fe-test-ui-scout
|
|
3
|
-
description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 testcase/frontend/rag/ui-anchors.md
|
|
3
|
+
description: 在写 frontend-test Wave PRD/回归目录之前,对真实运行的被测页面做只读锚点侦察,产出并维护 {testRoot}/rag/ui-anchors.md 锚点台账(默认 testcase/frontend/rag/ui-anchors.md)。用 playwright-cli snapshot/find 逐字面验证命中数与唯一性,防止 PRD 验收标准引用不存在或不唯一的文案。触发词:frontend-test、锚点、台账、ui-anchors、侦察、写 PRD 前、snapshot/find 校准、Wave PRD。
|
|
4
4
|
references:
|
|
5
5
|
- path: references/ledger-schema.md
|
|
6
6
|
required: true
|
|
@@ -13,7 +13,7 @@ references:
|
|
|
13
13
|
## 定位
|
|
14
14
|
|
|
15
15
|
写 Wave PRD / 校准回归目录 AC **之前**的前置侦察。产出锚点台账
|
|
16
|
-
`testcase/frontend/rag/ui-anchors.md
|
|
16
|
+
`{testRoot}/rag/ui-anchors.md`(默认 `testcase/frontend/rag/ui-anchors.md`):每个「页面 × 状态」下真实可见、
|
|
17
17
|
`find` 可命中的字面清单。PRD 的每条 `find "..."` 断言必须能在台账找到对应行;
|
|
18
18
|
台账没有的字面禁止写进验收标准。
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ references:
|
|
|
23
23
|
|
|
24
24
|
## 输入
|
|
25
25
|
|
|
26
|
-
- 被测 baseUrl(来自 `
|
|
26
|
+
- 被测 baseUrl(来自 `{testRoot}/rag/context.md` 的 `environmentProbe=reachable` URL;默认 `testcase/frontend/rag/context.md`)
|
|
27
27
|
- 目标 Wave 的 AC 清单(回归目录中本 Wave 范围的行)
|
|
28
28
|
- 既有台账(增量更新,不整表重写)
|
|
29
29
|
|
|
@@ -56,7 +56,7 @@ references:
|
|
|
56
56
|
|
|
57
57
|
## 输出
|
|
58
58
|
|
|
59
|
-
- `testcase/frontend/rag/ui-anchors.md
|
|
59
|
+
- `{testRoot}/rag/ui-anchors.md`(增量更新;默认 `testcase/frontend/rag/ui-anchors.md`)
|
|
60
60
|
- 侦察小结(对话内输出,不落盘;结论回写 PRD 草稿的锚点引用)
|
|
61
61
|
|
|
62
62
|
## References
|
|
@@ -55,7 +55,7 @@ request
|
|
|
55
55
|
|
|
56
56
|
## 推荐执行流程
|
|
57
57
|
|
|
58
|
-
使用 `testcase/frontend/rag/context.md
|
|
58
|
+
使用 `{testRoot}/rag/context.md`(默认 `testcase/frontend/rag/context.md`)中已由 environment shell 标记为 `reachable` 的非生产 `baseUrl` 和默认浏览器 session;不得创建 named session。交互前先获取 snapshot,并只使用 snapshot 中可见的 ref 或已知安全 locator。截图写入当前 case 的 `{evidenceDir}/<caseId>/`,canonical 文件名仍是 `final.png`。
|
|
59
59
|
|
|
60
60
|
```text
|
|
61
61
|
playwright-cli open --browser=chrome http://localhost:5173
|
|
@@ -13,10 +13,12 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
|
|
|
13
13
|
|
|
14
14
|
## 输入与边界
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
16
|
+
默认产物根是 `testcase/frontend/`。若任务配置了 `frontendTest.testRoot`,把下列路径中的 `testcase/frontend/` 整段替换为该根(不要只替换 `testcase/`)。
|
|
17
|
+
|
|
18
|
+
- 只读取 `{testRoot}/rag/context.md`、`coverage-map.md` 与已有 `{testRoot}/cases/`。
|
|
19
|
+
- 若存在 `{testRoot}/rag/ui-anchors.md` 锚点台账,必须一并读取并作为 UI 断言的唯一事实源;台账缺失时按 RAG 知识包生成,但不得引用台账外的具体控件字面(缺失信息标 `blocked`)。
|
|
20
|
+
- 只写 `{testRoot}/cases/**`;不得回读 PRD、读取 `.harness/`,或写
|
|
21
|
+
`{testRoot}/evidence/**`。
|
|
20
22
|
- 所有 API、字段限制、状态流转、数据来源、SLA、URL 与账号要求必须能在 RAG 知识包中追溯。
|
|
21
23
|
缺失信息标记 `blocked` 或“需人工确认”,不得猜测。
|
|
22
24
|
|
|
@@ -38,10 +40,10 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
|
|
|
38
40
|
(例:`FE-LOGIN-001-core`)。禁止把验收标准写成 caseId。
|
|
39
41
|
- `acIds` = **验收标准 ID 列表**,形态 `AC-FE-*` / `AC-*`
|
|
40
42
|
(例:`["AC-FE-001"]`)。禁止把用例 ID 放进 acIds。
|
|
41
|
-
- `casePath` 必须等于 `testcase/frontend/cases/<caseId>.md
|
|
42
|
-
`evidenceDir` 必须等于 `testcase/frontend/evidence/<caseId
|
|
43
|
+
- `casePath` 必须等于 `{casesDir}/<caseId>.md`(默认 `testcase/frontend/cases/<caseId>.md`);
|
|
44
|
+
`evidenceDir` 必须等于 `{evidenceDir}/<caseId>/`(默认 `testcase/frontend/evidence/<caseId>/`)。
|
|
43
45
|
- `manifest` 使用 `schemaVersion: 1`,每项只含 `caseId`、`casePath`、`dimension`、`acIds`、
|
|
44
|
-
`evidenceDir`。所有 ID、路径和 evidenceDir 必须唯一,并位于 `
|
|
46
|
+
`evidenceDir`。所有 ID、路径和 evidenceDir 必须唯一,并位于 `{testRoot}/` 内。
|
|
45
47
|
- `index.md` 按功能点列出 case、维度、AC、数据依赖、API 映射和预期执行状态。
|
|
46
48
|
|
|
47
49
|
每个 case 必须包含:
|
|
@@ -73,7 +75,7 @@ description: 根据 FE-test RAG 知识包生成可由受限 playwright_cli runti
|
|
|
73
75
|
7. 明确的 UI/API 预期与数据清理结果;无法满足的环境或数据依赖必须写为 `blocked`。
|
|
74
76
|
|
|
75
77
|
所有文件型输出均由 controller 绑定到执行节点提供的
|
|
76
|
-
`testcase/frontend/evidence/<case-id
|
|
78
|
+
`{evidenceDir}/<case-id>/` 工作目录(默认 `testcase/frontend/evidence/<case-id>/`)。截图一律使用 canonical 语法
|
|
77
79
|
`playwright-cli screenshot --filename final.png`;如确有从最新 snapshot 解析出的真实元素 ref,写为
|
|
78
80
|
`playwright-cli screenshot e5 --filename final.png`。`pdf` 必须写为
|
|
79
81
|
`playwright-cli pdf --filename final.pdf`。`snapshot` 无 filename 时仅返回响应;需要文件时使用
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ag as o,ah as n}from"./mermaid.core-C-D3ZRUa.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-DB2G4H8V.js";import{_ as i}from"./mermaid.core-C-D3ZRUa.js";import"./chunk-5VM5RSS4-SLBibhf5.js";import"./chunk-XXDRQBXY-BKkkC4VA.js";import"./chunk-KBJHAD2P-CG78FGkn.js";import"./chunk-2GRJ4B5K-DF_YSvOS.js";import"./index-D5zGX6fG.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-DB2G4H8V.js";import{_ as i}from"./mermaid.core-C-D3ZRUa.js";import"./chunk-5VM5RSS4-SLBibhf5.js";import"./chunk-XXDRQBXY-BKkkC4VA.js";import"./chunk-KBJHAD2P-CG78FGkn.js";import"./chunk-2GRJ4B5K-DF_YSvOS.js";import"./index-D5zGX6fG.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
|