sdd-flow-kit 1.3.6 → 1.3.12

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.
@@ -47,10 +47,16 @@ const projectRoots_1 = require("../core/projectRoots");
47
47
  const sessionState_1 = require("../core/sessionState");
48
48
  const fs_1 = require("../core/fs");
49
49
  const openspecGate_1 = require("../core/openspecGate");
50
+ const playwrightEnsure_1 = require("../core/playwrightEnsure");
51
+ const prdCoverage_1 = require("../core/prdCoverage");
52
+ const runPrdReview_1 = require("./runPrdReview");
53
+ const prdRemediate_1 = require("../core/prdRemediate");
54
+ const runPrdDiff_1 = require("./runPrdDiff");
50
55
  /** 验收回退最大次数,超过需用户确认 */
51
56
  exports.MAX_VALIDATE_RETRIES = 3;
52
57
  /**
53
- * 验收命令:跑 lint+type+unit(delivery-pipeline:short)+ e2e/Playwright,写入 .validate-pass.json .validate-fail.json。
58
+ * 验收命令:PRD 一致性复查门禁 + lint+type+unit + 强制 E2E(缺 Playwright 则自动安装)。
59
+ * 写入 .validate-pass.json 或 .validate-fail.json。
54
60
  * 验收不通过时回退到 apply_in_progress,超过 MAX_VALIDATE_RETRIES 次则需用户确认。
55
61
  */
56
62
  async function runValidate(options) {
@@ -72,7 +78,7 @@ async function runValidate(options) {
72
78
  }
73
79
  const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
74
80
  const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
75
- const agent = options.agent || "cursor";
81
+ const agent = options.agent || "claude-code";
76
82
  if (options.dryRun) {
77
83
  return { ok: true, detail: `dry-run: would validate in ${pkgRoot} (installRoot=${installRoot})`, needUserConfirm: false };
78
84
  }
@@ -81,6 +87,65 @@ async function runValidate(options) {
81
87
  if (!ensured.ready) {
82
88
  return { ok: false, detail: "ensure-opsx 未就绪", needUserConfirm: false };
83
89
  }
90
+ // ---- 检查0: PRD 一致性复查 + 自动修复循环(最多 3 次)----
91
+ await (0, runPrdReview_1.runPrdReview)({
92
+ projectRoot: options.projectRoot,
93
+ runId: loaded.resolved.runId,
94
+ dryRun: false,
95
+ });
96
+ let needUserConfirmFromRemediate = false;
97
+ const remediateLoop = await (0, prdRemediate_1.runPrdRemediateLoop)({
98
+ projectRoot: pkgRoot,
99
+ outputRoot: loaded.resolved.outputRoot,
100
+ runId: loaded.resolved.runId,
101
+ change: options.change,
102
+ agent,
103
+ dryRun: false,
104
+ maxAttempts: prdRemediate_1.MAX_PRD_FIX_RETRIES,
105
+ runDiff: async () => {
106
+ const diff = await (0, runPrdDiff_1.runPrdDiff)({
107
+ projectRoot: options.projectRoot,
108
+ runId: loaded.resolved.runId,
109
+ dryRun: false,
110
+ });
111
+ return { ok: diff.ok, semantic: diff.semantic };
112
+ },
113
+ });
114
+ if (remediateLoop.escalated) {
115
+ needUserConfirmFromRemediate = true;
116
+ }
117
+ let prdCoverage = await (0, prdCoverage_1.checkPrdCoverage)(loaded.resolved.outputRoot, pkgRoot);
118
+ if (!prdCoverage.ok) {
119
+ const failed = prdCoverage.checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
120
+ const payload = {
121
+ change: options.change,
122
+ verdict: "fail",
123
+ prdCoverageFailed: failed,
124
+ remediateAttempts: remediateLoop.attempts,
125
+ remediateEscalated: remediateLoop.escalated,
126
+ at: new Date().toISOString(),
127
+ };
128
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, ".validate-fail.json"), `${JSON.stringify(payload, null, 2)}\n`);
129
+ if (loaded.session) {
130
+ await (0, sessionState_1.writeSession)(loaded.resolved.outputRoot, {
131
+ ...loaded.session,
132
+ prdFixRetryCount: remediateLoop.attempts,
133
+ status: remediateLoop.escalated ? "awaiting_user_confirmation" : "apply_in_progress",
134
+ });
135
+ }
136
+ return {
137
+ ok: false,
138
+ detail: remediateLoop.escalated
139
+ ? `${remediateLoop.detail}。请处理 08 待人工确认汇总后重试 validate`
140
+ : `prd-coverage 未通过: ${failed.slice(0, 3).join("; ")}`,
141
+ needUserConfirm: needUserConfirmFromRemediate || remediateLoop.needUserConfirm,
142
+ };
143
+ }
144
+ await (0, prdCoverage_1.writePrdCoveragePassMarker)(loaded.resolved.outputRoot, {
145
+ change: options.change,
146
+ atomCount: (_b = (_a = prdCoverage.checks.find((c) => c.name === "prd-atoms")) === null || _a === void 0 ? void 0 : _a.detail) !== null && _b !== void 0 ? _b : "",
147
+ at: new Date().toISOString(),
148
+ });
84
149
  // ---- 检查1: tasks.md 全完成 ----
