plan-governance-cli 0.2.3 → 0.3.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/README.md +24 -1
- package/bin/plan-governance-cli.mjs +21 -0
- package/package.json +7 -3
- package/resources/manifest.json +2 -1
- package/resources/skill/SKILL.md +49 -7
- package/resources/skill/assets/plan.template.md +50 -8
- package/scripts/check_plan_governance.py +45 -6
- package/scripts/graph_governance.mjs +988 -0
- package/scripts/init_plan_governance.py +19 -108
package/README.md
CHANGED
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
- 互不依赖的待办列表。
|
|
22
22
|
- 不影响公共契约的普通实现任务。
|
|
23
23
|
|
|
24
|
+
## 需求探索与 grilling
|
|
25
|
+
|
|
26
|
+
对可能进入治理、但目标、边界、验收或关键取舍尚未收敛的任务,`plan-governance` 会优先使用可用的 `grill-me` 进行需求探索:高影响事项存在关键不明确内容时触发,或同时存在至少两项需求不确定性信号时触发。每次只推进一个问题;可从仓库和工具查证的事实先查证,决策由用户确认。
|
|
27
|
+
|
|
28
|
+
结束时应由用户确认结构化总结:目标、范围和非目标、已确认事实、暂定假设、候选方案、未决问题和下一步验证。只有需要治理的任务才把该摘要写入专项计划的 `需求探索` 章节;它不替代 Step 0 或阶段准入,也不会自动生成 ADR。未安装 `grill-me` 时保持在澄清/设计边界,不伪造访谈完成。
|
|
29
|
+
|
|
24
30
|
## 文档结构
|
|
25
31
|
|
|
26
32
|
最小结构:
|
|
@@ -167,10 +173,25 @@ npm install -g plan-governance-cli
|
|
|
167
173
|
plan-governance-cli check . --strict-readiness
|
|
168
174
|
```
|
|
169
175
|
|
|
176
|
+
功能图谱校验和影响分析:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
plan-governance-cli graph validate .
|
|
180
|
+
plan-governance-cli graph impact --from feature.model-lifecycle --depth 2 --format text .
|
|
181
|
+
plan-governance-cli graph impact --from feature.model-lifecycle --depth 2 --format json .
|
|
182
|
+
plan-governance-cli plan impact --input docs/graph/fixtures/plan-impact/model-lifecycle-api.json --format json .
|
|
183
|
+
plan-governance-cli graph code candidates --symbol APIHandler --kind class --format json .
|
|
184
|
+
plan-governance-cli graph code impact --repo modelpad --file Sources/ModelPadCore/API/APIServer.swift --symbol APIHandler --kind class --depth 2 --format json .
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
项目图谱固定放在 `docs/graph/functional.yaml`。CLI 只读,不修改 YAML、计划状态、计划正文或业务代码;图谱节点使用稳定 ID,证据使用 `kind/ref/locator`,代码对象使用带文件回退的 `code_refs`。影响分析默认两跳,按 `contains`、`orchestrates`、`exposes`、`implements`、`consumes`、`depends_on` 的契约方向传播,并输出最短路径、直接/间接分类和证据引用。代码候选命令根据 `file/symbol/kind` 搜索稳定锚点;多候选输出 `resolution: ask_user`,不自动写回。
|
|
188
|
+
|
|
189
|
+
`plan impact` 是计划治理的前置只读查询:输入文件声明 `graph_scope`、`change_kind`,默认只查询功能层;API 契约、数据、安全、外部边界或明确代码定位才升级到架构层或代码候选。输出查询层级、升级原因、行动分级、测试映射和未查询层级;失败不伪装成“无影响”,也不会触发 `gitnexus analyze`。
|
|
190
|
+
|
|
170
191
|
也可以不全局安装,直接使用锁定版本:
|
|
171
192
|
|
|
172
193
|
```bash
|
|
173
|
-
npx --yes --package plan-governance-cli@0.
|
|
194
|
+
npx --yes --package plan-governance-cli@0.3.0 plan-governance-cli check . --strict-readiness
|
|
174
195
|
```
|
|
175
196
|
|
|
176
197
|
npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接调用或复制该脚本。旧项目已有本地脚本时仍可保留作为兼容或回滚路径。
|
|
@@ -301,6 +322,8 @@ python3 -m pytest
|
|
|
301
322
|
|
|
302
323
|
进入 `待实施` 前,当前阶段必须有明确目标/范围、Step 0 基线、样本或替代基线矩阵、可执行验证方式、失败与回滚边界、无未解决阻塞项,以及最新独立准入复核“通过”。复核记录追加保留,`PLAN_MAP.md` 只链接最新有效结论。
|
|
303
324
|
|
|
325
|
+
机器识别的结构化章节标题固定为 `阶段路线图`、`当前阶段`、`阶段准入摘要`、`最新独立准入复核`、`独立复核记录`,不要在这些标题中插入或添加 `阶段 1` 等编号;阶段编号由 `PLAN_MAP.md` 的当前阶段提供。严格检查发现常见编号变体时,会在输出中给出直接修正提示。
|
|
326
|
+
|
|
304
327
|
阶段准入严格检查命令为 `--strict-readiness`。默认检查先以 `WARNING` 提示历史文档中的机械缺陷;严格模式才返回非零退出码。该检查只判断结构化准入条件,不替代业务验收。
|
|
305
328
|
|
|
306
329
|
## Codex 使用方式
|
|
@@ -18,6 +18,7 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
18
18
|
const checker = resolve(packageRoot, "scripts", "check_plan_governance.py");
|
|
19
19
|
const initializer = resolve(packageRoot, "scripts", "init_plan_governance.py");
|
|
20
20
|
const hookRuntime = resolve(packageRoot, "scripts", "plan_governance_hook.py");
|
|
21
|
+
const graphRuntime = resolve(packageRoot, "scripts", "graph_governance.mjs");
|
|
21
22
|
const manifestPath = resolve(packageRoot, "resources", "manifest.json");
|
|
22
23
|
|
|
23
24
|
function fail(message) {
|
|
@@ -51,6 +52,24 @@ function runPython(script, args) {
|
|
|
51
52
|
return result?.status ?? 1;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function runNode(script, args) {
|
|
56
|
+
try {
|
|
57
|
+
accessSync(script, constants.R_OK);
|
|
58
|
+
} catch {
|
|
59
|
+
return fail(`找不到包内脚本:${script}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = spawnSync(process.execPath, [script, ...args], { stdio: "inherit" });
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return fail(`无法启动 Node.js 脚本:${result.error.message}`);
|
|
65
|
+
}
|
|
66
|
+
if (result.signal) {
|
|
67
|
+
console.error(`plan-governance-cli: 脚本被信号 ${result.signal} 终止`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
return result.status ?? 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
function loadManifest() {
|
|
55
74
|
try {
|
|
56
75
|
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
@@ -168,6 +187,8 @@ function main() {
|
|
|
168
187
|
if (command === "setup") return setup(args.slice(1));
|
|
169
188
|
if (command === "init") return runPython(initializer, args.slice(1));
|
|
170
189
|
if (command === "hook") return runPython(hookRuntime, args.slice(1));
|
|
190
|
+
if (command === "graph") return runNode(graphRuntime, args.slice(1));
|
|
191
|
+
if (command === "plan") return runNode(graphRuntime, args);
|
|
171
192
|
if (command === "check") return runPython(checker, args.slice(1));
|
|
172
193
|
return runPython(checker, args);
|
|
173
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plan-governance-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "统一入口的计划治理检查 CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"resources/skill",
|
|
14
14
|
"scripts/check_plan_governance.py",
|
|
15
15
|
"scripts/init_plan_governance.py",
|
|
16
|
-
"scripts/plan_governance_hook.py"
|
|
16
|
+
"scripts/plan_governance_hook.py",
|
|
17
|
+
"scripts/graph_governance.mjs"
|
|
17
18
|
],
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=20"
|
|
@@ -22,6 +23,9 @@
|
|
|
22
23
|
"access": "public"
|
|
23
24
|
},
|
|
24
25
|
"scripts": {
|
|
25
|
-
"test": "node --test tests/npm_cli.test.mjs"
|
|
26
|
+
"test": "node --test tests/npm_cli.test.mjs tests/graph_cli.test.mjs tests/architecture_graph_cli.test.mjs tests/plan_impact_step0.test.mjs tests/plan_impact_cli.test.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"yaml": "^2.9.0"
|
|
26
30
|
}
|
|
27
31
|
}
|
package/resources/manifest.json
CHANGED
package/resources/skill/SKILL.md
CHANGED
|
@@ -24,6 +24,19 @@ description: 轻量计划治理工作流。Use when 用户要求管理路线图
|
|
|
24
24
|
- `<=1`:按普通任务处理。
|
|
25
25
|
- 大型公共契约迁移可以直接启用治理。
|
|
26
26
|
|
|
27
|
+
## 需求探索与 grilling
|
|
28
|
+
|
|
29
|
+
对可能进入治理的任务,如果运行环境声明 `grill-me` 可用,满足下列任一条件时自动调用它,在实施或冻结详细方案前先澄清需求:
|
|
30
|
+
|
|
31
|
+
1. 涉及公共 API、Schema、迁移、兼容性或不可逆操作,且目标、边界、验收或关键取舍中至少一项未明确;
|
|
32
|
+
2. 下列不确定性信号中至少两项同时存在:目标/验收不清、范围/非目标不清、存在多个有实质取舍的方案、关键事实尚待查证、后续返工成本显著。
|
|
33
|
+
|
|
34
|
+
目标、影响范围和验收方式均明确的小范围任务,以及当前阶段已通过准入且已确认假设没有变化的工作,跳过自动 grilling;用户仍可显式调用它。
|
|
35
|
+
|
|
36
|
+
grilling 每次只问一个问题。可从仓库、环境或工具确认的事实必须先查证;产品取舍、外部承诺和不可逆选择由用户确认。结束前必须汇总并得到用户确认:目标、范围和非目标、已确认事实、暂定假设及验证方式、候选方案与取舍、未决问题及阻塞级别,以及验收口径或下一步动作。
|
|
37
|
+
|
|
38
|
+
用户确认后,只有在任务需要治理时,才把结构化结论写入专项计划的 `需求探索` 章节;不保存完整访谈逐字稿,不自动创建 ADR。探索结论不替代当前阶段的 Step 0、样本矩阵、验证方式、失败/回滚边界或独立准入复核。`grill-me` 不可用时,说明依赖不可用并保持在澄清/设计边界,不得伪造已完成 grilling 或静默实施。
|
|
39
|
+
|
|
27
40
|
## 仓库模型
|
|
28
41
|
|
|
29
42
|
使用项目内这些文件作为事实来源:
|
|
@@ -106,7 +119,7 @@ docs/
|
|
|
106
119
|
5. 没有未解决的当前阶段阻塞项,且 `PLAN_MAP.md` 已同步。
|
|
107
120
|
6. 最新独立准入复核明确为“通过”。
|
|
108
121
|
|
|
109
|
-
|
|
122
|
+
机器识别的结构化章节标题必须保持固定名称:`阶段路线图`、`当前阶段`、`阶段准入摘要`、`最新独立准入复核`、`独立复核记录`。不要在这些标题前添加阶段编号(例如不要写成 `阶段 1 准入摘要` 或 `阶段 1 最新独立准入复核`);阶段身份以 `PLAN_MAP.md` 为准,不在计划正文中另定义阶段编号。若严格检查提示标题问题,优先按提示恢复固定标题。阶段准入摘要还必须使用固定字段:`准入状态`、`Step 0`、`样本矩阵`、`验证方式`、`失败/回滚边界`、`当前阻塞项`、`最新独立准入复核`。
|
|
110
123
|
|
|
111
124
|
### 独立复核记录
|
|
112
125
|
|
|
@@ -120,18 +133,19 @@ docs/
|
|
|
120
133
|
2. 按启用判定决定是否需要治理。
|
|
121
134
|
3. 如果需要初始化,运行 npm 包内的 `plan-governance-cli init` 生成最小结构;只有需要特殊改写时才手动复制 `assets/` 模板。已有项目不要重新初始化计划文档,使用 `--update-agent-rules-only` 或 `--upgrade-existing`。升级其他项目时也使用当前 npm 包提供的 `plan-governance-cli init`,不要调用目标项目里的旧脚本。旧项目若 `PLAN_MAP.md` 还是五列表,使用 `--migrate-plan-map-last-updated` 显式迁移。
|
|
122
135
|
4. 初始化治理文档后,立即执行事实源切换:识别已有 README、草案、旧计划、归档设计文档,将它们视为背景材料或历史记录;从此刻起,新规范只写入 `docs/plans/*.md`、`PLAN_MAP.md`、ADR、migration 或正式 spec,除非用户明确要求,不再编辑旧草案。
|
|
123
|
-
5.
|
|
136
|
+
5. 对需求尚未收敛且满足触发条件的任务,先执行 grilling;在用户确认结构化总结前,不实施也不把暂定内容写成规范事实。
|
|
137
|
+
6. 实施前让当前阶段可执行:
|
|
124
138
|
- 目标、范围、非目标明确
|
|
125
139
|
- 必要的不变量和信任边界明确
|
|
126
140
|
- 必要的公共契约和失败策略明确
|
|
127
141
|
- Step 0 证据存在
|
|
128
142
|
- 验证方式可运行
|
|
129
143
|
- 当前阶段没有未解决阻塞项
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
7. 只实施当前阶段。后续阶段保持粗粒度。
|
|
145
|
+
8. 新信息改变假设时,先更新 `PLAN_MAP.md` 和相关计划,再继续实施;计划被推进、恢复、暂停或复核时同步 `最后更新`。
|
|
146
|
+
9. 完成时记录验证证据、测试覆盖率证据并更新状态。
|
|
147
|
+
10. 验收治理文档时先做独立复核和反向引用检查:不得仅依据计划状态、完成证据文字或文档格式判定完成;必须核对当前仓库内容、可复现验证命令、测试或 CI 输出、覆盖率证据,以及用 `rg` 搜索同名计划、P 编号、状态名、关键字段和 `草案为准|以草案为事实源|详见草案|draft is source|source of truth.*draft|以.*draft.*为准`,确认所有路线图、优先级计划和索引已经同步,且旧草案没有重新成为事实源。
|
|
148
|
+
11. 运行 `plan-governance-cli check`;如果项目仍保留旧脚本,只把它作为回滚或对照,不作为新的分发源。需要检查活跃计划停滞时运行 `--stale-days`,默认 10 天。
|
|
135
149
|
|
|
136
150
|
## Step 0 证据
|
|
137
151
|
|
|
@@ -247,6 +261,34 @@ plan-governance-cli check . --stale-days 10
|
|
|
247
261
|
- `--attest <plan-name>`:为已登记计划创建或覆盖 `docs/attestations/<plan-name>.json` 完成快照。
|
|
248
262
|
- `--check-attestations`:检查完成快照中的计划文件和 `PLAN_MAP.md` hash 是否漂移;漂移、缺失或 JSON 损坏只输出 `WARNING`,不改变退出码。
|
|
249
263
|
|
|
264
|
+
## 图谱与计划前置影响分析
|
|
265
|
+
|
|
266
|
+
当项目声明了功能图谱或架构图谱时,使用 npm CLI 的只读命令查询影响范围:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
plan-governance-cli graph validate .
|
|
270
|
+
plan-governance-cli graph impact --from feature.model-lifecycle --depth 2 --format json .
|
|
271
|
+
plan-governance-cli graph code candidates --symbol APIHandler --kind class --format json .
|
|
272
|
+
plan-governance-cli graph code impact \
|
|
273
|
+
--repo modelpad \
|
|
274
|
+
--file Sources/ModelPadCore/API/APIServer.swift \
|
|
275
|
+
--symbol APIHandler \
|
|
276
|
+
--kind class \
|
|
277
|
+
--format json .
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
计划前置分析使用计划输入文件:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
plan-governance-cli plan impact \
|
|
284
|
+
--input docs/graph/fixtures/plan-impact/model-lifecycle-api.json \
|
|
285
|
+
--format json .
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`plan impact` 默认只查询功能层;API 契约、数据迁移、安全、外部边界或明确要求代码定位时,才升级到架构层或代码层。输出包含查询层级、升级原因、影响节点、行动分级、测试映射和未查询层级。`graph validate`、`graph impact`、代码查询和 `plan impact` 全部只读,不修改 YAML、计划、代码或 GitNexus 索引;失败也不能被解释为“无影响”。
|
|
289
|
+
|
|
290
|
+
GitNexus 只在需要代码定位时使用。CLI 不自动执行 `gitnexus analyze`,也不自动写回 UID 或 YAML;候选映射由 LLM 依据文件、符号、类型和测试等证据确认,证据不足或存在多个合理候选时再请求用户确认。
|
|
291
|
+
|
|
250
292
|
`影响模块或文件` 的作用域匹配规则:
|
|
251
293
|
|
|
252
294
|
- `--drift`、`--pre-commit` 和 `plan_governance_hook.py --event pre-write` 使用同一语义。
|
|
@@ -1,32 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 计划:{{title}}
|
|
2
2
|
|
|
3
3
|
## 背景
|
|
4
4
|
|
|
5
|
+
待补充。
|
|
6
|
+
|
|
5
7
|
## 目标
|
|
6
8
|
|
|
9
|
+
{{goal}}
|
|
10
|
+
|
|
7
11
|
## 非目标
|
|
8
12
|
|
|
13
|
+
- 待补充。
|
|
14
|
+
|
|
15
|
+
<!-- 仅在触发 grilling 且用户确认结构化结论后保留本节;否则删除本节和本注释。 -->
|
|
16
|
+
## 需求探索
|
|
17
|
+
|
|
18
|
+
### 已确认事实
|
|
19
|
+
|
|
20
|
+
### 暂定假设与验证方式
|
|
21
|
+
|
|
22
|
+
### 范围与非目标
|
|
23
|
+
|
|
24
|
+
### 候选方案与取舍
|
|
25
|
+
|
|
26
|
+
### 未决问题
|
|
27
|
+
|
|
28
|
+
### 用户确认的探索结论
|
|
29
|
+
|
|
9
30
|
## 不变量
|
|
10
31
|
|
|
32
|
+
- 当前阶段写细,后续阶段写粗。
|
|
33
|
+
- 完成时必须记录可验证证据。
|
|
34
|
+
|
|
11
35
|
## 影响模块或文件
|
|
12
36
|
|
|
37
|
+
- 待补充。
|
|
38
|
+
|
|
13
39
|
## 公共契约变化
|
|
14
40
|
|
|
41
|
+
待确认。若涉及 API、Schema、兼容性或迁移行为,需要在这里明确。
|
|
42
|
+
|
|
43
|
+
<!-- 机器识别的结构化章节标题必须保持固定名称,不要在标题前添加“阶段 1”等编号。 -->
|
|
15
44
|
## 阶段路线图
|
|
16
45
|
|
|
17
46
|
| 阶段 | 目标 | 进入条件 | 验证方向 | 状态 |
|
|
18
47
|
|---|---|---|---|---|
|
|
19
|
-
|
|
|
48
|
+
| {{phase}} | 建立当前阶段基线 | 治理文档已初始化 | Step 0 证据存在 | {{status}} |
|
|
20
49
|
|
|
21
50
|
## 当前阶段
|
|
22
51
|
|
|
23
52
|
### 范围
|
|
24
53
|
|
|
54
|
+
待补充。
|
|
55
|
+
|
|
56
|
+
<!-- 标题必须保持为“阶段准入摘要”,不要改成“阶段 1 准入摘要”等带编号变体;阶段身份以 PLAN_MAP.md 为准。 -->
|
|
25
57
|
### 阶段准入摘要
|
|
26
58
|
|
|
27
59
|
| 字段 | 内容 |
|
|
28
60
|
|---|---|
|
|
29
|
-
| 准入状态 |
|
|
61
|
+
| 准入状态 | {{status}} |
|
|
30
62
|
| Step 0 | 当前阶段 Step 0 证据的链接或锚点 |
|
|
31
63
|
| 样本矩阵 | 样本/fixture 或替代基线的链接或锚点 |
|
|
32
64
|
| 验证方式 | 可执行命令和输出位置的链接或锚点 |
|
|
@@ -36,21 +68,29 @@
|
|
|
36
68
|
|
|
37
69
|
### 实施步骤
|
|
38
70
|
|
|
39
|
-
1.
|
|
71
|
+
1. 补充 Step 0 证据。
|
|
72
|
+
2. 明确当前阶段完成条件。
|
|
73
|
+
3. 实施当前阶段。
|
|
74
|
+
4. 运行验证并记录证据。
|
|
40
75
|
|
|
41
76
|
### Step 0 证据
|
|
42
77
|
|
|
78
|
+
待补充。实施前需要记录失败测试、最小复现、现状快照、兼容样本、fixture 或关键假设验证。
|
|
79
|
+
|
|
43
80
|
### 验证方式
|
|
44
81
|
|
|
82
|
+
待补充。
|
|
45
83
|
### 测试覆盖率
|
|
46
84
|
|
|
47
85
|
待补充。完成后需要记录测试覆盖率报告,例如 pytest-cov 输出、覆盖率百分比、关键模块覆盖情况等。
|
|
48
86
|
|
|
49
87
|
### 完成条件
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
- Step 0 证据已记录。
|
|
90
|
+
- 当前阶段验证通过。
|
|
91
|
+
- `docs/PLAN_MAP.md` 状态和证据已同步。
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
## 最新独立准入复核
|
|
54
94
|
|
|
55
95
|
| 字段 | 内容 |
|
|
56
96
|
|---|---|
|
|
@@ -60,7 +100,7 @@
|
|
|
60
100
|
| 证据 | 命令、样本或 CI 链接 |
|
|
61
101
|
| 复核者 | 姓名或 Agent |
|
|
62
102
|
|
|
63
|
-
|
|
103
|
+
## 独立复核记录
|
|
64
104
|
|
|
65
105
|
| 日期 | 类型 | 阶段 | 结论 | 证据 | 复核者 |
|
|
66
106
|
|---|---|---|---|---|---|
|
|
@@ -70,10 +110,12 @@
|
|
|
70
110
|
|
|
71
111
|
| 问题 | 推荐方案 | 是否阻塞当前阶段 | 状态 |
|
|
72
112
|
|---|---|---|---|
|
|
73
|
-
|
|
|
113
|
+
| 当前阶段 Step 0 证据是什么? | 在实施前补充最小可观察基线。 | 是 | 待确认 |
|
|
74
114
|
|
|
75
115
|
## 风险和回滚
|
|
76
116
|
|
|
117
|
+
待补充。
|
|
118
|
+
|
|
77
119
|
## 关联 ADR、迁移、spec 或 issue
|
|
78
120
|
|
|
79
121
|
-
|
|
@@ -116,6 +116,32 @@ def key_value_table(section):
|
|
|
116
116
|
return values
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
def numbered_structural_heading(plan_text, heading_names):
|
|
120
|
+
"""查找误把阶段编号写入结构化章节标题的常见形式。"""
|
|
121
|
+
phase_number = r"[0-9一二三四五六七八九十百]+"
|
|
122
|
+
variants = []
|
|
123
|
+
for name in heading_names:
|
|
124
|
+
variants.append(rf"阶段\s*{phase_number}\s+{re.escape(name)}")
|
|
125
|
+
if name.startswith("阶段"):
|
|
126
|
+
variants.append(rf"阶段\s*{phase_number}\s*{re.escape(name[2:])}")
|
|
127
|
+
pattern = re.compile(
|
|
128
|
+
rf"^#+\s+((?:{'|'.join(variants)}))\s*$",
|
|
129
|
+
re.MULTILINE,
|
|
130
|
+
)
|
|
131
|
+
match = pattern.search(mask_fenced_code(plan_text))
|
|
132
|
+
return match.group(1) if match else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def structural_heading_hint(plan_text, heading_name):
|
|
136
|
+
numbered_heading = numbered_structural_heading(plan_text, [heading_name])
|
|
137
|
+
if not numbered_heading:
|
|
138
|
+
return ""
|
|
139
|
+
return (
|
|
140
|
+
f";检测到标题 `{numbered_heading}`,请改为固定标题 "
|
|
141
|
+
f"`{heading_name}`,阶段编号以 `PLAN_MAP.md` 的当前阶段为准"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
119
145
|
def phase_roadmap_rows(plan_text):
|
|
120
146
|
section = markdown_section(plan_text, ["阶段路线图"])
|
|
121
147
|
return [row for row in markdown_table_rows(section) if row and row[0] != "阶段"]
|
|
@@ -160,11 +186,12 @@ def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, war
|
|
|
160
186
|
roadmap = phase_roadmap_rows(plan_text)
|
|
161
187
|
matching_rows = [row for row in roadmap if row and row[0] == current_phase]
|
|
162
188
|
if not matching_rows:
|
|
189
|
+
roadmap_hint = structural_heading_hint(plan_text, "阶段路线图")
|
|
163
190
|
readiness_issue(
|
|
164
191
|
warnings,
|
|
165
192
|
errors,
|
|
166
193
|
strict,
|
|
167
|
-
f"{plan_name}: PLAN_MAP 当前阶段 {current_phase} 未在计划阶段路线图中找到",
|
|
194
|
+
f"{plan_name}: PLAN_MAP 当前阶段 {current_phase} 未在计划阶段路线图中找到{roadmap_hint}",
|
|
168
195
|
)
|
|
169
196
|
else:
|
|
170
197
|
roadmap_status = matching_rows[-1][4] if len(matching_rows[-1]) > 4 else ""
|
|
@@ -177,16 +204,21 @@ def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, war
|
|
|
177
204
|
)
|
|
178
205
|
|
|
179
206
|
if markdown_section(plan_text, ["当前阶段"]) is None:
|
|
180
|
-
|
|
207
|
+
section_hint = structural_heading_hint(plan_text, "当前阶段")
|
|
208
|
+
readiness_issue(warnings, errors, strict, f"{plan_name}: 缺少 `## 当前阶段` 章节{section_hint}")
|
|
181
209
|
|
|
182
|
-
|
|
210
|
+
summary_section = markdown_section(plan_text, ["阶段准入摘要"])
|
|
211
|
+
summary = key_value_table(summary_section)
|
|
183
212
|
missing_fields = sorted(READINESS_FIELDS - set(summary))
|
|
184
213
|
if missing_fields:
|
|
214
|
+
title_hint = ""
|
|
215
|
+
if summary_section is None:
|
|
216
|
+
title_hint = structural_heading_hint(plan_text, "阶段准入摘要")
|
|
185
217
|
readiness_issue(
|
|
186
218
|
warnings,
|
|
187
219
|
errors,
|
|
188
220
|
strict,
|
|
189
|
-
f"{plan_name}: 阶段准入摘要缺少字段:{', '.join(missing_fields)}",
|
|
221
|
+
f"{plan_name}: 阶段准入摘要缺少字段:{', '.join(missing_fields)}{title_hint}",
|
|
190
222
|
)
|
|
191
223
|
for field in sorted(READINESS_FIELDS - {"当前阻塞项"}):
|
|
192
224
|
if field in summary and is_placeholder(summary[field]):
|
|
@@ -215,11 +247,12 @@ def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, war
|
|
|
215
247
|
review_fields = {"日期", "阶段", "结论", "证据", "复核者"}
|
|
216
248
|
missing_review = sorted(review_fields - set(review))
|
|
217
249
|
if missing_review:
|
|
250
|
+
review_hint = structural_heading_hint(plan_text, "最新独立准入复核")
|
|
218
251
|
readiness_issue(
|
|
219
252
|
warnings,
|
|
220
253
|
errors,
|
|
221
254
|
strict,
|
|
222
|
-
f"{plan_name}: 最新独立准入复核缺少字段:{', '.join(missing_review)}",
|
|
255
|
+
f"{plan_name}: 最新独立准入复核缺少字段:{', '.join(missing_review)}{review_hint}",
|
|
223
256
|
)
|
|
224
257
|
if review.get("日期"):
|
|
225
258
|
try:
|
|
@@ -243,7 +276,13 @@ def check_phase_readiness(plan_map_text, plan_name, data, plan_text, strict, war
|
|
|
243
276
|
|
|
244
277
|
history = [row for row in review_history_rows(plan_text) if len(row) >= 4 and row[2] == current_phase]
|
|
245
278
|
if not history:
|
|
246
|
-
|
|
279
|
+
history_hint = structural_heading_hint(plan_text, "独立复核记录")
|
|
280
|
+
readiness_issue(
|
|
281
|
+
warnings,
|
|
282
|
+
errors,
|
|
283
|
+
strict,
|
|
284
|
+
f"{plan_name}: 独立复核记录缺少当前阶段 {current_phase} 的记录{history_hint}",
|
|
285
|
+
)
|
|
247
286
|
else:
|
|
248
287
|
latest_history = history[-1]
|
|
249
288
|
if review.get("日期") and latest_history[0] != review["日期"]:
|