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, model) {
334
+ async function runComment(requirementId, file, options) {
242
335
  const cfg = await ensureLoggedConfig();
243
- const content = readFileSync2(resolveCwdPath(file), "utf8");
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 escapeForCdata(text) {
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[${escapeForCdata(
705
+ ` <current><![CDATA[${escapeForCdata2(
563
706
  d.currentState ?? ""
564
707
  )}]]></current>`
565
708
  );
566
709
  lines.push(
567
- ` <expected><![CDATA[${escapeForCdata(
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 reviews = data.reviews ?? [];
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("POST /api/cli/requirements/comment\uFF08\u6B63\u6587\u6765\u81EA\u6587\u4EF6\uFF09").argument("<requirementId>", "\u9700\u6C42 ID").requiredOption("--file <path>", "\u8BC4\u8BBA\u6B63\u6587\u6587\u4EF6\u8DEF\u5F84").option("--model <model>", "\u8BC4\u8BBA\u6A21\u578B").action(
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, options.model);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "4.0.9",
3
+ "version": "4.0.11",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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. **正文 = 需求原文 + 已拍板补充**:合并 `prd.md` `reviews.xml` `reply` 的明确口径;未说到的保持原文或不写,不自行扩需求。
27
- 2. **评审辅助理解 `reply`**;与补充冲突时以补充为准;未回应的评审本次不处理。
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:`![描述](展示URL "相对路径")` — 圆括号内为展示 URL,**引号内**为相对路径(如 `attachments/图1.png`)
39
- - **优先读本地附件**:`.apm/workitems/<需求ID>/<相对路径>`;存在则用该文件理解图片内容
40
- - **本地不存在时**:再用圆括号内的展示 URL 下载图片后理解
41
- - 将理解到的界面/字段/交互规则写入对应需求点;回写时保留原 `![…](… "…")` 语法不变
61
+ - 图片为标准 Markdown:`![描述](展示URL "相对路径")`
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. `reply` 中已拍板内容并入正文对应需求点。
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
- 每轮评审从 **前端 / 后端 / 全栈** 中择定一种立场,按该视角撰写一篇评审正文,并执行一次 `apm comment`。
16
+ 每轮评审从 **前端 / 后端 / 全栈** 中择定一种立场,按该视角输出 **YAML 行级评审**,并执行一次 `apm comment --format=structured`。
17
17
 
18
18
  ### 如何确定立场
19
19
 
20
20
  1. **用户明示**:用户说明「前端 / 后端 / 全栈 / 仅 UI / 仅接口」等,以用户为准。
21
- 2. **用户未说明**:阅读仓库中与该需求相关的现有代码,推断评审者更贴近哪一端;仍无法判断时,**默认前端**,并在 `### 评审人` 中注明「未指定立场,按代码推断为 xxx」。
22
- 3. **全栈**:在同一篇评审中同时覆盖 UI 与流程/数据关注点;同一业务点合并为一条待澄清项(见下文去重规则)。
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
- - **全栈立场**:同一业务点若 UI 与规则都涉及,合并为**一条**待澄清(优先产品能直接回答的业务表述),**不要**拆成两句意思重复的问题。
36
+ - **全栈立场**:同一业务点合并为一条 `clarify`,**不要**拆成两句意思重复的问题。
37
37
 
38
38
  ## 表述规范(产品经理可读)
39
39
 
40
- 1. **产品语言优先**:用「医德考评弹窗」「行风办审批页」「列表页分类筛选」等说法;**禁止**用文件路径、组件名、函数名作为论据或问题主体(`prd.md` 行号锚定除外)。
40
+ 1. **产品语言优先**:用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体(行号锚定除外)。
41
41
  2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
42
- 3. **锚定 PRD**:每条评审用 **`prd.md` 行号**指回原文(如 `prd.md` L44–L55),**不**另写半句概括或复述需求内容。
43
- 4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点(例如:只说了改字段名,没说哪些页面/弹窗要改;只说了新增 Tab,没说各审批节点能否编辑)。已写清楚的规则不要重复质疑。
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
- 若**前端**评审时确知实现依赖某些后端信息,可在全文末尾单独增加 **`## 联调依赖项`**(见 `output-template.md`),**简短**记录接口/字段等诉求;**不用产品回复,仅作记录,待技术评审阶段处理**。
53
+ `coordination` **不用产品回复**,仅作记录,待技术评审阶段处理。若同一行区间既要澄清又要联调,**拆成两条** `items`。
54
54
 
55
55
  ## 评审原则
56
56
 
57
- 1. **产品视角优先**:用户侧影响、业务闭环、边界与风险;避免术语堆砌。
58
- 2. **与仓库对齐(能力边界)**:阅读 `AGENTS.md`、`.apm/product-capability-inventory` 及与需求相关的代码,判断需求是否超出现有能力。**禁止**用代码证明 PRD 对错;**禁止**根据代码**猜测** PRD 未写清的口径——口径不清归入「待澄清」。
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. **锁定立场**(见上文「评审立场」),在正文顶格写 `### 评审人` + 模型名 + 立场(如「Auto(前端)」)。
81
- 2. **拆条**:按 PRD 章节拆条(标题序号与 PRD 一致即可);每条写 **`prd.md` 行号锚定**,按需追加待澄清、实现难度高、业务合理性等字段,**不**复述该段需求原文。
82
- 3. **成文**:按 **[output-template.md](./output-template.md)** Markdown;**不含**「隐性知识沉淀」段落。
83
- 4. 使用 **Write** 写入临时文件(建议绝对路径 `/tmp/apm-review-<需求ID>.md`)。
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. 在项目根目录执行一次:`apm comment <需求ID> --file=<临时文件绝对路径> --model=<评论使用的模型名称>`。
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 表格**,汇总各步**结果**(成功/失败、摘要)。**不得**在表格外输出任何其他内容(完整评审正文已通过 `apm comment` 提交)。
96
-
97
- 建议表头:
95
+ 对用户可见回复 **仅限一张 Markdown 表格**,汇总各步**结果**。**不得**在表格外输出其他内容。
98
96
 
99
- | 步骤 | 结果 | 说明 |
100
- | ----------------- | ----------- | ---------------------------------------------------------------------------------------------- |
101
- | 1. 读取 prd.md | 成功 / 失败 | 实际路径;失败时写原因 |
102
- | 2. 评审与 comment | 成功 / 失败 | 临时文件(已删可写「已清理」);`apm 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
- 顶格写评审人,随后按 PRD 章节逐条输出。全文使用**产品经理可读**的表述;锚定 PRD 时**只写行号**,不复述需求。
4
-
5
- ```markdown
6
- ### 评审人
7
-
8
- {模型名}({前端 / 后端 / 全栈})
9
-
10
- ### 需求点 1
11
-
12
- - **锚定:** `prd.md` L44–L55
13
-
14
- - **待澄清:**
15
- - {文档未写清、影响本端理解边界的点;无则整段省略}
16
- - {示例(前端):「类型」隐藏后,统计分析页的「分类」筛选是否一并去掉,导出是否统一走临床模板}
17
- - {示例(后端):新建单据「默认临床」对应字典编码/文案未写明,历史非临床记录在列表是否仍展示原类型}
18
-
19
- ### 需求点 2
20
-
21
- - **锚定:** `prd.md` L59–L67
22
-
23
- {PRD 已写清且改造面不大 仅锚定,不写其他字段}
24
-
25
- ### 需求点 3
26
-
27
- - **锚定:** `prd.md` L69–L81
28
-
29
- - **待澄清:**
30
- - {仅列本端视角的缺口}
31
-
32
- ### 需求点 4(业务合理性:仅不合理时出现)
33
-
34
- - **锚定:** `prd.md` L83–L106
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 解析。