85
150
  const tasks = await (0, openspecGate_1.checkTasksAllDone)(installRoot, options.change);
86
151
  // ---- 检查2: delivery-pipeline:short(lint + type check + unit test) ----
@@ -89,43 +154,13 @@ async function runValidate(options) {
89
154
  stdio: "inherit",
90
155
  shell: process.platform === "win32",
91
156
  });
92
- const shortExitCode = (_a = shortRet.status) !== null && _a !== void 0 ? _a : 1;
93
- // ---- 检查3: e2e / Playwright ----
94
- let e2eExitCode = 0;
95
- let e2eDetail = "e2e 未执行";
96
- const hasPlaywrightScript = await hasScript(pkgRoot, "test:e2e");
97
- if (hasPlaywrightScript) {
98
- const e2eRet = (0, child_process_1.spawnSync)("pnpm", ["run", "test:e2e"], {
99
- cwd: pkgRoot,
100
- stdio: "inherit",
101
- shell: process.platform === "win32",
102
- env: { ...process.env, CI: "1" },
103
- });
104
- e2eExitCode = (_b = e2eRet.status) !== null && _b !== void 0 ? _b : 1;
105
- e2eDetail = e2eExitCode === 0 ? "e2e/Playwright 全通过" : `e2e/Playwright 退出码 ${e2eExitCode}`;
106
- }
107
- else {
108
- // 尝试直接用 npx playwright test
109
- const hasPlaywrightCli = (0, child_process_1.spawnSync)("npx", ["playwright", "--version"], {
110
- cwd: pkgRoot,
111
- stdio: "ignore",
112
- }).status === 0;
113
- if (hasPlaywrightCli) {
114
- const e2eRet = (0, child_process_1.spawnSync)("npx", ["playwright", "test"], {
115
- cwd: pkgRoot,
116
- stdio: "inherit",
117
- shell: process.platform === "win32",
118
- env: { ...process.env, CI: "1" },
119
- });
120
- e2eExitCode = (_c = e2eRet.status) !== null && _c !== void 0 ? _c : 1;
121
- e2eDetail = e2eExitCode === 0 ? "Playwright test 全通过" : `Playwright test 退出码 ${e2eExitCode}`;
122
- }
123
- else {
124
- e2eDetail = "未检测到 Playwright,跳过 e2e(建议配置 test:e2e 脚本)";
125
- }
126
- }
157
+ const shortExitCode = (_c = shortRet.status) !== null && _c !== void 0 ? _c : 1;
158
+ // ---- 检查3: 强制 E2E / Playwright(缺则自动安装) ----
159
+ const e2eResult = await (0, playwrightEnsure_1.runMandatoryE2e)(pkgRoot, false);
160
+ const e2eExitCode = e2eResult.exitCode;
161
+ const e2eDetail = e2eResult.detail;
127
162
  // ---- 综合判定 ----
128
- const allPassed = tasks.ok && shortExitCode === 0 && e2eExitCode === 0;
163
+ const allPassed = tasks.ok && shortExitCode === 0 && e2eResult.ok;
129
164
  const verdict = allPassed ? "pass" : "fail";
130
165
  const payload = {
131
166
  change: options.change,
@@ -173,20 +208,11 @@ async function runValidate(options) {
173
208
  needUserConfirm,
174
209
  };
175
210
  }
