ai-project-manage-cli 4.0.9 → 4.0.11
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/dist/index.js
CHANGED
|
@@ -100,6 +100,10 @@ var requestConfig = {
|
|
|
100
100
|
method: "POST",
|
|
101
101
|
path: "/cli/requirements/comment"
|
|
102
102
|
}),
|
|
103
|
+
commentStructured: defineEndpoint({
|
|
104
|
+
method: "POST",
|
|
105
|
+
path: "/cli/requirements/comment-structured"
|
|
106
|
+
}),
|
|
103
107
|
refine: defineEndpoint({
|
|
104
108
|
method: "POST",
|
|
105
109
|
path: "/cli/requirements/refine"
|
|
@@ -237,15 +241,120 @@ function resolveCwdPath(file) {
|
|
|
237
241
|
return resolve(process.cwd(), file);
|
|
238
242
|
}
|
|
239
243
|
|
|
244
|
+
// src/structured-review-yaml.ts
|
|
245
|
+
import { parse } from "yaml";
|
|
246
|
+
var KIND_MAP = {
|
|
247
|
+
clarify: "CLARIFY",
|
|
248
|
+
difficulty: "DIFFICULTY",
|
|
249
|
+
business: "BUSINESS",
|
|
250
|
+
coordination: "COORDINATION",
|
|
251
|
+
CLARIFY: "CLARIFY",
|
|
252
|
+
DIFFICULTY: "DIFFICULTY",
|
|
253
|
+
BUSINESS: "BUSINESS",
|
|
254
|
+
COORDINATION: "COORDINATION"
|
|
255
|
+
};
|
|
256
|
+
var STANCE_VALUES = /* @__PURE__ */ new Set(["frontend", "backend", "fullstack"]);
|
|
257
|
+
function asRecord(value) {
|
|
258
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
throw new Error("YAML \u7ED3\u6784\u65E0\u6548\uFF1A\u9700\u8981\u5BF9\u8C61\u6839\u8282\u70B9");
|
|
262
|
+
}
|
|
263
|
+
function parseAnchor(anchor, index) {
|
|
264
|
+
const rec = asRecord(anchor);
|
|
265
|
+
const start = Number(rec.start ?? rec.startLine);
|
|
266
|
+
const end = Number(rec.end ?? rec.endLine);
|
|
267
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start) {
|
|
268
|
+
throw new Error(`items[${index}].anchor \u884C\u53F7\u65E0\u6548\uFF08\u9700 1 \u2264 start \u2264 end\uFF09`);
|
|
269
|
+
}
|
|
270
|
+
return { start, end };
|
|
271
|
+
}
|
|
272
|
+
function parseKind(raw, index) {
|
|
273
|
+
const key = String(raw ?? "").trim();
|
|
274
|
+
const kind = KIND_MAP[key] ?? KIND_MAP[key.toLowerCase()];
|
|
275
|
+
if (!kind) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`items[${index}].kind \u65E0\u6548\uFF0C\u5E94\u4E3A clarify | difficulty | business | coordination`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return kind;
|
|
281
|
+
}
|
|
282
|
+
function parseStructuredReviewYaml(raw, cliModel) {
|
|
283
|
+
let doc;
|
|
284
|
+
try {
|
|
285
|
+
doc = parse(raw);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
288
|
+
throw new Error(`YAML \u89E3\u6790\u5931\u8D25\uFF1A${msg}`);
|
|
289
|
+
}
|
|
290
|
+
const root = asRecord(doc);
|
|
291
|
+
const reviewer = asRecord(root.reviewer ?? {});
|
|
292
|
+
const stance = String(reviewer.stance ?? root.stance ?? "").trim();
|
|
293
|
+
if (!STANCE_VALUES.has(stance)) {
|
|
294
|
+
throw new Error("reviewer.stance \u5FC5\u987B\u4E3A frontend\u3001backend \u6216 fullstack");
|
|
295
|
+
}
|
|
296
|
+
const modelFromYaml = reviewer.model != null ? String(reviewer.model).trim() : "";
|
|
297
|
+
const model = cliModel?.trim() || modelFromYaml || null;
|
|
298
|
+
const itemsRaw = root.items;
|
|
299
|
+
if (!Array.isArray(itemsRaw) || itemsRaw.length === 0) {
|
|
300
|
+
throw new Error("items \u4E0D\u80FD\u4E3A\u7A7A");
|
|
301
|
+
}
|
|
302
|
+
const items = itemsRaw.map((entry, index) => {
|
|
303
|
+
const item = asRecord(entry);
|
|
304
|
+
const anchor = parseAnchor(item.anchor ?? item, index);
|
|
305
|
+
const kind = parseKind(item.kind, index);
|
|
306
|
+
const body = typeof item.body === "string" ? item.body.trim() : String(item.body ?? "").trim();
|
|
307
|
+
if (!body) {
|
|
308
|
+
throw new Error(`items[${index}].body \u4E0D\u80FD\u4E3A\u7A7A`);
|
|
309
|
+
}
|
|
310
|
+
if (kind === "COORDINATION" && item.reply != null && String(item.reply).trim()) {
|
|
311
|
+
throw new Error(`items[${index}] \u8054\u8C03\u4F9D\u8D56\uFF08coordination\uFF09\u4E0D\u5141\u8BB8 reply`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
startLine: anchor.start,
|
|
315
|
+
endLine: anchor.end,
|
|
316
|
+
kind,
|
|
317
|
+
body
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return { stance, model, items };
|
|
321
|
+
}
|
|
322
|
+
function inferCommentFormat(filePath, explicit) {
|
|
323
|
+
if (explicit === "structured" || explicit === "legacy") {
|
|
324
|
+
return explicit;
|
|
325
|
+
}
|
|
326
|
+
const lower = filePath.toLowerCase();
|
|
327
|
+
if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
|
|
328
|
+
return "structured";
|
|
329
|
+
}
|
|
330
|
+
return "legacy";
|
|
331
|
+
}
|
|
332
|
+
|
|
240
333
|
// src/commands/comment.ts
|
|
241
|
-
async function runComment(requirementId, file,
|
|
334
|
+
async function runComment(requirementId, file, options) {
|
|
242
335
|
const cfg = await ensureLoggedConfig();
|
|
243
|
-
const
|
|
336
|
+
const resolvedPath = resolveCwdPath(file);
|
|
337
|
+
const raw = readFileSync2(resolvedPath, "utf8");
|
|
338
|
+
const format = inferCommentFormat(resolvedPath, options?.format);
|
|
244
339
|
const api = createApmApiClient(cfg);
|
|
340
|
+
if (format === "structured") {
|
|
341
|
+
const parsed = parseStructuredReviewYaml(raw, options?.model);
|
|
342
|
+
const data2 = await api.cliRequirements.commentStructured({
|
|
343
|
+
requirementId,
|
|
344
|
+
stance: parsed.stance,
|
|
345
|
+
model: parsed.model,
|
|
346
|
+
items: parsed.items
|
|
347
|
+
});
|
|
348
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!raw.trim()) {
|
|
352
|
+
throw new Error("\u8BC4\u8BBA\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A");
|
|
353
|
+
}
|
|
245
354
|
const data = await api.cliRequirements.comment({
|
|
246
355
|
requirementId,
|
|
247
|
-
content,
|
|
248
|
-
model
|
|
356
|
+
content: raw,
|
|
357
|
+
model: options?.model
|
|
249
358
|
});
|
|
250
359
|
console.log(JSON.stringify(data, null, 2));
|
|
251
360
|
}
|
|
@@ -481,6 +590,40 @@ async function runUploadArtifact(requirementId, workspaceDir) {
|
|
|
481
590
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
482
591
|
import { join as join5 } from "path";
|
|
483
592
|
import { stringify as yamlStringify } from "yaml";
|
|
593
|
+
|
|
594
|
+
// src/pull-reviews-xml.ts
|
|
595
|
+
function escapeForCdata(text) {
|
|
596
|
+
return text.replace(/\]\]>/g, "]]]]><![CDATA[>");
|
|
597
|
+
}
|
|
598
|
+
function buildReviewsXml(reviews) {
|
|
599
|
+
const lines = ["<reviews>"];
|
|
600
|
+
for (const review of reviews) {
|
|
601
|
+
lines.push(
|
|
602
|
+
` <review id="${xmlEscape(review.id)}" model="${xmlEscape(
|
|
603
|
+
review.model ?? ""
|
|
604
|
+
)}" stance="${xmlEscape(review.stance ?? "")}" memberRole="${xmlEscape(
|
|
605
|
+
review.memberRole
|
|
606
|
+
)}">`
|
|
607
|
+
);
|
|
608
|
+
for (const item of review.items) {
|
|
609
|
+
const kind = item.kind.toLowerCase();
|
|
610
|
+
lines.push(
|
|
611
|
+
` <item id="${xmlEscape(item.id)}" start="${item.startLine}" end="${item.endLine}" kind="${xmlEscape(kind)}" status="open">`
|
|
612
|
+
);
|
|
613
|
+
lines.push(
|
|
614
|
+
` <body><![CDATA[${escapeForCdata(item.body ?? "")}]]></body>`
|
|
615
|
+
);
|
|
616
|
+
const reply = item.reply?.trim() ?? "";
|
|
617
|
+
lines.push(` <reply><![CDATA[${escapeForCdata(reply)}]]></reply>`);
|
|
618
|
+
lines.push(" </item>");
|
|
619
|
+
}
|
|
620
|
+
lines.push(" </review>");
|
|
621
|
+
}
|
|
622
|
+
lines.push("</reviews>", "");
|
|
623
|
+
return lines.join("\n");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/commands/pull.ts
|
|
484
627
|
var PULL_ARTIFACT_FILE_NAMES = ["api.md", "backend.md"];
|
|
485
628
|
function normalizeArtifactPath(fileName) {
|
|
486
629
|
return fileName.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
@@ -547,7 +690,7 @@ function unknownArrayToXml(rootName, itemName, items) {
|
|
|
547
690
|
lines.push(`</${rootName}>`, "");
|
|
548
691
|
return lines.join("\n");
|
|
549
692
|
}
|
|
550
|
-
function
|
|
693
|
+
function escapeForCdata2(text) {
|
|
551
694
|
return text.replace(/\]\]>/g, "]]]]><![CDATA[>");
|
|
552
695
|
}
|
|
553
696
|
function defectsToXml(defects) {
|
|
@@ -559,12 +702,12 @@ function defectsToXml(defects) {
|
|
|
559
702
|
lines.push(` <defect id="${xmlEscape(d.id)}">`);
|
|
560
703
|
lines.push(` <status>${xmlEscape(d.status)}</status>`);
|
|
561
704
|
lines.push(
|
|
562
|
-
` <current><![CDATA[${
|
|
705
|
+
` <current><![CDATA[${escapeForCdata2(
|
|
563
706
|
d.currentState ?? ""
|
|
564
707
|
)}]]></current>`
|
|
565
708
|
);
|
|
566
709
|
lines.push(
|
|
567
|
-
` <expected><![CDATA[${
|
|
710
|
+
` <expected><![CDATA[${escapeForCdata2(
|
|
568
711
|
d.expectedEffect ?? ""
|
|
569
712
|
)}]]></expected>`
|
|
570
713
|
);
|
|
@@ -596,25 +739,7 @@ async function runPull(requirementId, workspaceDir) {
|
|
|
596
739
|
"utf8"
|
|
597
740
|
);
|
|
598
741
|
writeFileSync3(join5(WORKITEMS_DIR, "prd.md"), req2.content || "", "utf8");
|
|
599
|
-
const
|
|
600
|
-
const reviewsXml = [
|
|
601
|
-
"<reviews>",
|
|
602
|
-
...reviews.map((r) => {
|
|
603
|
-
return [
|
|
604
|
-
` <review id="${xmlEscape(r.id)}">`,
|
|
605
|
-
` <model>${xmlEscape(r.model ?? "")}</model>`,
|
|
606
|
-
` <content>`,
|
|
607
|
-
`${xmlEscape(r.content ?? "")}`,
|
|
608
|
-
` </content>`,
|
|
609
|
-
` <reply>`,
|
|
610
|
-
`${xmlEscape(r.reply ?? "")}`,
|
|
611
|
-
` </reply>`,
|
|
612
|
-
" </review>"
|
|
613
|
-
].join("\n");
|
|
614
|
-
}),
|
|
615
|
-
"</reviews>",
|
|
616
|
-
""
|
|
617
|
-
].join("\n");
|
|
742
|
+
const reviewsXml = buildReviewsXml(data.reviews ?? []);
|
|
618
743
|
writeFileSync3(join5(WORKITEMS_DIR, "reviews.xml"), reviewsXml, "utf8");
|
|
619
744
|
const defectsXml = defectsToXml(data.defects ?? []);
|
|
620
745
|
writeFileSync3(join5(WORKITEMS_DIR, "defect.xml"), defectsXml, "utf8");
|
|
@@ -2173,9 +2298,20 @@ function buildProgram() {
|
|
|
2173
2298
|
).action(async (requirementId, opts) => {
|
|
2174
2299
|
await runBranch(requirementId, { message: opts.message });
|
|
2175
2300
|
});
|
|
2176
|
-
program.command("comment").description(
|
|
2301
|
+
program.command("comment").description(
|
|
2302
|
+
"\u63D0\u4EA4\u9700\u6C42\u8BC4\u5BA1\uFF1Alegacy \u4E3A\u6574\u7BC7 Markdown\uFF08POST \u2026/comment\uFF09\uFF1Bstructured \u4E3A\u884C\u7EA7 YAML\uFF08POST \u2026/comment-structured\uFF09"
|
|
2303
|
+
).argument("<requirementId>", "\u9700\u6C42 ID").requiredOption(
|
|
2304
|
+
"--file <path>",
|
|
2305
|
+
"\u8BC4\u8BBA\u6587\u4EF6\u8DEF\u5F84\uFF08.yaml/.yml \u9ED8\u8BA4 structured\uFF09"
|
|
2306
|
+
).option(
|
|
2307
|
+
"--format <format>",
|
|
2308
|
+
"structured | legacy\uFF08\u9ED8\u8BA4 legacy\uFF1B.yaml/.yml \u672A\u6307\u5B9A\u65F6\u89C6\u4E3A structured\uFF09"
|
|
2309
|
+
).option("--model <model>", "\u8BC4\u8BBA\u6A21\u578B\uFF08\u8986\u76D6 YAML reviewer.model\uFF09").action(
|
|
2177
2310
|
async (requirementId, options) => {
|
|
2178
|
-
await runComment(requirementId, options.file,
|
|
2311
|
+
await runComment(requirementId, options.file, {
|
|
2312
|
+
model: options.model,
|
|
2313
|
+
format: options.format
|
|
2314
|
+
});
|
|
2179
2315
|
}
|
|
2180
2316
|
);
|
|
2181
2317
|
program.command("refine").description("POST /api/cli/requirements/refine\uFF08\u6B63\u6587\u6765\u81EA\u6587\u4EF6\uFF09").argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
|
package/package.json
CHANGED
|
@@ -21,12 +21,35 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
|
|
|
21
21
|
|
|
22
22
|
正文骨架、需求点写法、示例与落盘自检见 **[apm-refine-template.md](./apm-refine-template.md)**。
|
|
23
23
|
|
|
24
|
+
## `reviews.xml` 格式(v2)
|
|
25
|
+
|
|
26
|
+
`apm pull` 生成的 `reviews.xml` 为**行级条目**,仅含 **OPEN** 状态、且 **有 `reply` 的条目**才需要合并进正文(无 `reply` 或空 `reply` 的条目本次不处理)。
|
|
27
|
+
|
|
28
|
+
```xml
|
|
29
|
+
<reviews>
|
|
30
|
+
<review id="..." model="Auto" stance="frontend" memberRole="FE">
|
|
31
|
+
<item id="..." start="44" end="55" kind="clarify" status="open">
|
|
32
|
+
<body><![CDATA[评审正文…]]></body>
|
|
33
|
+
<reply><![CDATA[产品已拍板口径…]]></reply>
|
|
34
|
+
</item>
|
|
35
|
+
</review>
|
|
36
|
+
</reviews>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| 属性 / 节点 | 含义 |
|
|
40
|
+
| --------------- | --------------------------------------------------------------------------------------- |
|
|
41
|
+
| `start` / `end` | 锚定 `prd.md` 行号(1-based,闭区间),合并时优先据此定位段落 |
|
|
42
|
+
| `kind` | `clarify` / `difficulty` / `business` / `coordination`;**仅处理含非空 `reply` 的条目** |
|
|
43
|
+
| `body` | 评审意见(辅助理解,不作为正文来源) |
|
|
44
|
+
| `reply` | 产品回复,**并入正文**的权威补充 |
|
|
45
|
+
|
|
24
46
|
## 合并原则
|
|
25
47
|
|
|
26
|
-
1. **正文 = 需求原文 +
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
48
|
+
1. **正文 = 需求原文 + 已拍板补充**:将各 `<item>` 中非空 `reply` 并入 `prd.md` 对应行区间(或该行所在需求点);未回复的评审本次不处理。
|
|
49
|
+
2. **定位**:优先按 `start`–`end` 行号找到段落;行号区间与章节标题不一致时,以**业务语义**归入最近的需求点,**不要**机械插入到错误章节。
|
|
50
|
+
3. **冲突**:`reply` 与原文冲突时以 `reply` 为准;`coordination` 类条目通常无 `reply`,润色时忽略。
|
|
51
|
+
4. **同一议题只写一处**;合并后篇幅短于或接近原文。
|
|
52
|
+
5. **图片**:原文图片语法原样保留;理解图片后把规则写入对应需求点的文字描述。
|
|
30
53
|
|
|
31
54
|
## 执行步骤
|
|
32
55
|
|
|
@@ -35,22 +58,22 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
|
|
|
35
58
|
1. **Read** `.apm/workitems/<需求ID>/prd.md`
|
|
36
59
|
2. **Read** `.apm/workitems/<需求ID>/reviews.xml`(不存在则视为无评审)
|
|
37
60
|
3. **理解 PRD 中的图片**(有则执行):
|
|
38
|
-
- 图片为标准 Markdown:``
|
|
39
|
-
- **优先读本地附件**:`.apm/workitems/<需求ID
|
|
40
|
-
-
|
|
41
|
-
-
|
|
61
|
+
- 图片为标准 Markdown:``
|
|
62
|
+
- **优先读本地附件**:`.apm/workitems/<需求ID>/<相对路径>`
|
|
63
|
+
- **本地不存在时**:再用展示 URL 下载后理解
|
|
64
|
+
- 回写时保留原 `` 语法不变
|
|
42
65
|
|
|
43
66
|
`prd.md` 不可读 → 终止,步骤 5 注明原因。
|
|
44
67
|
|
|
45
68
|
### 步骤 2:润色
|
|
46
69
|
|
|
47
|
-
**不向用户追问。** `reply`
|
|
70
|
+
**不向用户追问。** 非空 `reply` 视为已拍板补充。
|
|
48
71
|
|
|
49
|
-
1.
|
|
50
|
-
2. **Read** **[apm-refine-template.md](./apm-refine-template.md)
|
|
51
|
-
3. 写完后按模板 **「落盘前自检」**
|
|
52
|
-
4.
|
|
53
|
-
5.
|
|
72
|
+
1. 遍历 `reviews.xml` 中每个 `<item>`:若 `<reply>` 非空,将口径并入 `prd.md` 对应段落(参考 `start`/`end`)。
|
|
73
|
+
2. **Read** **[apm-refine-template.md](./apm-refine-template.md)**,按骨架组织全文。
|
|
74
|
+
3. 写完后按模板 **「落盘前自检」** 核对。
|
|
75
|
+
4. **待确认**:仅当补充信息原文含「待定」「再议」等时追加该章。
|
|
76
|
+
5. **局部替换**:只改涉及段落时,保留其余章节。
|
|
54
77
|
|
|
55
78
|
### 步骤 3:回写
|
|
56
79
|
|
|
@@ -60,6 +83,8 @@ description: 根据需求 ID 读取 prd.md 与 reviews.xml,润色为简短、
|
|
|
60
83
|
|
|
61
84
|
仓库根目录执行:`apm refine <需求ID>`
|
|
62
85
|
|
|
86
|
+
(平台会将旧正文追加到 `contentHistory`,并将全部 OPEN 评审标为已解决;侧栏不再展示这些条目。)
|
|
87
|
+
|
|
63
88
|
### 步骤 5:回复用户
|
|
64
89
|
|
|
65
90
|
**仅**输出一张状态表,表格外无其他文字:
|
|
@@ -9,17 +9,17 @@ description: 结合本仓库上下文对需求做结构化评审,只有当用
|
|
|
9
9
|
|
|
10
10
|
## 评审目标
|
|
11
11
|
|
|
12
|
-
帮助产品经理**梳理需求边界、发现文档中未写清的口径**,并在实现难度较高时给出提示。**不是**为了凑问题而提问——PRD
|
|
12
|
+
帮助产品经理**梳理需求边界、发现文档中未写清的口径**,并在实现难度较高时给出提示。**不是**为了凑问题而提问——PRD 已足够清晰且无高难度改造时,可只输出少量锚定条目,不必强行列出待澄清项。
|
|
13
13
|
|
|
14
14
|
## 评审立场
|
|
15
15
|
|
|
16
|
-
每轮评审从 **前端 / 后端 / 全栈**
|
|
16
|
+
每轮评审从 **前端 / 后端 / 全栈** 中择定一种立场,按该视角输出 **YAML 行级评审**,并执行一次 `apm comment --format=structured`。
|
|
17
17
|
|
|
18
18
|
### 如何确定立场
|
|
19
19
|
|
|
20
20
|
1. **用户明示**:用户说明「前端 / 后端 / 全栈 / 仅 UI / 仅接口」等,以用户为准。
|
|
21
|
-
2. **用户未说明**:阅读仓库中与该需求相关的现有代码,推断评审者更贴近哪一端;仍无法判断时,**默认前端**,并在
|
|
22
|
-
3.
|
|
21
|
+
2. **用户未说明**:阅读仓库中与该需求相关的现有代码,推断评审者更贴近哪一端;仍无法判断时,**默认前端**,并在 `reviewer.stance` 使用 `frontend`。
|
|
22
|
+
3. **全栈**:在同一轮评审中同时覆盖 UI 与流程/数据关注点;同一业务点若需澄清,合并为**一条** `clarify`(不要拆成两句重复问题)。
|
|
23
23
|
|
|
24
24
|
### 各端关注点(互斥优先,减少并行评审重复)
|
|
25
25
|
|
|
@@ -31,37 +31,34 @@ description: 结合本仓库上下文对需求做结构化评审,只有当用
|
|
|
31
31
|
|
|
32
32
|
**去重规则**(按立场选用):
|
|
33
33
|
|
|
34
|
-
- **前端立场**:只写界面与交互侧待澄清,不写落库/API
|
|
34
|
+
- **前端立场**:只写界面与交互侧待澄清,不写落库/API 契约类问题(联调诉求用 `kind: coordination`)。
|
|
35
35
|
- **后端立场**:只写流程、规则与数据侧待澄清,不写控件选型、Tab 布局等纯 UI 问题。
|
|
36
|
-
-
|
|
36
|
+
- **全栈立场**:同一业务点合并为一条 `clarify`,**不要**拆成两句意思重复的问题。
|
|
37
37
|
|
|
38
38
|
## 表述规范(产品经理可读)
|
|
39
39
|
|
|
40
|
-
1.
|
|
40
|
+
1. **产品语言优先**:用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体(行号锚定除外)。
|
|
41
41
|
2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
|
|
42
|
-
3. **锚定 PRD
|
|
43
|
-
4. **问题 = 文档缺口**:只写 PRD
|
|
42
|
+
3. **锚定 PRD**:每条 `items[]` 必须写 **`anchor: { start, end }`**,与 Read 读到的 `prd.md` 行号一致;**不**在 `body` 里复述该段需求原文。
|
|
43
|
+
4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点。已写清楚的规则不要重复质疑。
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## 不在「待澄清」里写的内容
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
以下不得使用 `kind: clarify`,若需记录则用 **`kind: coordination`**(且必须有行号锚定):
|
|
48
48
|
|
|
49
49
|
- 接口字段名、请求体结构、子表编码
|
|
50
50
|
- 前后端谁传哪个参数、现有 edit 接口是否够用
|
|
51
51
|
- 与现网代码实现对齐的改造方案
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
`coordination` **不用产品回复**,仅作记录,待技术评审阶段处理。若同一行区间既要澄清又要联调,**拆成两条** `items`。
|
|
54
54
|
|
|
55
55
|
## 评审原则
|
|
56
56
|
|
|
57
|
-
1.
|
|
58
|
-
2. **与仓库对齐(能力边界)**:阅读 `AGENTS.md`、`.apm/product-capability-inventory`
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
61
|
-
|
|
62
|
-
- 实现难度**高**时额外写 `- **实现难度高:**`(用产品语言简述原因);一般难度**不写**该字段。
|
|
63
|
-
- **高难度**指:对现有代码改造面大(涉及文件/模块较多),或业务逻辑变化影响较大。
|
|
64
|
-
5. **澄清优先但不泛问**:不阻塞理解则不提问;不问可从 PRD 或常识推断的琐碎点。
|
|
57
|
+
1. **产品视角优先**:用户侧影响、业务闭环、边界与风险。
|
|
58
|
+
2. **与仓库对齐(能力边界)**:阅读 `AGENTS.md`、`.apm/product-capability-inventory` 及与需求相关的代码,判断改造面。**禁止**用代码证明 PRD 对错;口径不清归入 `clarify`。
|
|
59
|
+
3. **`kind: business`**:仅当方案与业务目标明显冲突或明显非较优解时使用。
|
|
60
|
+
4. **`kind: difficulty`**:仅当改造面大或业务逻辑影响大时使用。
|
|
61
|
+
5. **澄清优先但不泛问**:不阻塞理解则不提问。
|
|
65
62
|
|
|
66
63
|
## 执行步骤
|
|
67
64
|
|
|
@@ -69,35 +66,36 @@ description: 结合本仓库上下文对需求做结构化评审,只有当用
|
|
|
69
66
|
|
|
70
67
|
1. 使用 **Read** 读取 `.apm/workitems/<需求ID>/prd.md`。
|
|
71
68
|
2. 当 PRD 涉及到图片链接时,自动下载对应的图片,存放在 `.apm/workitems/<需求ID>/images/<对应图片名称>.png`,并理解图片内容。
|
|
72
|
-
3. 若 Read
|
|
69
|
+
3. 若 Read **失败**:本步骤记为失败,**终止**,不执行后续步骤。
|
|
73
70
|
|
|
74
71
|
### 步骤 2:读代码
|
|
75
72
|
|
|
76
|
-
阅读仓库源码以及 `.apm/product-capability-inventory`
|
|
73
|
+
阅读仓库源码以及 `.apm/product-capability-inventory` 中与需求相关的条目,用于**理解现网能力与边界、判断改造面**。
|
|
77
74
|
|
|
78
75
|
### 步骤 3:评审与提交
|
|
79
76
|
|
|
80
|
-
1.
|
|
81
|
-
2.
|
|
82
|
-
3. **成文**:按 **[output-template.md](./output-template.md)**
|
|
83
|
-
4. 使用 **Write**
|
|
77
|
+
1. **锁定立场**,填入 YAML `reviewer.stance`(`frontend` / `backend` / `fullstack`)。
|
|
78
|
+
2. **拆条**:每条对应 `prd.md` 连续行号;按需设置 `kind`(`clarify` / `difficulty` / `business` / `coordination`)与 `body`。
|
|
79
|
+
3. **成文**:按 **[output-template.md](./output-template.md)** 写 YAML。
|
|
80
|
+
4. 使用 **Write** 写入临时文件(建议 `/tmp/apm-review-<需求ID>.yaml`)。
|
|
84
81
|
5. **自检**:
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
6.
|
|
82
|
+
- 每条均有 `anchor`,且无全篇评审;
|
|
83
|
+
- `stance` 与立场一致;
|
|
84
|
+
- `clarify` 均为产品可回答的业务问题;
|
|
85
|
+
- `coordination` 无 `reply` 字段;
|
|
86
|
+
- 无重复语义的条目。
|
|
87
|
+
6. 在项目根目录执行:
|
|
88
|
+
|
|
89
|
+
`apm comment <需求ID> --file=<临时文件绝对路径> --format=structured --model=<模型名>`
|
|
90
|
+
|
|
91
91
|
7. 命令结束后 **删除临时文件**(失败时若需排错可保留,默认仍删除)。
|
|
92
92
|
|
|
93
93
|
### 步骤 4:回复用户
|
|
94
94
|
|
|
95
|
-
对用户可见回复 **仅限一张 Markdown
|
|
96
|
-
|
|
97
|
-
建议表头:
|
|
95
|
+
对用户可见回复 **仅限一张 Markdown 表格**,汇总各步**结果**。**不得**在表格外输出其他内容。
|
|
98
96
|
|
|
99
|
-
| 步骤 | 结果 | 说明
|
|
100
|
-
| ----------------- | ----------- |
|
|
101
|
-
| 1. 读取 prd.md | 成功 / 失败 | 实际路径;失败时写原因
|
|
102
|
-
| 2. 评审与 comment | 成功 / 失败 |
|
|
103
|
-
| 3. 清理临时文件 | 成功 / 失败 | —
|
|
97
|
+
| 步骤 | 结果 | 说明 |
|
|
98
|
+
| ----------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
99
|
+
| 1. 读取 prd.md | 成功 / 失败 | 实际路径;失败时写原因 |
|
|
100
|
+
| 2. 评审与 comment | 成功 / 失败 | 临时文件;`apm comment` 情况;**注明 stance**(frontend / backend / fullstack) |
|
|
101
|
+
| 3. 清理临时文件 | 成功 / 失败 | — |
|
|
@@ -1,75 +1,64 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
- {为何与业务目标冲突或方案非较优;可给替代思路}
|
|
39
|
-
|
|
40
|
-
- **待澄清:**
|
|
41
|
-
- {若仍有文档缺口}
|
|
42
|
-
|
|
43
|
-
### 需求点 5
|
|
44
|
-
|
|
45
|
-
- **锚定:** `prd.md` L108–L132
|
|
46
|
-
|
|
47
|
-
- **实现难度高:**
|
|
48
|
-
- {用产品语言说明:改造涉及哪些页面/流程/规则,为何面大;示例:加/减分明细需按审批层级分块改造,涉及弹窗表单、子表编辑与汇总多处联动,业务规则变化面大}
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 联调依赖项
|
|
53
|
-
|
|
54
|
-
{**仅当前端评审**且存在联调前置信息时出现;**可选**,无则省略整节}
|
|
55
|
-
|
|
56
|
-
> 不用产品回复,仅记录,待技术评审阶段处理。
|
|
1
|
+
# 评审输出模板(YAML · structured)
|
|
2
|
+
|
|
3
|
+
使用 **YAML** 文件提交行级评审;每条必须锚定 `prd.md` 行号。全文使用**产品经理可读**的表述;**不**在 `body` 里复述需求原文。
|
|
4
|
+
|
|
5
|
+
## 文件示例
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
reviewer:
|
|
9
|
+
model: Auto
|
|
10
|
+
stance: frontend # frontend | backend | fullstack
|
|
11
|
+
|
|
12
|
+
items:
|
|
13
|
+
- anchor: { start: 44, end: 55 }
|
|
14
|
+
kind: clarify
|
|
15
|
+
body: |
|
|
16
|
+
- 「类型」隐藏后,统计分析页的「分类」筛选是否一并去掉,导出是否统一走临床模板
|
|
17
|
+
|
|
18
|
+
- anchor: { start: 59, end: 67 }
|
|
19
|
+
kind: clarify
|
|
20
|
+
body: |
|
|
21
|
+
- (PRD 已写清且无缺口时可省略该条,不要写「仅锚定」空条目)
|
|
22
|
+
|
|
23
|
+
- anchor: { start: 83, end: 106 }
|
|
24
|
+
kind: business
|
|
25
|
+
body: |
|
|
26
|
+
- 方案与业务目标冲突的原因;可给替代思路
|
|
27
|
+
|
|
28
|
+
- anchor: { start: 108, end: 132 }
|
|
29
|
+
kind: difficulty
|
|
30
|
+
body: |
|
|
31
|
+
- 用产品语言说明改造面:涉及哪些页面/流程/规则,为何面大
|
|
32
|
+
|
|
33
|
+
- anchor: { start: 44, end: 55 }
|
|
34
|
+
kind: coordination
|
|
35
|
+
body: |
|
|
36
|
+
- 需与后端对齐的接口/字段诉求(不用产品回复)
|
|
37
|
+
```
|
|
57
38
|
|
|
58
|
-
|
|
39
|
+
## kind 与技能表述
|
|
59
40
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
| `kind`(YAML 小写) | 含义 | 说明 |
|
|
42
|
+
| ------------------- | ---------- | ---------------------------- |
|
|
43
|
+
| `clarify` | 待澄清 | 文档未写清、影响本端理解边界 |
|
|
44
|
+
| `difficulty` | 实现难度高 | 仅改造面大或业务逻辑影响大时 |
|
|
45
|
+
| `business` | 业务合理性 | 仅方案明显不合理时 |
|
|
46
|
+
| `coordination` | 联调依赖 | **禁止** `reply`;待技术评审 |
|
|
63
47
|
|
|
64
48
|
## 字段规则
|
|
65
49
|
|
|
50
|
+
| 字段 | 规则 |
|
|
51
|
+
| ----------------- | ---------------------------------------------------------------- |
|
|
52
|
+
| `reviewer.stance` | 必填:`frontend` / `backend` / `fullstack` |
|
|
53
|
+
| `reviewer.model` | 建议填写;可被 CLI `--model` 覆盖 |
|
|
54
|
+
| `anchor` | 必填 `{ start, end }`,1-based 闭区间,与 Read `prd.md` 行号一致 |
|
|
55
|
+
| `body` | 必填;产品语言;**禁止**全篇评审(必须有行号) |
|
|
56
|
+
| 同区间多类型 | 拆成多条 `items`(例如同时 `clarify` 与 `coordination` 各一条) |
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
| --------- | -------------------------------------------------------------- |
|
|
69
|
-
| **锚定** | 每条必有;写 `prd.md` L{起}–L{止},与 Read 工具读到的行号一致;**不**概括、**不**复述该段需求 |
|
|
70
|
-
| **待澄清** | 按需;无缺口则**不写**(不要写「无」) |
|
|
71
|
-
| **实现难度高** | 按需;**仅**改造面大或业务逻辑影响大时写;一般难度**不写** |
|
|
72
|
-
| **业务合理性** | 按需;仅方案明显不合理时出现 |
|
|
73
|
-
| **联调依赖项** | 全文最多一节;**不算待澄清**;不用产品回复,仅记录,待技术评审阶段处理 |
|
|
58
|
+
## 提交命令
|
|
74
59
|
|
|
60
|
+
```bash
|
|
61
|
+
apm comment <需求ID> --file=/tmp/apm-review-<需求ID>.yaml --format=structured --model=<模型名>
|
|
62
|
+
```
|
|
75
63
|
|
|
64
|
+
`.yaml` / `.yml` 扩展名在未指定 `--format` 时默认按 structured 解析。
|