@tea-agent/loop-agent 0.36.3-beta.0 → 0.36.4-beta.0
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 +19 -0
- package/dist/build-stamp.json +3 -3
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/task/frontend-project-capability.js +162 -12
- package/dist/workflows/dag/frontend-prewrite-gate.js +37 -8
- package/dist/workflows/dag/init-hybrid.js +26 -2
- package/docs/templates/frontend-design-contract.md +34 -14
- package/docs/templates/frontend-task-constraints.md +25 -11
- package/docs/templates/frontend-task-requirement.md +54 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## [0.36.4-beta.0] - 2026-08-16
|
|
4
|
+
|
|
5
|
+
> 前端 DAG 规范上下文确定性落地(beta,发布到 `beta` dist-tag)。
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- `discoverFrontendProjectCapability` 产出 `designEvidence.classified` 语义归类:按通用目录约定(`openspec/schemas/**`、`openspec/project-specs/templates/**`、`openspec/project-specs/rules/**`、`openspec/project-specs/ui/**`、`ai_workspace/**`)把规范路径分入稳定键 `schemas` / `codeTemplate` / `rule.*` / `theme` / `component` / `uiOther` / `advisoryOther`,并进入 zod schema 校验(分区恒等式:归类值集合 == `normativePaths` 集合),重复探测稳定,不硬编码具体项目文件名
|
|
10
|
+
- 前端 contract/scout/plan/design-review 节点的 `subtask_prompt` 注入归类后的规范路径(含角色语义)并明确要求报告适用规则、命中路径/章节/行号、冲突与缺失
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- `frontend-prewrite-gate` 在 `openspecCandidatePaths` 非空时强制生效 plan/review 节点实际读取候选路径:未读 → `retryable-invalid` + 新增 `openspec-not-read` failureCode,错误信息列出未读路径;空候选(greenfield)不阻断。此前读取证据仅记录不阻断(advisory),本次真正 fail-closed
|
|
15
|
+
|
|
16
|
+
### 文档
|
|
17
|
+
|
|
18
|
+
- `docs/templates/frontend-task-constraints.md` 把裸 `TODO` 改写为「探索协议 + 结果 schema + 缺失/冲突 fail-closed」,不再出现具体业务组件名或项目专属路径/命令
|
|
19
|
+
- `docs/templates/frontend-task-requirement.md` 与 `docs/templates/frontend-design-contract.md` 把裸 `TODO` 改写为「编写协议」三要素(要写什么 / DAG 如何消费 / 缺失/冲突时 fail-closed),并新增 `test/frontend-template-no-todo.test.ts` 扫描三个前端模板断言无裸 `TODO`
|
|
20
|
+
- `docs/runtime/frontend-implementation-workflow.md` 的项目规范归类清单改为通用目录约定 + 归类规则,删除写死的示例项目文件名
|
|
21
|
+
|
|
3
22
|
## [0.36.3-beta.0] - 2026-08-16
|
|
4
23
|
|
|
5
24
|
> 前端 DAG Mock 治理收紧(beta,发布到 `beta` dist-tag)。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.36.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.36.4-beta.0",
|
|
4
|
+
"gitSha": "b4d154e6c1e109f754c059c22588cfd0298d4764",
|
|
5
|
+
"builtAt": "2026-08-16T17:25:17.099Z"
|
|
6
6
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertHarnessWriteAllowed, } from "./completed-facts-guard.js";
|
|
4
|
+
let atomicWriteSequence = 0;
|
|
4
5
|
async function writeAtomic(targetPath, content, options) {
|
|
5
6
|
assertHarnessWriteAllowed(targetPath, options);
|
|
6
7
|
const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
|
|
7
8
|
? path.resolve(options.repoRoot, targetPath)
|
|
8
9
|
: path.resolve(targetPath);
|
|
9
10
|
await mkdir(path.dirname(resolved), { recursive: true });
|
|
10
|
-
const tempPath = `${resolved}.${process.pid}.tmp`;
|
|
11
|
+
const tempPath = `${resolved}.${process.pid}.${(atomicWriteSequence += 1)}.tmp`;
|
|
11
12
|
try {
|
|
12
13
|
await writeFile(tempPath, content, "utf-8");
|
|
13
14
|
try {
|
|
@@ -5,7 +5,73 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath, } from "../shared/openspec-spec.js";
|
|
10
|
+
const classifiedPathSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.refine((candidate) => isOpenspecSpecFilePath(candidate), "classified path must be a repo-relative supported spec file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
|
|
13
|
+
/**
|
|
14
|
+
* Stable semantic partition of `designEvidence.normativePaths`. Keys are a
|
|
15
|
+
* generic directory convention (not project-specific file names); the same
|
|
16
|
+
* repository therefore yields the same classification on repeated probes.
|
|
17
|
+
*/
|
|
18
|
+
export const frontendDesignClassifiedSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
schemas: z.array(classifiedPathSchema),
|
|
21
|
+
codeTemplate: z.array(classifiedPathSchema),
|
|
22
|
+
rule: z
|
|
23
|
+
.object({
|
|
24
|
+
api: z.array(classifiedPathSchema),
|
|
25
|
+
mock: z.array(classifiedPathSchema),
|
|
26
|
+
router: z.array(classifiedPathSchema),
|
|
27
|
+
hooks: z.array(classifiedPathSchema),
|
|
28
|
+
utils: z.array(classifiedPathSchema),
|
|
29
|
+
components: z.array(classifiedPathSchema),
|
|
30
|
+
other: z.array(classifiedPathSchema),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
theme: z.array(classifiedPathSchema),
|
|
34
|
+
component: z.array(classifiedPathSchema),
|
|
35
|
+
uiOther: z.array(classifiedPathSchema),
|
|
36
|
+
advisoryOther: z.array(classifiedPathSchema),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
export const frontendDesignEvidenceSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
normativePaths: z.array(z.string()),
|
|
42
|
+
advisoryPaths: z.array(z.string()),
|
|
43
|
+
conflicts: z.array(z.string()),
|
|
44
|
+
classified: frontendDesignClassifiedSchema,
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.superRefine((evidence, ctx) => {
|
|
48
|
+
const classified = [
|
|
49
|
+
...evidence.classified.schemas,
|
|
50
|
+
...evidence.classified.codeTemplate,
|
|
51
|
+
...evidence.classified.rule.api,
|
|
52
|
+
...evidence.classified.rule.mock,
|
|
53
|
+
...evidence.classified.rule.router,
|
|
54
|
+
...evidence.classified.rule.hooks,
|
|
55
|
+
...evidence.classified.rule.utils,
|
|
56
|
+
...evidence.classified.rule.components,
|
|
57
|
+
...evidence.classified.rule.other,
|
|
58
|
+
...evidence.classified.theme,
|
|
59
|
+
...evidence.classified.component,
|
|
60
|
+
...evidence.classified.uiOther,
|
|
61
|
+
...evidence.classified.advisoryOther,
|
|
62
|
+
];
|
|
63
|
+
const classifiedSet = new Set(classified);
|
|
64
|
+
const normativeSet = new Set(evidence.normativePaths);
|
|
65
|
+
const onlyClassified = [...classifiedSet].filter((candidate) => !normativeSet.has(candidate));
|
|
66
|
+
const onlyNormative = [...normativeSet].filter((candidate) => !classifiedSet.has(candidate));
|
|
67
|
+
if (onlyClassified.length > 0 || onlyNormative.length > 0) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: z.ZodIssueCode.custom,
|
|
70
|
+
message: `classified must partition normativePaths exactly; extra in classified: ${onlyClassified.join(", ") || "(none)"}; missing from classified: ${onlyNormative.join(", ") || "(none)"}`,
|
|
71
|
+
path: ["classified"],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
9
75
|
async function exists(filePath) {
|
|
10
76
|
try {
|
|
11
77
|
await stat(filePath);
|
|
@@ -41,13 +107,14 @@ async function listOpenspec(repoRoot) {
|
|
|
41
107
|
continue;
|
|
42
108
|
await walk(root, 0);
|
|
43
109
|
}
|
|
110
|
+
out.sort();
|
|
44
111
|
return out.slice(0, 40);
|
|
45
112
|
async function walk(dir, depth) {
|
|
46
113
|
if (depth > 4)
|
|
47
114
|
return;
|
|
48
115
|
let entries = [];
|
|
49
116
|
try {
|
|
50
|
-
entries = await readdir(dir);
|
|
117
|
+
entries = (await readdir(dir)).sort();
|
|
51
118
|
}
|
|
52
119
|
catch {
|
|
53
120
|
return;
|
|
@@ -68,6 +135,97 @@ async function listOpenspec(repoRoot) {
|
|
|
68
135
|
}
|
|
69
136
|
}
|
|
70
137
|
}
|
|
138
|
+
function emptyClassified() {
|
|
139
|
+
return {
|
|
140
|
+
schemas: [],
|
|
141
|
+
codeTemplate: [],
|
|
142
|
+
rule: { api: [], mock: [], router: [], hooks: [], utils: [], components: [], other: [] },
|
|
143
|
+
theme: [],
|
|
144
|
+
component: [],
|
|
145
|
+
uiOther: [],
|
|
146
|
+
advisoryOther: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Classify discovered openspec paths by generic directory convention. Only
|
|
151
|
+
* `isOpenspecSpecFilePath` paths enter a bucket; the classifier never invents
|
|
152
|
+
* project-specific file names. Basename stems use word-boundary matching so
|
|
153
|
+
* e.g. `*api*` (case-insensitive) maps to `rule.api` without hardcoding names.
|
|
154
|
+
*/
|
|
155
|
+
function classifyOpenspecPaths(paths) {
|
|
156
|
+
const classified = emptyClassified();
|
|
157
|
+
const ruleStem = (basename) => {
|
|
158
|
+
const lower = basename.toLowerCase();
|
|
159
|
+
const stems = [
|
|
160
|
+
["api", /\bapi\b/],
|
|
161
|
+
["mock", /\bmock\b/],
|
|
162
|
+
["router", /\brouter\b/],
|
|
163
|
+
["hooks", /\bhooks?\b/],
|
|
164
|
+
["utils", /\butils?\b/],
|
|
165
|
+
["components", /\bcomponents?\b/],
|
|
166
|
+
];
|
|
167
|
+
for (const [key, re] of stems) {
|
|
168
|
+
if (re.test(lower))
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
return "other";
|
|
172
|
+
};
|
|
173
|
+
for (const candidate of paths) {
|
|
174
|
+
if (!isOpenspecSpecFilePath(candidate))
|
|
175
|
+
continue;
|
|
176
|
+
const segments = candidate.split("/");
|
|
177
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
178
|
+
if (candidate.startsWith("openspec/schemas/")) {
|
|
179
|
+
classified.schemas.push(candidate);
|
|
180
|
+
}
|
|
181
|
+
else if (candidate.startsWith("openspec/project-specs/templates/")) {
|
|
182
|
+
classified.codeTemplate.push(candidate);
|
|
183
|
+
}
|
|
184
|
+
else if (candidate.startsWith("openspec/project-specs/rules/")) {
|
|
185
|
+
classified.rule[ruleStem(basename)].push(candidate);
|
|
186
|
+
}
|
|
187
|
+
else if (candidate.startsWith("openspec/project-specs/ui/")) {
|
|
188
|
+
const lower = basename.toLowerCase();
|
|
189
|
+
if (/\btheme\b/.test(lower))
|
|
190
|
+
classified.theme.push(candidate);
|
|
191
|
+
else if (/\bcomponents?\b/.test(lower))
|
|
192
|
+
classified.component.push(candidate);
|
|
193
|
+
else
|
|
194
|
+
classified.uiOther.push(candidate);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
classified.advisoryOther.push(candidate);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const sortBucket = (bucket) => bucket.sort();
|
|
201
|
+
classified.schemas = sortBucket(classified.schemas);
|
|
202
|
+
classified.codeTemplate = sortBucket(classified.codeTemplate);
|
|
203
|
+
for (const key of [
|
|
204
|
+
"api",
|
|
205
|
+
"mock",
|
|
206
|
+
"router",
|
|
207
|
+
"hooks",
|
|
208
|
+
"utils",
|
|
209
|
+
"components",
|
|
210
|
+
"other",
|
|
211
|
+
]) {
|
|
212
|
+
classified.rule[key] = sortBucket(classified.rule[key]);
|
|
213
|
+
}
|
|
214
|
+
classified.theme = sortBucket(classified.theme);
|
|
215
|
+
classified.component = sortBucket(classified.component);
|
|
216
|
+
classified.uiOther = sortBucket(classified.uiOther);
|
|
217
|
+
classified.advisoryOther = sortBucket(classified.advisoryOther);
|
|
218
|
+
return classified;
|
|
219
|
+
}
|
|
220
|
+
function buildDesignEvidence(openspec) {
|
|
221
|
+
const normativePaths = [...new Set(openspec)].sort();
|
|
222
|
+
return frontendDesignEvidenceSchema.parse({
|
|
223
|
+
normativePaths,
|
|
224
|
+
advisoryPaths: [],
|
|
225
|
+
conflicts: [],
|
|
226
|
+
classified: classifyOpenspecPaths(normativePaths),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
71
229
|
export function buildAdapterGuidance(capability) {
|
|
72
230
|
const lines = [
|
|
73
231
|
"## Frontend project capability (generation-time)",
|
|
@@ -121,11 +279,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
121
279
|
a11y: { status: "unknown", tools: [], evidencePaths: [] },
|
|
122
280
|
evidencePaths: openspec.slice(0, 5),
|
|
123
281
|
reasons: ["package.json missing or unreadable"],
|
|
124
|
-
designEvidence:
|
|
125
|
-
normativePaths: openspec,
|
|
126
|
-
advisoryPaths: [],
|
|
127
|
-
conflicts: [],
|
|
128
|
-
},
|
|
282
|
+
designEvidence: buildDesignEvidence(openspec),
|
|
129
283
|
};
|
|
130
284
|
if (openspec.length) {
|
|
131
285
|
base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
|
|
@@ -284,11 +438,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
284
438
|
if (hasDep(deps, "vue-router"))
|
|
285
439
|
router = "vue-router";
|
|
286
440
|
const openspec = await listOpenspec(repoRoot);
|
|
287
|
-
const designEvidence =
|
|
288
|
-
normativePaths: openspec,
|
|
289
|
-
advisoryPaths: [],
|
|
290
|
-
conflicts: [],
|
|
291
|
-
};
|
|
441
|
+
const designEvidence = buildDesignEvidence(openspec);
|
|
292
442
|
if (openspec.length)
|
|
293
443
|
evidencePaths.push(...openspec.slice(0, 5));
|
|
294
444
|
const base = {
|
|
@@ -25,6 +25,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
25
25
|
"mock-strategy-no-verification-commands",
|
|
26
26
|
"target-outside-write-set",
|
|
27
27
|
"verification-target-outside-write-set",
|
|
28
|
+
"openspec-not-read",
|
|
28
29
|
]);
|
|
29
30
|
export const frontendPrewriteResultV1Schema = z
|
|
30
31
|
.object({
|
|
@@ -498,13 +499,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
498
499
|
});
|
|
499
500
|
}
|
|
500
501
|
}
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
outputDir: input.config.outputDir,
|
|
505
|
-
artifactName: input.config.artifactName,
|
|
506
|
-
canonical: analysis.canonical,
|
|
507
|
-
});
|
|
502
|
+
// Effective plan/review must actually read the candidate openspec paths when
|
|
503
|
+
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
504
|
+
// so a writer is never authorized on top of unread normative evidence.
|
|
508
505
|
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
509
506
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
510
507
|
runDir: input.runDir,
|
|
@@ -513,6 +510,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
513
510
|
reviewNodeId,
|
|
514
511
|
repoRoot: workspaceRoot ?? process.cwd(),
|
|
515
512
|
});
|
|
513
|
+
if (candidatePaths.length > 0) {
|
|
514
|
+
const readSet = new Set(openspecReadPaths);
|
|
515
|
+
const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
516
|
+
if (unread.length > 0) {
|
|
517
|
+
return finalizePrewrite(input, {
|
|
518
|
+
...basePending,
|
|
519
|
+
verdict,
|
|
520
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
521
|
+
normalizationActions: [],
|
|
522
|
+
mockStrategy,
|
|
523
|
+
classification: "retryable-invalid",
|
|
524
|
+
failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun.`,
|
|
525
|
+
failureCode: "openspec-not-read",
|
|
526
|
+
openspecReadPaths,
|
|
527
|
+
openspecCandidatePaths: candidatePaths,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Materialize the canonical contract only after every governance check passed.
|
|
532
|
+
const artifact = await writeFrontendImplementationContractArtifact({
|
|
533
|
+
runDir: input.runDir,
|
|
534
|
+
outputDir: input.config.outputDir,
|
|
535
|
+
artifactName: input.config.artifactName,
|
|
536
|
+
canonical: analysis.canonical,
|
|
537
|
+
});
|
|
516
538
|
const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
|
|
517
539
|
return finalizePrewrite(input, {
|
|
518
540
|
...basePending,
|
|
@@ -556,7 +578,14 @@ export function formatFrontendPrewriteGateStdout(result) {
|
|
|
556
578
|
if (result.openspecReadPaths.length > 0) {
|
|
557
579
|
lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
|
|
558
580
|
}
|
|
559
|
-
|
|
581
|
+
if (result.openspecCandidatePaths.length > 0) {
|
|
582
|
+
const readSet = new Set(result.openspecReadPaths);
|
|
583
|
+
const unread = result.openspecCandidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
584
|
+
if (unread.length > 0) {
|
|
585
|
+
lines.push(`openspec unread: ${unread.join(", ")}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
560
589
|
lines.push("openspec: unavailable");
|
|
561
590
|
}
|
|
562
591
|
return lines.join("\n");
|
|
@@ -2175,9 +2175,33 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2175
2175
|
}
|
|
2176
2176
|
if (capability) {
|
|
2177
2177
|
parts.push("", capability.adapterGuidance);
|
|
2178
|
-
|
|
2179
|
-
|
|
2178
|
+
const classified = capability.designEvidence.classified;
|
|
2179
|
+
const bucketLines = [];
|
|
2180
|
+
const pushBucket = (label, paths) => {
|
|
2181
|
+
if (paths.length > 0)
|
|
2182
|
+
bucketLines.push(`${label}: ${paths.join(", ")}`);
|
|
2183
|
+
};
|
|
2184
|
+
pushBucket("schemas", classified.schemas);
|
|
2185
|
+
pushBucket("code-template", classified.codeTemplate);
|
|
2186
|
+
pushBucket("rule.api", classified.rule.api);
|
|
2187
|
+
pushBucket("rule.mock", classified.rule.mock);
|
|
2188
|
+
pushBucket("rule.router", classified.rule.router);
|
|
2189
|
+
pushBucket("rule.hooks", classified.rule.hooks);
|
|
2190
|
+
pushBucket("rule.utils", classified.rule.utils);
|
|
2191
|
+
pushBucket("rule.components", classified.rule.components);
|
|
2192
|
+
pushBucket("rule.other", classified.rule.other);
|
|
2193
|
+
pushBucket("theme", classified.theme);
|
|
2194
|
+
pushBucket("component", classified.component);
|
|
2195
|
+
pushBucket("ui-other", classified.uiOther);
|
|
2196
|
+
pushBucket("advisory-other", classified.advisoryOther);
|
|
2197
|
+
parts.push("## Classified openspec specification paths (role semantics)");
|
|
2198
|
+
if (bucketLines.length > 0) {
|
|
2199
|
+
parts.push(...bucketLines);
|
|
2200
|
+
}
|
|
2201
|
+
else {
|
|
2202
|
+
parts.push("(no openspec specification paths discovered — greenfield)");
|
|
2180
2203
|
}
|
|
2204
|
+
parts.push("Each consuming node MUST report in its output: applicable rules, the hit path/section/line number for every applied specification, and any conflicts or missing specifications. Missing or conflicting required specifications must fail closed rather than silently substituting nearby repository conventions.");
|
|
2181
2205
|
parts.push(`A11y capability: ${capability.a11y.status}` +
|
|
2182
2206
|
(capability.a11y.tools.length
|
|
2183
2207
|
? ` (${capability.a11y.tools.join(", ")})`
|
|
@@ -1,42 +1,62 @@
|
|
|
1
1
|
# 前端设计契约模板
|
|
2
2
|
|
|
3
|
+
本模板只承载**通用编写协议 + 三要素 + fail-closed 规则**,不包含任何具体业务组件名、项目专属路径或项目专属命令。设计契约在进入实现前由 `frontend-plan-pi` 落入结构化 implementation contract,`frontend-design-review-pi` 按本模板逐节审查;每一节都要给出「**要写什么** / **DAG 如何消费** / **缺失/冲突时 fail-closed**」三要素。
|
|
4
|
+
|
|
3
5
|
## 页面目标
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
- **要写什么**:写清每个目标页面的职责、用户到达该页面的目的与成功结果,逐页列出,与需求.md 的用户目标/目标页面一一对应。
|
|
8
|
+
- **DAG 如何消费**:`frontend-plan-pi` 据它确定 `targets.routes` 与每页的实现步骤,`frontend-design-review-pi` 据它核对范围与需求一致性。
|
|
9
|
+
- **缺失/冲突时 fail-closed**:页面目标与需求不一致、缺页或目标含糊(无成功结果)时阻塞,不得自行补页或改目标。
|
|
6
10
|
|
|
7
11
|
## 信息结构
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
- **要写什么**:写清每页展示的信息层级与数据来源(字段、分组、优先级、空/错误占位),接口数据与静态文案分开写。
|
|
14
|
+
- **DAG 如何消费**:`frontend-plan-pi` 据它安排组件数据流与状态展示,`frontend-design-review-pi` 据它核对接口字段映射。
|
|
15
|
+
- **缺失/冲突时 fail-closed**:信息层级缺失、数据来源不可追溯、与接口字段冲突时阻塞,不得臆造字段或层级。
|
|
10
16
|
|
|
11
17
|
## 组件拆分
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
- **要写什么**:把页面拆成组件树,逐组件写职责、复用边界、props/状态归属与可替换点;复用现有设计系统组件时引用其来源路径。
|
|
20
|
+
- **DAG 如何消费**:`frontend-plan-pi` 据它确定 `targets.files` 与实现顺序,`frontend-design-review-pi` 据它审查拆分是否越界/重复。
|
|
21
|
+
- **缺失/冲突时 fail-closed**:组件边界不清、同一职责被拆到多处、或与设计规范冲突时阻塞,不得按邻近代码自行决定复用。
|
|
14
22
|
|
|
15
23
|
## 交互规则
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
- **要写什么**:逐条写清每个交互的触发、预期行为、状态变化与可恢复性(对应需求.md 的交互要求)。
|
|
26
|
+
- **DAG 如何消费**:`frontend-plan-pi` 据它填充 `interactions[]`(`trigger` + `expectedBehavior`),verify 用行为命令断言。
|
|
27
|
+
- **缺失/冲突时 fail-closed**:交互无预期行为、与 UI 状态冲突、或不可自动化断言时阻塞。
|
|
18
28
|
|
|
19
29
|
## UI 状态
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
- **要写什么**:逐状态(loading/empty/error/success/disabled)写清触发条件、展示内容与转移;不适用状态写 `N/A` + 非空理由。
|
|
32
|
+
- **DAG 如何消费**:进入 implementation contract 的 `uiStates[]`,verify 逐状态断言,`frontend-review-pi` 据它核对实现是否遗漏状态。
|
|
33
|
+
- **缺失/冲突时 fail-closed**:适用状态缺失预期、N/A 无理由、或状态展示与交互/接口状态冲突时阻塞。
|
|
22
34
|
|
|
23
35
|
## Mock / API 策略
|
|
24
36
|
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- DAG
|
|
30
|
-
-
|
|
37
|
+
策略枚举固定为 `native | browser-intercept | request-adapter | not-needed | blocked`,与 runtime Mock 语义一致:`blocked` 永远不通过(缺少/冲突接口契约、路径或依赖未授权、无法证明生产默认关闭、固定验证入口无法覆盖、或唯一方案是注释真实请求时必须选它);`not-needed` 必须有后端可用或任务不涉及远程接口的真实/无远程证据,且固定 behavior 入口能覆盖相应行为;显式 `policy=required` 不接受 `not-needed`。契约只接受当前 policy 允许的非 `blocked` 首行。
|
|
38
|
+
|
|
39
|
+
- **接口文档或 schema — 要写什么**:引用接口文档/schema 路径与版本,作为 fixture 与字段映射的唯一来源。**DAG 如何消费**:`frontend-plan-pi` 据它冻结 endpoint/fixture 映射,prewrite gate 校验 fixture 可追溯。**缺失/冲突时 fail-closed**:无文档/schema、字段冲突时选 `blocked`,不得自行发明接口。
|
|
40
|
+
- **策略**:只写枚举中的一个值:`native | browser-intercept | request-adapter | not-needed | blocked`。
|
|
41
|
+
- **endpoint / fixture / UI 状态映射 — 要写什么**:逐 endpoint 列 method、path、fixture 路径与消费组件,并映射到 UI 状态。**DAG 如何消费**:写入 `mockApi.endpoints[]`(method/path/fixture/consumer),非 `not-needed` 策略要求每个 endpoint 都有 fixture 与 consumer。**缺失/冲突时 fail-closed**:非 `not-needed` 却缺 fixture/consumer、endpoint 与 UI 状态映射不一致时阻塞。
|
|
42
|
+
- **显式启用方式与 production 默认关闭边界 — 要写什么**:写清真实请求为默认路径、Mock 仅通过显式开关(环境变量/构建开关)启用的具体边界。**DAG 如何消费**:`productionDefaultOff` 恒为 `true`,`frontend-review-pi` 检查是否注释真实请求或默认开启 Mock。**缺失/冲突时 fail-closed**:无法证明生产默认关闭、或 Mock 会进入生产入口时选 `blocked`。
|
|
43
|
+
- **DAG 已固化的验证入口 — 要写什么**:引用 DAG 生成时已冻结的 static/behavior/Mock 命令 label,不发明新命令。**DAG 如何消费**:`verificationTarget.commandLabel` 必须逐字落在冻结命令集合内,否则 contract 物化 `invalid-output`。**缺失/冲突时 fail-closed**:策略需要 Mock 验证命令却没有冻结命令时生成期 fail-closed(`no authorized Mock verification commands`)。
|
|
44
|
+
- **Real Integration Gap 与后端就绪后的复验 — 要写什么**:写清当前未联通的真实集成缺口,以及后端就绪后的复验路径。**DAG 如何消费**:进入 `evidenceGaps[]`,closeout 报告 `Real integration: pending`,复验任务 `<task-id>-real-api-integration-verify` 由操作者显式触发。**缺失/冲突时 fail-closed**:把 Mock 证据当真实联调证据时 review 拒绝。
|
|
31
45
|
|
|
32
46
|
## 样式与设计系统映射
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
- **要写什么**:写清页面用到的主题/Token、设计系统组件与样式来源(引用具体来源路径),自定义样式与复用样式的边界。
|
|
49
|
+
- **DAG 如何消费**:`frontend-plan-pi` 据它安排样式实现,`frontend-design-review-pi` 据它核对是否与设计系统冲突。
|
|
50
|
+
- **缺失/冲突时 fail-closed**:样式来源缺失、Token/组件冲突、或自定义样式会破坏设计系统时阻塞。
|
|
35
51
|
|
|
36
52
|
## 响应式范围
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
- **要写什么**:写清支持的视口范围(desktop/mobile/tablet)与断点、每个断点下的布局差异。
|
|
55
|
+
- **DAG 如何消费**:进入 contract 的 target runtime environment,`frontend-plan-pi` 据它决定响应式策略。
|
|
56
|
+
- **缺失/冲突时 fail-closed**:视口范围与需求运行环境不一致、或断点未覆盖声明环境时阻塞。
|
|
39
57
|
|
|
40
58
|
## 风险与非目标
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
- **要写什么**:写清设计上的已知风险、依赖缺口与明确不做/排除的设计范围。
|
|
61
|
+
- **DAG 如何消费**:进入 `evidenceGaps[]` 与 Non-goals,`frontend-review-pi`/closeout 据它保留风险与后续项。
|
|
62
|
+
- **缺失/冲突时 fail-closed**:范围与需求非目标冲突、或风险被隐藏时阻塞。
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
# 前端任务执行约束模板
|
|
2
2
|
|
|
3
|
+
本模板只承载**通用探索协议 + 结果 schema + 缺失/冲突时 fail-closed 规则**,不包含任何具体业务组件名、项目专属路径或项目专属命令。项目事实由生成期能力探测(`openspec/schemas/**`、`openspec/project-specs/**`、`ai_workspace/**`)产出并经语义归类注入任务契约。
|
|
4
|
+
|
|
3
5
|
## 技术约束
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
- **要探索什么**:项目 `package.json` 的直接依赖与 scripts、框架/路由/状态/数据获取/组件库/样式/测试/构建相关配置与引导文件,以及 `openspec/schemas/**` 与 `openspec/project-specs/rules/**` 中声明技术边界的规范。
|
|
8
|
+
- **结果必须包含**:每一条适用技术规则的源路径、命中章节与行号;被排除依赖的明确理由;与既有技术栈一致的运行时要求(编译、构建、入口、环境变量)。
|
|
9
|
+
- **缺失/冲突时如何 fail-closed**:无法找到技术规范来源时,显式标注 `source: unavailable` 并只采用任务源与现有代码的可验证事实;发现冲突规则时不臆造取舍,先记录冲突并返回 `request-revision`/blocked,而不是静默采用邻近代码惯例。
|
|
6
10
|
|
|
7
11
|
## 代码风格约束
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
- **要探索什么**:`openspec/project-specs/templates/**`(代码模板)与 `openspec/project-specs/rules/**` 中组件/钩子/工具等代码风格规则,以及现有组件、主题/Token、stories 与测试作为较弱 repository fallback。
|
|
14
|
+
- **结果必须包含**:命中模板/规则的源路径、章节与行号;命名、目录、导出、类型、样式组织等可执行约定;冲突字段逐条列出。
|
|
15
|
+
- **缺失/冲突时如何 fail-closed**:没有模板/规则命中时明确说明并记录 `repository fallback` 证据;冲突未解决时不得进入实现,必须返回阻塞信息。
|
|
10
16
|
|
|
11
17
|
## 设计约束
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
- **要探索什么**:`openspec/project-specs/ui/**`(主题/组件目录)与 `openspec/project-specs/rules/**` 中页面/组件拆分、交互、UI 状态与响应式相关规则。
|
|
20
|
+
- **结果必须包含**:命中设计规范/组件目录的源路径、章节与行号;组件拆分与复用边界、样式与设计系统映射、UI 状态与响应式范围。
|
|
21
|
+
- **缺失/冲突时如何 fail-closed**:设计规范缺失时明确记录来源为 `unavailable` 并保留为已知风险;组件/主题冲突时必须阻断,不能按邻近代码自行决定。
|
|
14
22
|
|
|
15
23
|
## 验证约束
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
- **要探索什么**:项目 `package.json` scripts 中真实存在的 lint/typecheck/build/test 命令,以及任务源 `需求.md`/`执行约束.md` 中声明的验证命令。
|
|
26
|
+
- **结果必须包含**:命令的 label、原始命令文本与来源(scripts/任务源);每条命令可自终止(启动→断言→退出)的说明。
|
|
27
|
+
- **缺失/冲突时如何 fail-closed**:只在确定命令真实存在时写入冻结命令集;不发明 shell 命令。命令不可执行或漂移时 fail closed,不宣称验证通过。
|
|
18
28
|
|
|
19
29
|
## Mock 约束(数据型任务)
|
|
20
30
|
|
|
@@ -24,18 +34,22 @@ TODO
|
|
|
24
34
|
- **命令要探索、不要硬编码**:到项目 `package.json` scripts 里找实际存在的 Mock 相关脚本(如 `mock`、`mock:*`、`dev:mock`,或名字含 mock 的脚本),结合既有 service root 的 handler/fixture/bootstrap 启动方式,确定一条**真实存在、确定、可自终止**(启动→断言→退出 0)的验证命令。常驻 dev server 必须包装成自终止脚本(start→assert→stop),否则 verify shell 会超时 fail-closed。
|
|
25
35
|
- **写入 task.json**:把探索到的命令原样写进 `frontendMock.verifyCommands`(`label` 唯一、`command` 与项目脚本一致)。`label` 会进入冻结命令集,implementation contract 的 `verificationTarget.commandLabel` 必须逐字引用它。
|
|
26
36
|
- **命令来源白名单**:只允许项目 `package.json` 已有脚本、Mock capability seed、或本段声明的 `verifyCommands`;plan/design 阶段不能发明 shell 命令。
|
|
27
|
-
- **Mock/API/schema
|
|
28
|
-
- **既有 Mock service root、handler/fixture/bootstrap
|
|
29
|
-
- **既有 browser/e2e interception 或 request adapter/DI seam
|
|
30
|
-
- **production
|
|
31
|
-
- **真实请求默认路径与 Mock
|
|
37
|
+
- **Mock/API/schema 规范路径**:到 `openspec/project-specs/**`(rules 中的 mock/api 规则、templates 中的接口模板)与 `ai_workspace/**` 检索;结果必须列出源路径、章节与行号。缺失或冲突时 fail closed,不自行发明接口契约。
|
|
38
|
+
- **既有 Mock service root、handler/fixture/bootstrap**:检索项目现有 Mock 服务根目录、handler、fixture 与 bootstrap 启动方式;结果必须列出发现路径,未发现时明确记录 `unavailable`。
|
|
39
|
+
- **既有 browser/e2e interception 或 request adapter/DI seam**:检索项目现有浏览器/e2e 拦截或请求适配层/依赖注入 seam;结果必须列出发现路径或明确记录缺失。
|
|
40
|
+
- **production 禁用边界**:结果必须说明真实请求为默认路径、Mock 仅通过显式测试/开发开关启用的具体边界;无法证明生产默认关闭时返回 `blocked`。
|
|
41
|
+
- **真实请求默认路径与 Mock 显式启用方式**:结果必须写出真实请求默认路径与 Mock 显式启用方式(如环境变量/构建开关),不得通过注释真实请求或在生产组件内硬编码假数据实现。
|
|
32
42
|
- **`task.json.frontendMock.policy`**:`auto | required | disabled`(规范强制 Mock 用 `required`)
|
|
33
43
|
- **被拦截时怎么修**:`mock-strategy-outside-allowed` / `no authorized Mock verification commands` 是**生成期契约问题,不是 plan 问题**——补 `frontendMock.verifyCommands`(或 `policy: "required"` + 命令)后**重新生成 DAG** 再跑,plan-revision 无法修复它。
|
|
34
44
|
|
|
35
45
|
## allowedPaths
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
- **要探索什么**:任务源 `需求.md`/`执行约束.md`/`task.json` 中声明的写入边界,以及本次交付实际涉及的文件/目录。
|
|
48
|
+
- **结果必须包含**:与 `task.json.allowedPaths` 一致且窄化的路径列表;任何越界路径都必须在 plan 中记为阻断性 scope conflict。
|
|
49
|
+
- **缺失/冲突时如何 fail-closed**:缺失允许路径或发现计划目标在允许路径之外时,不得擅自扩大边界,返回 blocked。
|
|
38
50
|
|
|
39
51
|
## forbiddenPaths
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
- **要探索什么**:任务源与 `task.json` 中声明的禁止写入路径(运行时产物、依赖目录、构建输出、私有路径等)。
|
|
54
|
+
- **结果必须包含**:禁止写入的路径清单,以及实现/验证步骤不触碰这些路径的确认。
|
|
55
|
+
- **缺失/冲突时如何 fail-closed**:任何写入落入禁止路径立即阻断;禁止路径语义不变,不得新增读取权限字段或扩大写入边界。
|
|
@@ -1,70 +1,104 @@
|
|
|
1
1
|
# 前端任务需求模板
|
|
2
2
|
|
|
3
|
+
本模板只承载**通用编写协议 + 三要素 + fail-closed 规则**,不包含任何具体业务组件名、项目专属路径或项目专属命令。填写时每一节都要给出「**要写什么** / **DAG 如何消费** / **缺失/冲突时 fail-closed**」三要素;缺节、空占位或与验收标准冲突时必须阻塞,不得由模型猜测补写。
|
|
4
|
+
|
|
3
5
|
## 用户目标
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
- **要写什么**:以用户视角写清本次要交付的前端行为与用户价值——用户能完成什么、为什么需要它、成功后的可观察结果。必须可验证(可被界面状态或交互断言覆盖),不得只写技术动作或孤立关键词。
|
|
8
|
+
- **DAG 如何消费**:`frontend-contract-pi` 把它映射为 Scope 与 `requirements[].expectedOutcome`,`frontend-plan-pi` 据它安排实现步骤并映射验收标准。
|
|
9
|
+
- **缺失/冲突时 fail-closed**:缺失用户目标,或用户目标与验收标准互相矛盾时阻塞(`request-revision`/blocked),不得猜测补写或改写为技术实现描述。
|
|
6
10
|
|
|
7
11
|
## 目标页面/组件/路由
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
- **要写什么**:明确本次交付涉及的目标路由、页面入口与组件范围,逐条列出路径/名称与职责边界;相关但超出范围的页面写清「不做」。
|
|
14
|
+
- **DAG 如何消费**:对齐 `docs/runtime/frontend-implementation-workflow.md` §「需求.md 应包含」的「目标路由/页面/组件」,`frontend-contract-pi` 据它确定 `targets.files` / `targets.routes`,`frontend-plan-pi` 据它划定 writeSet 与实现步骤。
|
|
15
|
+
- **缺失/冲突时 fail-closed**:无法确定目标路由/页面/组件,或范围与验收标准不一致时阻塞;只写泛化名词(如「页面」「组件」)而不给具体范围视为缺失。
|
|
10
16
|
|
|
11
17
|
## 用户流程
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
- **要写什么**:写清端到端用户路径,从入口到结果,逐步列出触发动作、顺序与关键分支;涉及多页面/多状态时把每条路径分开写。
|
|
20
|
+
- **DAG 如何消费**:`frontend-contract-pi` 据它抽取 `interactions[]`(每条带 `trigger` 与 `expectedBehavior`),`frontend-plan-pi` 据它安排组件/状态/交互实现步骤。
|
|
21
|
+
- **缺失/冲突时 fail-closed**:缺少可执行的端到端路径、流程与验收标准冲突或关键分支未写时阻塞,不得只写「用户进入页面即可」。
|
|
14
22
|
|
|
15
23
|
## 必须状态
|
|
16
24
|
|
|
25
|
+
每个适用状态必须写清该状态的触发条件与预期界面表现;不适用状态必须写 `N/A` 并给出非空理由(对齐 runtime 的 `uiStates` / N/A state 规则)。
|
|
26
|
+
|
|
17
27
|
### loading
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
- **要写什么**:loading 状态出现与结束的触发条件、展示内容(骨架/文案/禁用交互)与超时/失败转移。
|
|
30
|
+
- **DAG 如何消费**:进入 implementation contract 的 `uiStates[]`(`name: "loading"`),`frontend-implement-pi` 按它实现,verify 用它的 `verificationTargetIds` 断言。
|
|
31
|
+
- **缺失/冲突时 fail-closed**:适用但缺失预期行为时阻塞;不适用必须写 `N/A` 并给非空理由,只写 `N/A` 不带理由同样阻塞。
|
|
20
32
|
|
|
21
33
|
### empty
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
- **要写什么**:无数据/无结果状态的触发条件与展示(空态文案/引导动作/是否可刷新)。
|
|
36
|
+
- **DAG 如何消费**:进入 `uiStates[]`(`name: "empty"`),verify 断言空态展示与引导。
|
|
37
|
+
- **缺失/冲突时 fail-closed**:适用但缺失预期行为、或与 success/error 展示冲突时阻塞;不适用写 `N/A` + 非空理由。
|
|
24
38
|
|
|
25
39
|
### error
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
- **要写什么**:失败状态(请求失败/校验失败/权限失败)的触发条件、错误展示与可恢复动作。
|
|
42
|
+
- **DAG 如何消费**:进入 `uiStates[]`(`name: "error"`),verify 断言错误展示与重试/回退行为。
|
|
43
|
+
- **缺失/冲突时 fail-closed**:适用但缺失错误预期、或错误态会覆盖用户数据时阻塞;不适用写 `N/A` + 非空理由。
|
|
28
44
|
|
|
29
45
|
### success
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
- **要写什么**:成功状态的触发条件与展示(数据呈现/确认反馈/后续入口)。
|
|
48
|
+
- **DAG 如何消费**:进入 `uiStates[]`(`name: "success"`),verify 断言成功展示与验收标准对应。
|
|
49
|
+
- **缺失/冲突时 fail-closed**:成功态与验收标准不一致、或缺失成功结果时阻塞;不适用写 `N/A` + 非空理由。
|
|
32
50
|
|
|
33
51
|
### disabled
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
- **要写什么**:禁用/不可交互状态的触发条件(权限/前置未满足/进行中)与视觉/交互表现。
|
|
54
|
+
- **DAG 如何消费**:进入 `uiStates[]`(`name: "disabled"`),verify 断言禁用态不会被误触。
|
|
55
|
+
- **缺失/冲突时 fail-closed**:适用但缺失禁用条件、或禁用态与交互要求冲突时阻塞;不适用写 `N/A` + 非空理由。
|
|
36
56
|
|
|
37
57
|
## 目标运行环境
|
|
38
58
|
|
|
59
|
+
每个环境必须声明适用性;不适用环境写 `N/A` 并给非空理由。
|
|
60
|
+
|
|
39
61
|
### desktop
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
- **要写什么**:desktop 视口下的适用性声明与关键布局/交互差异(断点、栅格、悬停等)。
|
|
64
|
+
- **DAG 如何消费**:进入 contract 的 target runtime environment,`frontend-plan-pi` 据它决定响应式范围与样式策略。
|
|
65
|
+
- **缺失/冲突时 fail-closed**:适用但缺失断点/布局说明,或与响应式要求冲突时阻塞;不适用写 `N/A` + 非空理由。
|
|
42
66
|
|
|
43
67
|
### mobile
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
- **要写什么**:mobile 视口下的适用性声明与触控/布局/安全区差异。
|
|
70
|
+
- **DAG 如何消费**:进入 target runtime environment,`frontend-plan-pi` 据它决定响应式范围与组件策略。
|
|
71
|
+
- **缺失/冲突时 fail-closed**:适用但缺失移动端表现,或与交互要求冲突时阻塞;不适用写 `N/A` + 非空理由。
|
|
46
72
|
|
|
47
73
|
### tablet
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
- **要写什么**:tablet 视口下的适用性声明与中间断点表现。
|
|
76
|
+
- **DAG 如何消费**:进入 target runtime environment,`frontend-plan-pi` 据它决定响应式策略。
|
|
77
|
+
- **缺失/冲突时 fail-closed**:适用但缺失 tablet 表现时阻塞;不适用写 `N/A` + 非空理由。
|
|
50
78
|
|
|
51
79
|
## 交互要求
|
|
52
80
|
|
|
53
|
-
|
|
81
|
+
- **要写什么**:逐条写清每个可交互元素的触发动作与预期行为(点击/输入/提交/滚动/键盘等),可被自动化断言覆盖。
|
|
82
|
+
- **DAG 如何消费**:对齐 §「需求.md 应包含」的「交互要求」,`frontend-contract-pi` 抽取 `interactions[]`(`trigger` + `expectedBehavior`),verify 用行为命令断言。
|
|
83
|
+
- **缺失/冲突时 fail-closed**:交互触发或预期行为缺失、与 UI 状态冲突、或不可自动化断言时阻塞,不得用「用户可正常操作」这类空话。
|
|
54
84
|
|
|
55
85
|
## 接口与 Mock 输入
|
|
56
86
|
|
|
57
|
-
-
|
|
58
|
-
- endpoint、method
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
- success/empty/error/permission
|
|
62
|
-
-
|
|
87
|
+
- **接口文档/schema — 要写什么**:引用接口文档或任务附件路径与版本,声明请求/响应 schema 来源。**DAG 如何消费**:`frontend-plan-pi` 据此选择 Mock 策略并冻结 endpoint/fixture 映射。**缺失/冲突时 fail-closed**:涉及接口但未引用文档或 schema 时阻塞,不得自行发明字段。
|
|
88
|
+
- **endpoint、method、关键请求/响应字段 — 要写什么**:逐条列出 method、path 与关键字段及含义。**DAG 如何消费**:写入 `mockApi.endpoints[]`(method/path/fixture/consumer)。**缺失/冲突时 fail-closed**:字段缺失、与接口文档冲突或路径不合法时阻塞。
|
|
89
|
+
- **是否允许依赖真实后端 — 要写什么**:明确声明真实后端当前是否可调用、是否允许生产/预览依赖真实请求。**DAG 如何消费**:决定 `mockApi.strategy` 是否可选 `not-needed`(需要真实/无远程证据)。**缺失/冲突时 fail-closed**:未声明后端就绪度却要求真实数据时阻塞,选 `not-needed` 却没有真实/无远程证据会被拦截。
|
|
90
|
+
- **是否要求离线或独立行为验证 — 要写什么**:声明是否需要离线、本地预览或自动化行为验证(或两者都要)。**DAG 如何消费**:决定生成期 `frontendMock.verifyCommands` 与冻结命令集。**缺失/冲突时 fail-closed**:要求离线/自动化却未声明确定性验证命令时,生成期收敛为 `not-needed` 或直接 blocked,plan-revision 无法修复。
|
|
91
|
+
- **success/empty/error/permission 状态 — 要写什么**:逐条声明接口各响应状态对应的 UI 表现。**DAG 如何消费**:与「必须状态」交叉校验,写入 `uiStates[]` 与 fixture 映射。**缺失/冲突时 fail-closed**:接口状态与 UI 状态不匹配或缺失 permission 分支时阻塞。
|
|
92
|
+
- **后端当前就绪状态与 Real Integration Gap — 要写什么**:写清后端是否就绪、未就绪时保留的 Real Integration Gap 与后端就绪后的复验路径。**DAG 如何消费**:进入 `evidenceGaps[]` 与 closeout 的 `Real integration: pending`。**缺失/冲突时 fail-closed**:声称已联通真实接口却无真实证据时阻塞;未就绪却把 Mock 结果写成真实联调会被 review 拒绝。
|
|
63
93
|
|
|
64
94
|
## 验收标准
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
- **要写什么**:逐条编号(如 `AC-001`)写出可独立断言的前端验收标准,每条都能映射到需求与一个 verification target(可逐条追踪)。
|
|
97
|
+
- **DAG 如何消费**:`frontend-contract-pi` 生成 `requirements[].verificationTargetIds` 映射,`frontend-plan-pi` 把每条 AC 落到有序步骤与验证命令,verify 逐条断言。
|
|
98
|
+
- **缺失/冲突时 fail-closed**:无验收标准、AC 无法映射到需求/验证目标、或 AC 与其它节冲突时阻塞,不得把「看起来能用」当验收。
|
|
67
99
|
|
|
68
100
|
## 非目标
|
|
69
101
|
|
|
70
|
-
|
|
102
|
+
- **要写什么**:明确列出本次不做的前端范围/排除项(页面、组件、交互、浏览器/视觉/无障碍检查等)。
|
|
103
|
+
- **DAG 如何消费**:`frontend-contract-pi` 记录为 Non-goals,`frontend-review-pi` 据它拒绝范围扩散。
|
|
104
|
+
- **缺失/冲突时 fail-closed**:非目标与目标/验收标准冲突、或范围被悄悄扩大时阻塞;明确不做前端时也必须写入非目标。
|