176
- return { ok: true, detail: "验收通过(lint+type+unit+e2e),可进入交付", needUserConfirm: false };
177
- }
178
- /** 检查 package.json 是否有指定 script */
179
- async function hasScript(pkgRoot, scriptName) {
180
- var _a;
181
- try {
182
- const { readFile: readFileFs } = await Promise.resolve().then(() => __importStar(require("fs/promises")));
183
- const pkgText = await readFileFs(path_1.default.join(pkgRoot, "package.json"), "utf8");
184
- const pkg = JSON.parse(pkgText);
185
- return Boolean((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[scriptName]);
186
- }
187
- catch {
188
- return false;
189
- }
211
+ return {
212
+ ok: true,
213
+ detail: "验收通过(prd-coverage + lint+type+unit + 强制 e2e),可进入交付",
214
+ needUserConfirm: false,
215
+ };
190
216
  }
191
217
  /** 清理验收标记文件,重新实现后需要重新验收 */
192
218
  async function cleanupValidateMarkers(outputRoot) {
@@ -82,6 +82,11 @@ async function startFlowScaffold(options) {
82
82
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "06-openspec-提案草稿.md"), openspecEntryTemplate);
83
83
  const opsxAutoChainTemplate = await (0, templates_1.loadTemplateText)("artifacts/07-opsx-auto-chain.template.md");
84
84
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "07-opsx-自动串联指引.md"), opsxAutoChainTemplate);
85
+ const prdReviewReportTemplate = await (0, templates_1.loadTemplateText)("artifacts/08-PRD一致性复查报告.template.md");
86
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "08-PRD一致性复查报告.md"), prdReviewReportTemplate
87
+ .replace("{{ATOM_COUNT}}", "0")
88
+ .replace("{{REVIEW_TABLE_ROWS}}", "| (运行 prd-review 后生成) | | | | |")
89
+ .replace("{{GENERATED_AT}}", new Date().toISOString()));
85
90
  const session = (0, sessionState_1.defaultSession)({
86
91
  product: options.product,
87
92
  version: options.version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.3.6",
3
+ "version": "1.3.12",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",
@@ -12,14 +12,18 @@ python3 confluence-doc.py {{VERSION_EXAMPLE}}
12
12
 
13
13
  版本号可传:`2.3.2`、`V2.3.2`、`ADI_V2.3.2`、`ADI-V2.3.2`(脚本会归一化)。
14
14
 
15
- 2. 脚本会依次搜索(含用户原样输入):`ADI_V*` → `ADI-V*` → `ADI_*` → `ADI-*` → `ADI_v*` → `ADI-v*` → `ADI V*` → `ADI_V*结算` → `V*` 等。
15
+ 2. 脚本会依次用关键词打开 **Confluence 专用搜索页** `dosearchsite.action?queryString=<关键词>`(非首页顶栏搜索),并依次尝试:`ADI_V*` → `ADI-V*` → `ADI_*` → `ADI-*` → `ADI_v*` → `ADI-v*` → `ADI V*` → `ADI_V*结算` → `V*` 等。
16
16
 
17
- 3. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
17
+ 3. 匹配规则:**标题包含关键词或以关键词为前缀**,或 **URL 路径包含关键词**,任一满足即命中(Confluence 列表标题常被截断)。
18
18
 
19
- 4. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
19
+ 4. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
20
+
21
+ 5. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
20
22
  - `source/PRD.md`
21
23
 
22
- ## 直链兜底(已知 pageId 时推荐)
24
+ ## 直链兜底(可选,非默认)
25
+
26
+ 仅在关键字搜索全部失败、且已知 pageId 时使用:
23
27
 
24
28
  ADI V2.3.4 示例(`yyzcpt` 空间 pageId `109609740`):
25
29
 
@@ -44,8 +48,8 @@ python3 confluence-doc.py 2.3.4
44
48
 
45
49
  ## 失败排查
46
50
 
47
- - 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V`)
48
- - 页面存在但搜不到:设置 `CONFLUENCE_PAGE_ID=<pageId>` `CONFLUENCE_PAGE_URL=<完整URL>` 直链下载
51
+ - 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V`);可尝试输入更长关键词如 `ADI_V2.3.4结算`
52
+ - 页面存在但搜不到:加 `CONFLUENCE_DEBUG=1` 查看选择器解析日志;或(可选)设置 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链下载
49
53
  - Playwright 失败且设置 `CONFLUENCE_USE_REST=1` 时才回退 Token;登录最多重试 3 次,并用 `expect_navigation` 避免竞态
50
54
  - 登录偶发失败:用 `CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500` 有头观察;失败截图 `confluence-login-fail-*.png`
51
55
  - `playwright` 未安装:`pip install playwright && playwright install chromium`
@@ -41,7 +41,7 @@ npx sdd-flow-kit run --product {{DEFAULT_PRODUCT}} --version <版本号> -y --ag
41
41
  3. propose 后 **必须** `gate ac-ready`(P0 映射到 spec/tasks)才能 `phase impl`。
42
42
  4. **仅当** `gate --expect impl-allowed` 通过后,才允许按 `tasks.md` 改业务代码。
43
43
  5. **Apply 前必读**:`source/PRD.md` 对应章节 + `05-验收清单.md`(tasks 是子集,不能替代 PRD)。
44
- 6. **deliver 前必须** `gate ac-signed`(05 P0 已验收且已签收)。
44
+ 6. **deliver 前必须** `prd-diff` + `gate prd-coverage`(脚本 PRD 语义 diff + 08 复查)+ `gate ac-signed`。
45
45
  7. 提案/实现 **必须** 读并执行:
46
46
  - `.cursor/skills/opsx-superpowers-workflow/SKILL.md`
47
47
  - `openspec-propose` → `openspec-apply-change` → `pnpm run opsx:delivery-pipeline -- <change>`
@@ -77,7 +77,7 @@ npx sdd-flow-kit run --product {{DEFAULT_PRODUCT}} --version <版本号> -y --ag
77
77
  | B | 用户确认后 **05→04→01/02** → `gate docs-closed` → `phase docs-done` |
78
78
  | C | `propose` + artifacts → `gate propose-ready` + **`gate ac-ready`** → `phase propose-done` |
79
79
  | D | `phase impl` → `gate impl-allowed` → `/opsx-apply`(读 PRD+05) |
80
- | E | `validate` → **05 签收** → `gate ac-signed` → `deliver` → `phase shipped` |
80
+ | E | `prd-review` → AI 08 → `prd-diff` → `gate prd-coverage` → `validate`(强制 E2E)→ **05 签收** → `deliver` |
81
81
 
82
82
  ## 环境
83
83
 
@@ -45,12 +45,22 @@
45
45
  4. 读并执行:`openspec-apply-change` → `tdd-script`(分层测试策略)→ `openspec-superpowers-pipeline` → `opsx-dev-delivery-pipeline`
46
46
  5. Red 断言须写死 PRD/05 原文(toast、tooltip、列序等)
47
47
  6. `pnpm run opsx:delivery-pipeline -- <change-name>` 直至 **exit 0**
48
- 7. deliver 前执行 `gate ac-signed`,再 `deliver`
48
+
49
+ ## 阶段 E — PRD 一致性复查 + 脚本语义 diff + 强制验收(交付前)
50
+
51
+ 1. `npx sdd-flow-kit prd-review --project-root <repo> --run-id <runId>`
52
+ 2. AI 执行 `08-PRD一致性复查提示词.md`:**最细粒度**逐条对比 PRD 原子 × 代码,禁止揣摩/兜底
53
+ 3. `npx sdd-flow-kit prd-diff --project-root <repo> --run-id <runId>`(脚本 PRD×代码字面量 diff → `09-PRD语义diff报告.md`)
54
+ 4. 若 diff 失败:`prd-remediate --change <name>` 或 `validate` 内置循环(按 09 调 `/opsx-apply` 修,**每项最多 3 次**、每轮只修剩余失败项,重试附带上次错误代码摘要;仍失败 → 08 待人工确认)
55
+ 5. `npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect prd-coverage`
49
56
 
50
57
  ## 完成判定
51
58
 
52
59
  - [ ] `gate ac-ready` 通过(P0 已映射)
53
60
  - [ ] `openspec/changes/<name>/` 下 applyRequires 全部 done
54
61
  - [ ] `tasks.md` 全部 `- [x]` 且含 `[AC-XX]`
62
+ - [ ] `prd-diff` 通过(09 报告无 missing/partial/report_mismatch)
63
+ - [ ] `gate prd-coverage` 通过(08 报告 + 语义 diff + 全量 E2E 测试覆盖)
64
+ - [ ] `validate` 通过(含强制 e2e)
55
65
  - [ ] `gate ac-signed` 通过
56
66
  - [ ] delivery-pipeline exit 0
@@ -0,0 +1,93 @@
1
+ # 08-PRD一致性复查报告
2
+
3
+ > **交付前强制门禁**。按 `source/PRD.md` **最细粒度**逐条对比已实现代码。
4
+ > **禁止**揣摩、禁止兜底方案;无法确认则标 `待人工确认`,不得自行假设。
5
+
6
+ ## 复查状态
7
+
8
+ - **复查状态**:未复查(完成后改为 `复查完成`)
9
+ - **PRD 原子条目数**:{{ATOM_COUNT}}
10
+ - **生成时间**:{{GENERATED_AT}}
11
+
12
+ ## 填写规则(强制)
13
+
14
+ 1. **粒度**:本表每一行对应 `.prd-review-atoms.json` 中的一条 PRD 原子(标题 / 列表项 / 表格行 / 实质段落),不得合并或省略。
15
+ 2. **代码证据**:必须写可核对位置,例如 `src/pages/foo/index.tsx:42`、E2E `e2e/foo.spec.ts` 中断言原文、DOM 选择器 + 期望文案。
16
+ 3. **结论**(仅允许三选一):
17
+ - `一致` — 代码与 PRD 该条完全一致
18
+ - `不一致-按PRD修复` — 代码与 PRD 冲突,须按 PRD 改代码(不得改 PRD 迁就实现)
19
+ - `待人工确认` — 无法从 PRD 或代码得出确定结论,须人工裁定
20
+ 4. **禁止措辞**(出现在证据/备注中将导致 gate 失败):推测、兜底、暂定、可能、大概、应该可以、或沿用、默认处理、先这样、暂时、估计、猜测、或许。
21
+ 5. **不一致处理**:标 `不一致-按PRD修复` 的项必须在修复代码后改为 `一致`,方可 deliver。
22
+ 6. **待人工确认**:不得自行「合理推断」;须在下方汇总表列出,交付前必须清零或用户明确签收。
23
+
24
+ ## 填写示例(参考格式;示例在代码块内,不会被 gate 解析,填表时勿照抄 ID)
25
+
26
+ 以下演示**合格写法**。填「逐条对比」表时:按 `.prd-review-atoms.json` 的真实 `PRD-001…` 逐条填写,证据粒度须达到示例水平。
27
+
28
+ ### 示例 A — `一致`
29
+
30
+ ```
31
+ | PRD-ID | PRD 原文(摘要) | 代码证据 | 结论 | 备注 |
32
+ | PRD-0xx | 列表页「导出」后 toast 为「导出成功,请稍后查看下载中心」 | src/views/order/List.vue:88 message.success('导出成功,请稍后查看下载中心'); E2E e2e/order-export.spec.ts:34 getByText('导出成功,请稍后查看下载中心') | 一致 | §3.2 列表操作 > 导出 L156 |
33
+ ```
34
+
35
+ ### 示例 B — `不一致-按PRD修复`
36
+
37
+ ```
38
+ | PRD-ID | PRD 原文(摘要) | 代码证据 | 结论 | 备注 |
39
+ | PRD-0xx | 筛选「状态」默认选中「全部」 | List.vue:41 statusDefault:'pending'(PRD 要求「全部」) | 不一致-按PRD修复 | §3.1 筛选 > 状态 L142 |
40
+ ```
41
+
42
+ 修复追踪写法:
43
+
44
+ ```
45
+ | PRD-ID | 修复说明 | 修复后证据 | 状态 |
46
+ | PRD-0xx | statusDefault 改为 'all' | List.vue:41 + e2e/order-filter.spec.ts:12 断言默认「全部」 | 已关闭 |
47
+ ```
48
+
49
+ ### 示例 C — `待人工确认`
50
+
51
+ ```
52
+ | PRD-ID | PRD 原文(摘要) | 代码证据 | 结论 | 备注 |
53
+ | PRD-0xx | 批量审核在选中 0 条时的交互 | BatchAudit.vue:67 仅 selected.length>0 时渲染按钮;PRD 未定义 0 条时行为 | 待人工确认 | §4.5 批量操作 L203 |
54
+ ```
55
+
56
+ 待人工确认汇总:
57
+
58
+ ```
59
+ | PRD-ID | PRD 原文 | 不确定原因 | 需确认人 |
60
+ | PRD-0xx | 批量审核 0 条交互 | PRD 未写隐藏/disabled/toast,代码为隐藏 | 产品负责人 |
61
+ ```
62
+
63
+ ### 不合格写法(gate 会失败)
64
+
65
+ - 「推测 PRD 意思是…」— 含禁止词
66
+ - 「先沿用旧版,后续再改」— 兜底措辞
67
+ - 「组件里应该有处理」— 无路径/行号/断言
68
+ - 「基本满足」— 结论不在三选一内
69
+
70
+ ## 逐条对比(PRD 原子 × 代码)
71
+
72
+ | PRD-ID | PRD 原文(摘要) | 代码证据(路径+行号/选择器/断言) | 结论 | 备注(章节路径) |
73
+ |--------|-----------------|----------------------------------|------|-----------------|
74
+ {{REVIEW_TABLE_ROWS}}
75
+
76
+ ## 待人工确认汇总
77
+
78
+ | PRD-ID | PRD 原文 | 不确定原因 | 需确认人 |
79
+ |--------|---------|-----------|---------|
80
+ | (无则写「无」) | | | |
81
+
82
+ ## 不一致项修复追踪(须全部关闭)
83
+
84
+ | PRD-ID | 修复说明(按 PRD 改了什么) | 修复后证据 | 状态 |
85
+ |--------|---------------------------|-----------|------|
86
+ | (无则写「无」) | | | 已关闭 |
87
+
88
+ ## 复查完成签收
89
+
90
+ - [ ] 全部 PRD 原子条目已逐条填写
91
+ - [ ] 无 `不一致-按PRD修复` 未关闭项
92
+ - [ ] 无未解决的 `待人工确认` 项
93
+ - [ ] 已将「复查状态」改为 `复查完成`
@@ -0,0 +1,49 @@
1
+ # PRD 一致性复查提示词(最细粒度,禁止揣摩)
2
+
3
+ 你正在执行 **PRD × 已实现代码** 的一致性复查。输入:
4
+
5
+ - `source/PRD.md`(唯一需求来源)
6
+ - `.prd-review-atoms.json`(已拆好的最细粒度 PRD 原子,**不得删行**)
7
+ - `08-PRD一致性复查报告.md`(待填写)
8
+ - `05-验收清单.md`(AC 映射参考,**不能替代** PRD 全文)
9
+ - 业务代码 `src/`、`frontend/src/`、相关 `e2e/` 测试
10
+
11
+ ## 强制原则
12
+
13
+ 1. **拆分粒度**:以 `.prd-review-atoms.json` 为准,**一条 PRD 原子 = 报告表格一行**,不得合并、不得跳过。
14
+ 2. **只陈述可核对事实**:代码证据必须可定位(文件路径 + 行号,或 E2E 测试名 + 断言字符串)。
15
+ 3. **禁止揣摩**:不得写「可能」「大概」「推测」「兜底」「暂定」「或沿用」「默认」等措辞。
16
+ 4. **禁止兜底方案**:不得建议「先这样」「暂时接受」「后续再改」来绕过 PRD。
17
+ 5. **不一致时**:结论标 `不一致-按PRD修复`,并在「不一致项修复追踪」登记;**以 PRD 为准改代码**,不得改 PRD 迁就实现。
18
+ 6. **无法确认时**:结论标 `待人工确认`,写入「待人工确认汇总」,写清**缺什么信息**才能判断;**不得自行假设**。
19
+
20
+ ## 填写格式
21
+
22
+ 填表前阅读 `08-PRD一致性复查报告.md` 中的 **「填写示例」** 一节(含 `一致` / `不一致-按PRD修复` / `待人工确认` 三种合格写法,以及不合格反例)。
23
+ 你的每一行须达到示例中的证据粒度(路径+行号 或 E2E 断言原文),不得照抄示例中的 PRD-ID 与业务内容。
24
+
25
+ ## 执行步骤
26
+
27
+ 1. 读取 `.prd-review-atoms.json`,确认共 {{ATOM_COUNT}} 条(与报告行数一致)。
28
+ 2. 对每条 PRD 原子:
29
+ - 在 PRD 中定位原文(章节路径 + 行号)
30
+ - 在代码/测试中查找**直接实现**该条的证据
31
+ - 填写 `08-PRD一致性复查报告.md` 对应行
32
+ 3. 若实现与 PRD 冲突 → `不一致-按PRD修复`
33
+ 4. 若 PRD 要求明确但代码找不到对应实现 → `不一致-按PRD修复`
34
+ 5. 若 PRD 表述模糊且代码有多种解读 → `待人工确认`(说明歧义点,不要猜)
35
+ 6. 全部填完后:
36
+ - 更新「待人工确认汇总」「不一致项修复追踪」
37
+ - 将「复查状态」改为 `复查完成`(仅当无未关闭不一致、无待人工确认时)
38
+
39
+ ## 完成后执行门禁
40
+
41
+ ```bash
42
+ npx sdd-flow-kit prd-diff --project-root <repo> --run-id <runId>
43
+ npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect prd-coverage
44
+ ```
45
+
46
+ `prd-diff` 会机械扫描 PRD 字面量是否在代码/测试中出现,并生成 `09-PRD语义diff报告.md`。
47
+ 若 08 报告标「一致」但脚本 diff 为 `missing_in_code` / `report_mismatch`,gate 将失败。
48
+
49
+ gate 失败则根据 `09-PRD语义diff报告.md` 修正报告或代码,**不得**跳过 gate。
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: SDD 脚本门禁:必须执行 sdd-flow-kit gate/phase;docModeOnly 时禁止改 src;交付前必须 ac-signed + deliver 成功
2
+ description: SDD 脚本门禁:必须执行 sdd-flow-kit gate/phase;docModeOnly 时禁止改 src;交付前必须 prd-coverage + ac-signed + deliver 成功
3
3
  alwaysApply: true
4
4
  ---
5
5
 
@@ -22,7 +22,7 @@ alwaysApply: true
22
22
  - `npx sdd-flow-kit gate --expect docs-closed`(含 `05-验收清单.md` 已定义)
23
23
  - `npx sdd-flow-kit gate --expect ac-ready --change <name>`(P0 已映射到 spec/tasks)
24
24
  - `npx sdd-flow-kit phase advance --to impl` + `gate --expect impl-allowed`(阶段 D 前)
25
- 4. **禁止**在未执行 `gate ac-signed` 与 `npx sdd-flow-kit deliver` 成功前宣称需求开发完成。
25
+ 4. **禁止**在未执行 `gate prd-coverage`、`gate ac-signed` 与 `npx sdd-flow-kit deliver` 成功前宣称需求开发完成。
26
26
 
27
27
  ## docModeOnly
28
28
 
@@ -59,6 +59,10 @@ npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect ac-ready -
59
59
  npx sdd-flow-kit phase advance --project-root <repo> --run-id <runId> --to impl --change <name>
60
60
  # Apply 前必读 source/PRD.md + 05-验收清单.md
61
61
  # … 实现 …
62
+ npx sdd-flow-kit prd-review --project-root <repo> --run-id <runId>
63
+ # AI 执行 08-PRD一致性复查提示词.md(最细粒度,禁止揣摩)
64
+ npx sdd-flow-kit prd-diff --project-root <repo> --run-id <runId>
65
+ npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect prd-coverage
62
66
  npx sdd-flow-kit validate --project-root <repo> --run-id <runId> --change <name>
63
67
  # 05 签收:P0 已验收 + 签收状态已签收
64
68
  npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect ac-signed
@@ -177,8 +177,10 @@ def href_path_matches_keyword(href: str, keyword: str) -> bool:
177
177
 
178
178
 
179
179
  def text_matches_keyword(text: str, keyword: str) -> bool:
180
- """标题/链接文案匹配:忽略 -/_ 差异。"""
181
- return normalize_match_token(keyword) in normalize_match_token(text)
180
+ """标题/链接文案匹配:忽略 -/_ 差异;支持前缀命中(如 ADI_V2.3.4结算…)。"""
181
+ t_norm = normalize_match_token(text or "")
182
+ kw_norm = normalize_match_token(keyword)
183
+ return kw_norm in t_norm or t_norm.startswith(kw_norm)
182
184
 
183
185
 
184
186
  def score_link_match(text: str, href: str, prefix: str, version: str, keyword: str) -> int:
@@ -212,22 +214,34 @@ def score_link_match(text: str, href: str, prefix: str, version: str, keyword: s
212
214
  space = env("CONFLUENCE_SPACE", "").lower()
213
215
  if space and f"/spaces/{space}/" in href.lower():
214
216
  score += 5
217
+ # ADI/产品 PRD 标题或 URL 含版本号时加权,降低误命中其他空间页面
218
+ if re.search(r"adi[-_]?v", text, re.IGNORECASE) or re.search(
219
+ r"adi[-_]?v", unquote(href), re.IGNORECASE
220
+ ):
221
+ score += 2
215
222
  return score
216
223
 
217
224
 
218
225
  def collect_search_candidates(
219
226
  page, base_url: str, prefix: str, version: str, keyword: str
220
227
  ) -> list[tuple[str, str, int]]:
221
- """返回 [(url, title, score), ...]"""
228
+ """返回 [(url, title, score), ...];标题命中或 URL 路径命中均可得分(>=2)。"""
222
229
  seen: set[str] = set()
223
230
  scored: list[tuple[int, str, str]] = []
231
+ debug = env_bool("CONFLUENCE_DEBUG")
224
232
 
225
233
  selectors = [
226
234
  ".search-result .title a",
227
235
  ".search-result-title a",
228
236
  "#search-results .gs-title a",
237
+ "ol.search-results-list a",
238
+ ".search-results a",
239
+ ".search-result a",
240
+ "#search-results a",
229
241
  ".search-results-panel a",
230
242
  "a[data-linked-resource-id]",
243
+ f"a[href*='{keyword}']",
244
+ f"a:has-text('{keyword}')",
231
245
  "a[href*='/pages/']",
232
246
  "a[href*='/display/']",
233
247
  ]
@@ -235,6 +249,8 @@ def collect_search_candidates(
235
249
  for selector in selectors:
236
250
  links = page.locator(selector)
237
251
  count = min(links.count(), 200)
252
+ if debug and count:
253
+ print(f" [debug] selector={selector!r} count={count}")
238
254
  for i in range(count):
239
255
  href = links.nth(i).get_attribute("href") or ""
240
256
  raw_text = (links.nth(i).inner_text() or "").strip()
@@ -252,6 +268,9 @@ def collect_search_candidates(
252
268
  if s >= 2:
253
269
  scored.append((s, full_url, text[:120]))
254
270
 
271
+ if debug and not scored:
272
+ print(" [debug] 未解析到 score>=2 的 wiki 页面链接")
273
+
255
274
  scored.sort(key=lambda x: x[0], reverse=True)
256
275
  return [(u, t, s) for s, u, t in scored]
257
276
 
@@ -625,19 +644,36 @@ def try_rest_download(
625
644
 
626
645
 
627
646
  def run_search_dosearch(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
647
+ """打开 Confluence 全站专用搜索页(dosearchsite),比首页 #all-updates 顶栏搜索更稳定。"""
628
648
  url = f"{base_url.rstrip('/')}/dosearchsite.action?queryString={quote(keyword)}"
629
- print(f" → 全站搜索: {url}")
630
- try:
631
- page.goto(url, wait_until="domcontentloaded", timeout=60000)
632
- except Exception:
633
- page.goto(url, wait_until="load", timeout=60000)
634
- page.wait_for_timeout(2500)
635
- if is_login_page(page):
636
- return []
649
+ print(f" → 专用搜索页: .../dosearchsite.action?queryString={quote(keyword)}")
650
+ for attempt in range(2):
651
+ try:
652
+ page.goto(url, wait_until="domcontentloaded", timeout=60000)
653
+ except Exception as exc:
654
+ # 搜索页常有重定向/长连接,networkidle 易误报 ERR_ABORTED
655
+ if "ERR_ABORTED" not in str(exc) and "Timeout" not in str(exc):
656
+ raise
657
+ if attempt == 0:
658
+ print(" 导航被中断,等待搜索结果页稳定...")
659
+ try:
660
+ page.goto(url, wait_until="load", timeout=60000)
661
+ except Exception:
662
+ pass
663
+ page.wait_for_load_state("domcontentloaded", timeout=30000)
664
+ page.wait_for_timeout(2500)
665
+ if is_login_page(page):
666
+ return []
667
+ current = page.url.lower()
668
+ on_search = "dosearchsite" in current or "search" in current
669
+ if on_search or attempt == 1:
670
+ break
671
+ print(" 未停留在搜索结果页,重试一次...")
637
672
  return collect_search_candidates(page, base_url, prefix, version, keyword)
638
673
 
639
674
 
640
675
  def run_search_quick(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
676
+ """顶栏快速搜索(dosearchsite 无结果时的兜底,非主路径)。"""
641
677
  search_selector = None
642
678
  for selector in [
643
679
  "#quick-search-query",
@@ -653,10 +689,10 @@ def run_search_quick(page, base_url: str, prefix: str, version: str, keyword: st
653
689
  break
654
690
  if not search_selector:
655
691
  return []
656
- print(f" → 顶栏搜索框: {search_selector} 关键词: {keyword}")
692
+ print(f" → 顶栏搜索框(兜底): {search_selector} 关键词: {keyword}")
657
693
  page.fill(search_selector, keyword, timeout=8000)
658
694
  page.keyboard.press("Enter")
659
- page.wait_for_load_state("networkidle", timeout=30000)
695
+ page.wait_for_load_state("domcontentloaded", timeout=30000)
660
696
  page.wait_for_timeout(2500)
661
697
  return collect_search_candidates(page, base_url, prefix, version, keyword)
662
698
 
@@ -60,9 +60,15 @@ python3 confluence-doc.py {{PRODUCT_PREFIX}}_V1.2.3
60
60
  4. `{{PRODUCT_PREFIX}}-<版本>`(如 ADI-2.3.2)
61
61
  5. `ADI_V<版本>结算`(ADI 长标题页,如 V2.3.4 结算对接欢聚)
62
62
 
63
- 每个关键词都会走 **dosearchsite 全站搜索** + 顶栏搜索;匹配时忽略 `-`/`_` 差异;ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
63
+ 每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates` 顶栏搜索稳定);无结果时再兜底顶栏搜索。匹配时 **标题或 URL 路径** 任一命中即可,并忽略 `-`/`_` 差异;ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
64
64
 
65
- ## 直链(跳过搜索)
65
+ 调试搜索解析:
66
+
67
+ ```bash
68
+ CONFLUENCE_DEBUG=1 python3 confluence-doc.py ADI_V2.3.4
69
+ ```
70
+
71
+ ## 直链(可选,跳过搜索)
66
72
 
67
73
  ```bash
68
74
  CONFLUENCE_PAGE_ID=109609740 python3 confluence-doc.py 2.3.4
@@ -88,6 +94,6 @@ CONFLUENCE_PAGE_URL="https://confluence.huan.tv/spaces/yyzcpt/pages/109609740/..
88
94
  - 脚本:`docs/{{SKILL_DIR}}/confluence-doc.py`
89
95
  - 脚本已内置默认账号密码;如需使用其他账号可用环境变量覆盖
90
96
  - 默认 Confluence 地址为 `https://confluence.huan.tv`(可用 `CONFLUENCE_BASE_URL` 覆盖)
91
- - 默认搜索入口为 `https://confluence.huan.tv/#all-updates`(可用 `CONFLUENCE_SEARCH_URL` 覆盖)
97
+ - 搜索主路径为 `dosearchsite.action?queryString=<关键词>`(全站专用搜索页)
92
98
  - 脚本会自动尝试读取仓库根目录的 `.env.pre` / `.env` / `.env.local` / `.env.development` / `.env.test`(若其中存在 `CONFLUENCE_*` 配置可直接复用)
93
99