plan-governance-cli 1.1.0 → 1.1.2
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 -18
- package/bin/plan-governance-cli.mjs +30 -24
- package/package.json +1 -1
- package/resources/manifest.json +1 -2
- package/resources/skill/SKILL.md +1 -1
- package/resources/skill/references/cli.md +3 -3
- package/resources/skill/references/planning.md +1 -1
- package/resources/skill/references/verification.md +16 -16
- package/scripts/check_plan_governance.py +232 -68
- package/scripts/init_plan_governance.py +6 -34
package/README.md
CHANGED
|
@@ -111,7 +111,6 @@ plan-governance-cli init \
|
|
|
111
111
|
.git/
|
|
112
112
|
docs/PLAN_MAP.md
|
|
113
113
|
docs/plans/api-compat-migration.md
|
|
114
|
-
CLAUDE.md
|
|
115
114
|
AGENTS.md
|
|
116
115
|
```
|
|
117
116
|
|
|
@@ -119,12 +118,11 @@ AGENTS.md
|
|
|
119
118
|
|
|
120
119
|
如果目标目录还不是 Git 仓库,初始化流程会先执行 `git init`;已有 `.git/` 时会跳过,不重复初始化。
|
|
121
120
|
|
|
122
|
-
`--update-agent-rules` 会创建或更新 `
|
|
121
|
+
`--update-agent-rules` 会创建或更新 `AGENTS.md` 中带标记的计划治理章节,只写稳定执行规则,不写具体计划内容。具体计划仍以 `docs/PLAN_MAP.md` 和 `docs/plans/*.md` 为准。
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
也可以使用含义更直接的别名:
|
|
125
124
|
|
|
126
125
|
```bash
|
|
127
|
-
--update-claude-md
|
|
128
126
|
--update-agents-md
|
|
129
127
|
```
|
|
130
128
|
|
|
@@ -144,7 +142,7 @@ plan-governance-cli init \
|
|
|
144
142
|
--update-agent-rules-only
|
|
145
143
|
```
|
|
146
144
|
|
|
147
|
-
|
|
145
|
+
`--update-agents-md-only` 与上述兼容别名作用相同,也只更新 `AGENTS.md`。
|
|
148
146
|
|
|
149
147
|
如果要升级已有项目的辅助文件,刷新检查脚本并更新代理规则,但不覆盖 `docs/`,运行:
|
|
150
148
|
|
|
@@ -157,7 +155,7 @@ plan-governance-cli init \
|
|
|
157
155
|
`--upgrade-existing` 会:
|
|
158
156
|
|
|
159
157
|
- 按兼容模式覆盖更新项目中的 `scripts/check_plan_governance.py`
|
|
160
|
-
- 创建或更新 `
|
|
158
|
+
- 创建或更新 `AGENTS.md` 中带标记的计划治理章节,既有其他代理入口保持原样
|
|
161
159
|
- 保留已有 `docs/PLAN_MAP.md` 和 `docs/plans/*.md`
|
|
162
160
|
- 提示缺失的治理文档
|
|
163
161
|
|
|
@@ -205,17 +203,24 @@ plan-governance-cli graph code impact --repo modelpad --file Sources/ModelPadCor
|
|
|
205
203
|
|
|
206
204
|
## 持续推进
|
|
207
205
|
|
|
208
|
-
在已授权的跨轮任务中可使用 `goal`;skill
|
|
206
|
+
在已授权的跨轮任务中可使用 `goal`;skill 本身不定义整计划自主执行运行时。普通改动自验,高风险或高影响同范围只独立复核一次,修复后自验;旧策略名和旧记录格式采用相同行为并保留历史,无需逐计划迁移。
|
|
209
207
|
|
|
210
|
-
[验证规范](resources/skill/references/verification.md)
|
|
208
|
+
[验证规范](resources/skill/references/verification.md)统一维护单次复核、任务复用、证据复用、修复自验与用户验收;其他会话从同一 skill 读取。需要独立复核且授权明确时自动安排只读 subagent,复用已有同范围任务/结果,等待异常先核准任务 ID 和状态,且复核者不得再委派复核;不为普通小改逐次派发。技术完成待用户验收的适用功能计划仍保持实施中。
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
恢复已有计划时可读取派生工作集:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
plan-governance-cli workset . --json
|
|
214
|
+
plan-governance-cli workset . --json --strict-readiness
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
工作集返回活跃计划、阶段、阻塞、下一动作、并行提示和最近证据,不写计划或自动执行动作;严格模式让活跃计划的机械准入缺陷返回非零。需要缩小恢复上下文时使用最近记录窗口:
|
|
213
218
|
|
|
214
219
|
```bash
|
|
215
220
|
plan-governance-cli workset . --json --evidence-limit 3
|
|
216
221
|
```
|
|
217
222
|
|
|
218
|
-
每个计划保留末尾三条记录,并给出省略数量和原文位置;默认完整输出和门禁判断保持。窗口用于恢复阅读,必要时去掉参数回查完整记录,详见 [CLI 参考](resources/skill/references/cli.md#当前工作集)
|
|
223
|
+
每个计划保留末尾三条记录,并给出省略数量和原文位置;默认完整输出和门禁判断保持。窗口用于恢复阅读,必要时去掉参数回查完整记录,详见 [CLI 参考](resources/skill/references/cli.md#当前工作集)。
|
|
219
224
|
|
|
220
225
|
## 按需读取规则
|
|
221
226
|
|
|
@@ -230,10 +235,10 @@ plan-governance-cli guide cli # CLI 操作
|
|
|
230
235
|
|
|
231
236
|
SKILL 保留任务边界、自主执行原则和按需入口,三份 references 按需读取并随 manifest/setup/npm 分发。已有项目只刷新受管入口,不迁移旧计划、不改用户块外内容。选择同一版本的规范来源,安装副本与仓库同版本号也可能有不同内容。
|
|
232
237
|
|
|
233
|
-
|
|
238
|
+
也可以不全局安装,直接使用当前公开版本:
|
|
234
239
|
|
|
235
240
|
```bash
|
|
236
|
-
npx --yes --package plan-governance-cli@
|
|
241
|
+
npx --yes --package plan-governance-cli@latest plan-governance-cli check . --strict-readiness
|
|
237
242
|
```
|
|
238
243
|
|
|
239
244
|
npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接调用或复制该脚本。旧项目已有本地脚本时仍可保留作为兼容或回滚路径。
|
|
@@ -252,11 +257,10 @@ npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接
|
|
|
252
257
|
|
|
253
258
|
```bash
|
|
254
259
|
plan-governance-cli setup --target codex --dry-run
|
|
255
|
-
plan-governance-cli setup --target
|
|
256
|
-
plan-governance-cli setup --target all
|
|
260
|
+
plan-governance-cli setup --target codex --force
|
|
257
261
|
```
|
|
258
262
|
|
|
259
|
-
`setup`
|
|
263
|
+
`setup` 只向 Codex 同步 npm 包 manifest 指定的 skill、代理元数据和模板文件,必须恰好指定一次 `--target codex`。旧版或重复 target、混合 help、缺失 target 都会在写入前失败;目标之外的用户目录不会被读取、修改或删除。默认先用 `--dry-run` 查看差异,目标文件有本地修改时不会静默覆盖。检查器、初始化器和 hook runtime 由 npm 包内部调用,不复制到项目或 skill 目录;项目初始化器只维护 `AGENTS.md`,当前实现不自动安装 hook 配置。
|
|
260
264
|
|
|
261
265
|
初始化项目时使用包内初始化器:
|
|
262
266
|
|
|
@@ -268,7 +272,7 @@ plan-governance-cli init --root . --plan api-compat-migration --title "API 兼
|
|
|
268
272
|
|
|
269
273
|
## Hook runtime
|
|
270
274
|
|
|
271
|
-
本仓库提供只读 hook runtime,供 Codex
|
|
275
|
+
本仓库提供只读 hook runtime,供 Codex 项目级 hooks 手动接入。脚本只输出短提示和检查结果,不修改治理文档,不更新 `最后更新`,不安装或修改全局配置。
|
|
272
276
|
|
|
273
277
|
```bash
|
|
274
278
|
plan-governance-cli hook --event session-start
|
|
@@ -366,7 +370,7 @@ python3 -m pytest
|
|
|
366
370
|
| `设计中` | 正在明确范围、契约和门禁 |
|
|
367
371
|
| `待实施` | 当前阶段门禁已通过,但尚未开始 |
|
|
368
372
|
| `实施中` | 当前阶段正在修改代码或文档 |
|
|
369
|
-
| `已完成` |
|
|
373
|
+
| `已完成` | 实现、测试、证据、文档及适用用户验收均已完成 |
|
|
370
374
|
| `已替代` | 被另一个计划取代 |
|
|
371
375
|
| `已合并` | 并入另一个计划 |
|
|
372
376
|
| `已废弃` | 明确不再推进 |
|
|
@@ -375,7 +379,9 @@ python3 -m pytest
|
|
|
375
379
|
|
|
376
380
|
状态与当前阶段由 `docs/PLAN_MAP.md` 维护。每阶段必须具备自身 Step 0、验证/完成和失败边界、无有效阻塞及适用复核,不能凭上一阶段完成自动待实施。
|
|
377
381
|
|
|
378
|
-
|
|
382
|
+
新旧计划统一采用单次复核行为:普通改动自验,高风险或高影响独立检查一次后由实施者修复自验,原发现保留。旧 `风险分流` 标签和缺省六列格式只作为兼容记录形式,不恢复多次独立门禁;固定字段、历史保护和严格检查语义以[阶段门规范](resources/skill/references/verification.md#step-0-与阶段门)为准。
|
|
383
|
+
|
|
384
|
+
派发独立复核时显式传入推理强度,不依赖全局 subagent 默认值:安全、权限、隐私、资金、数据完整性等高风险复核使用 `high`,其他独立复核使用 `medium`。该分层只影响本次复核的思考强度,不改变是否复核或单次复核规则。
|
|
379
385
|
|
|
380
386
|
使用 `check --strict-readiness` 做准入/CI/发布机械检查;默认检查兼容告警,机械通过不代替真实风险判断、业务验收或用户接受。
|
|
381
387
|
|
|
@@ -79,11 +79,23 @@ function loadManifest() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function parseSetupArgs(args) {
|
|
82
|
+
if (args.length === 1 && ["-h", "--help"].includes(args[0])) {
|
|
83
|
+
console.log("用法:plan-governance-cli setup --target codex [--destination DIR] [--dry-run] [--force]");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
const options = { target: null, destination: null, dryRun: false, force: false };
|
|
83
88
|
for (let index = 0; index < args.length; index += 1) {
|
|
84
89
|
const arg = args[index];
|
|
85
90
|
if (arg === "--target") {
|
|
86
|
-
options.target
|
|
91
|
+
if (options.target !== null) {
|
|
92
|
+
throw new Error("setup 的 --target 只能指定一次");
|
|
93
|
+
}
|
|
94
|
+
const value = args[++index];
|
|
95
|
+
if (!value || value.startsWith("-")) {
|
|
96
|
+
throw new Error("setup 的 --target 缺少值");
|
|
97
|
+
}
|
|
98
|
+
options.target = value;
|
|
87
99
|
} else if (arg === "--destination") {
|
|
88
100
|
options.destination = args[++index];
|
|
89
101
|
} else if (arg === "--dry-run") {
|
|
@@ -91,18 +103,14 @@ function parseSetupArgs(args) {
|
|
|
91
103
|
} else if (arg === "--force") {
|
|
92
104
|
options.force = true;
|
|
93
105
|
} else if (arg === "-h" || arg === "--help") {
|
|
94
|
-
|
|
95
|
-
return null;
|
|
106
|
+
throw new Error("setup 的 --help 必须单独使用");
|
|
96
107
|
} else {
|
|
97
108
|
throw new Error(`setup 不支持参数:${arg}`);
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
if (
|
|
102
|
-
throw new Error("setup
|
|
103
|
-
}
|
|
104
|
-
if (options.destination && options.target === "all") {
|
|
105
|
-
throw new Error("--destination 只能与单个 setup target 一起使用");
|
|
112
|
+
if (options.target !== "codex") {
|
|
113
|
+
throw new Error("setup 仅支持 --target codex");
|
|
106
114
|
}
|
|
107
115
|
return options;
|
|
108
116
|
}
|
|
@@ -112,12 +120,11 @@ function expandTarget(target) {
|
|
|
112
120
|
return target.replace(/^~(?=\/|$)/, home);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}));
|
|
123
|
+
function targetRoot(manifest, options) {
|
|
124
|
+
return {
|
|
125
|
+
name: "codex",
|
|
126
|
+
root: resolve(options.destination ? options.destination : expandTarget(manifest.skill.targets.codex)),
|
|
127
|
+
};
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
function resourceFiles(manifest) {
|
|
@@ -148,17 +155,16 @@ function setup(args) {
|
|
|
148
155
|
|
|
149
156
|
const plans = [];
|
|
150
157
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
plans.push({ target: target.name, destination, existing, sourceContent });
|
|
158
|
+
const target = targetRoot(manifest, options);
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
accessSync(file.source, constants.R_OK);
|
|
161
|
+
const destination = resolve(target.root, file.relative);
|
|
162
|
+
const existing = existsSync(destination) ? readFileSync(destination, "utf8") : null;
|
|
163
|
+
const sourceContent = readFileSync(file.source, "utf8");
|
|
164
|
+
if (existing !== null && existing !== sourceContent && !options.force) {
|
|
165
|
+
throw new Error(`目标文件存在本地差异,未覆盖:${destination}(如确认覆盖请加 --force)`);
|
|
161
166
|
}
|
|
167
|
+
plans.push({ target: target.name, destination, existing, sourceContent });
|
|
162
168
|
}
|
|
163
169
|
} catch (error) {
|
|
164
170
|
return fail(error.message);
|
package/package.json
CHANGED
package/resources/manifest.json
CHANGED
package/resources/skill/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ description: 维护已有项目计划,或为需要跨阶段、跨会话协调
|
|
|
13
13
|
|
|
14
14
|
首次接手读取地图、当前相关计划及必要契约;恢复只核对当前阶段、实际差异和最近证据。只实施已授权且准入的范围;实质边界变化时调整受影响计划,日常进展随工作同步。事实源与格式见 planning。
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
按 verification 验证:普通改动自验,高风险或高影响的同一交付范围独立检查一次,修复由实施者自验;派发独立复核时显式传入推理强度,高风险用 `high`,其余用 `medium`。旧策略名和旧记录格式不改变此行为。适用检查通过后完成交付;未解决问题如实保留。交付说明完成结果、必要证据和剩余事项,明确约定的用户体验验收继续有效。
|
|
17
17
|
|
|
18
18
|
## 按需读取
|
|
19
19
|
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
## 分发与资源
|
|
20
20
|
|
|
21
|
-
SKILL、三个 references、agents 元数据与 assets 由同一 npm 包的 manifest 分发。`plan-governance-cli setup --target codex
|
|
21
|
+
SKILL、三个 references、agents 元数据与 assets 由同一 npm 包的 manifest 分发。`plan-governance-cli setup --target codex --dry-run` 查看 Codex 同步差异;实际覆盖需既有用户授权并按 setup 冲突提示处理,不自动清理目标之外的用户目录。setup 只支持 Codex 单目标同步;项目初始化器只维护 `AGENTS.md`。项目升级不迁移旧计划,受管块外内容必须保持。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
仅更新计划格式不等于更新项目入口。已有项目在任务交接、没有进行中的实施或复核时更新受管入口;升级不批量回写历史,旧策略名和六列格式按 verification 解释为统一单次行为。安装副本与仓库开发规则有差异时说明本次选用来源,不能因版本号相同就混读正文。
|
|
24
24
|
|
|
25
25
|
## 初始化、模板和检查脚本
|
|
26
26
|
|
|
@@ -54,7 +54,7 @@ plan-governance-cli init \
|
|
|
54
54
|
--update-agent-rules-only
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
升级已有项目的辅助文件,刷新 `scripts/check_plan_governance.py
|
|
57
|
+
升级已有项目的辅助文件,刷新 `scripts/check_plan_governance.py` 和 `AGENTS.md`,但不覆盖 `docs/` 或其他代理入口:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
plan-governance-cli init \
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
## 执行与恢复
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
沿用已确认决定,只展开本次差异、证据缺口及直接依赖。跨阶段工作按 [阶段门](verification.md#step-0-与阶段门)判断,每阶段有自身基线与完成条件;上一阶段通过不自动放行下一阶段,但同一交付范围的独立检查可按 verification 显式复用而不重审。未来事项需注明承接范围与依赖,不以未准入的后续工作阻断无关的当前交付。
|
|
38
38
|
|
|
39
39
|
文档变更后做适用链接、结构和引用检查,运行所选版本的 `plan-governance-cli check .`;准入/发布使用 `--strict-readiness`。纯落档不重复代码测试。用户验收与证据真实性按 verification,不以文档格式通过替代。
|
|
40
40
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
# 验证、复核与用户验收
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本页维护统一复核行为、阶段门和交付证据;CLI 入口为 `guide verification`。写入记录时再读格式部分。
|
|
4
4
|
|
|
5
5
|
## 记录与兼容
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
新旧计划采用同一复核行为;字段只决定记录格式,不决定是否重审或如何恢复:
|
|
8
8
|
|
|
9
|
-
|
|
|
9
|
+
| 记录形式 | 解释 |
|
|
10
10
|
|---|---|
|
|
11
|
-
| 单次独立复核 |
|
|
12
|
-
| 风险分流 |
|
|
13
|
-
| 无字段 |
|
|
11
|
+
| 单次独立复核 | 当前模板标签 |
|
|
12
|
+
| 风险分流 | 历史标签,按同一单次行为解释,不触发迁移或重审 |
|
|
13
|
+
| 无字段 | 保留六列历史格式,后续自验类型见旧计划兼容 |
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
显式字段的空值、未知值和重复声明不能放行。升级不批量改写计划;原检查按真实日期、结论、证据和身份保留,旧失败须由真实修复自验闭环,格式统一本身不解决问题。CLI 能力不符时说明缺口,更新按授权处理。
|
|
16
16
|
|
|
17
17
|
## 按风险复核
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
以下方式适用于所有计划;普通无计划任务也按实际风险选择检查,不建立阶段表。
|
|
20
20
|
|
|
21
21
|
普通改动由实施者自验。改变权限、安全、隐私、资金、数据完整性、核心契约或产生广泛/难以撤销后果时,按实际高风险或高影响安排独立检查;不按目录、行数或阶段事件触发。风险不清先查事实。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
同一交付范围只检查一次,通常在实现和自验收敛后、交付或高影响外部动作前;需要设计准入时将这一次前置,后续实施与完成自验。已审范围内的增量和修复合并处理,确有新增高风险范围才另行判断,不把每个修复拆成新范围;后续独立通过不能清除这一次检查留下的发现。
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
授权明确时自动安排一名未参与实现或测试编写的只读复核者,提供范围、差异、风险和证据入口,并要求其自行完成、不得再委派复核。创建复核 subagent 时必须显式传入推理强度,不依赖全局默认:涉及安全、权限、隐私、资金、数据完整性或同类高风险后果时用 `high`,其他独立复核用 `medium`;推理强度不改变是否复核、复核次数或完成判定。派发前复用已有同范围任务或结果;等待失败先核对准确任务 ID 与实际状态,只有确认原任务不存在或不可恢复才补发。冻结受审范围并告知变化。独立者给出发现后,实施者修复并验证原问题及受影响回归,不再送审,未解决则保持未完成。检查不可用/超时直接记录状态,不能伪填已完成结论;业务缺陷描述中的“超时”不代表检查未完成。复核不代替外部授权。
|
|
26
26
|
|
|
27
27
|
## Step 0 与阶段门
|
|
28
28
|
|
|
@@ -34,9 +34,9 @@ Step 0 可采用缺陷复现、迁移快照/兼容样本、新功能验收 fixtu
|
|
|
34
34
|
|
|
35
35
|
验证用户可观察结果和受影响回归,完成项目要求的检查。纯文档做适用文档检查并注明覆盖率不适用。检查通过后,只有新变化、失败或未解疑点才扩大或重复验证。
|
|
36
36
|
|
|
37
|
-
复用证据须覆盖当前工作树、依赖/配置、环境和验收契约,内容可访问;仅 HEAD
|
|
37
|
+
复用证据须覆盖当前工作树、依赖/配置、环境和验收契约,内容可访问;仅 HEAD/版本相同不足以证明有效。同一交付范围的前置独立检查可跨阶段复用,但须在当前阶段以 `复核基线复用` 明确关联原记录、来源阶段和范围;未来阶段或未解决基线不可复用,新增范围重新判断风险。缺口补受影响验证,未解决冲突如实保留。只有状态、证据或链接同步时,不重新跑代码全套;无关任务失败如实报告,不擅自接管。
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
自验失败自主修复/复验。保留独立发现,按“发现 → 修复 → 验证”闭环并准确标记自验。机械检查只证明结构,不能证明风险、范围、独立性或证据真实性。可选 attestation 的显式检查见 [CLI](cli.md#初始化模板和检查脚本)。
|
|
40
40
|
|
|
41
41
|
## 用户可观察验收与交付
|
|
42
42
|
|
|
@@ -48,24 +48,24 @@ Step 0 可采用缺陷复现、迁移快照/兼容样本、新功能验收 fixtu
|
|
|
48
48
|
|
|
49
49
|
## 格式附录:写入阶段记录时读取
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### 显式策略格式
|
|
52
52
|
|
|
53
53
|
显式策略在摘要以 `最新阶段复核` 引用替代旧 `最新独立准入复核`,其余阶段字段保留。最新表采用两列字段/内容:
|
|
54
54
|
|
|
55
55
|
| 字段 | 要求 |
|
|
56
56
|
|---|---|
|
|
57
57
|
| 日期 / 阶段 | YYYY-MM-DD / 与地图当前阶段一致 |
|
|
58
|
-
| 方式 / 风险 |
|
|
58
|
+
| 方式 / 风险 | 自验或独立;低风险、高影响、高风险或待判断 |
|
|
59
59
|
| 风险依据 | 当前实际范围、后果与验证依据 |
|
|
60
60
|
| 结论 | 尚未进行或明确结果;待判断风险不能通过,候选、疑问或占位不算通过 |
|
|
61
61
|
| 证据 / 复核者 | 可复验记录或链接 / 真实执行者身份 |
|
|
62
62
|
|
|
63
63
|
`阶段复核记录` 按 `日期、类型、阶段、方式、风险、结论、证据、复核者` 八列追加,日期顺序和最新表与当前阶段末条一致。完整结果须有有效日期、证据、身份及已判断风险;普通无计划任务无需建表。
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
高风险/高影响自验须有同范围完整独立基线。本阶段已有基线直接引用;复用前阶段基线时追加类型 `复核基线复用`、方式 `自验` 的当前阶段记录,证据须包含来源阶段并关联原记录和相同范围。独立发现修复后用类型 `修复自验`、方式 `自验` 追加,证据说明原发现与修复回归,不能以普通自验或第二次独立通过覆盖未修复发现。自验不伪称独立通过;上一阶段历史不自动阻断当前阶段。
|
|
66
66
|
|
|
67
67
|
结论以明确前缀记录:`通过`、`未通过`等;复核入口不可用/超时直接写其状态,实际查出的证据冲突可作为发现。通过说明不能覆盖结构化未解决事项。check 默认对新准入缺陷 WARNING,待实施/实施中在 `--strict-readiness` 下 ERROR。workset 保持 schema 1:待自验 `verify`、待独立 `independent_review`、通过待地图同步 `sync`、失败 `blocked/resolve_blocker`、歧义 `unknown/unknown`;不自动写状态,hook 只提示。
|
|
68
68
|
|
|
69
69
|
### 旧计划兼容
|
|
70
70
|
|
|
71
|
-
没有策略字段时,保留固定标题
|
|
71
|
+
没有策略字段时,保留固定标题 `阶段路线图`、`当前阶段`、`阶段准入摘要`、`最新独立准入复核`、`独立复核记录`,不为统一行为重写历史。摘要字段为准入状态、Step 0、样本矩阵、验证方式、失败/回滚边界、当前阻塞项及最新独立准入复核。最新表含日期、阶段、结论、证据、复核者;历史使用日期、类型、阶段、结论、证据、复核者六列。普通改动写类型 `普通自验`;独立发现后的闭环写 `修复自验`,且不能清除未完成或缺证据的检查;跨阶段复用写 `复核基线复用` 并关联原记录。其他历史类型按独立检查解释,最新表与当前阶段末条适用记录一致。标题不加阶段编号,阶段身份以地图为准。
|
|
@@ -217,8 +217,144 @@ def review_outcome(conclusion):
|
|
|
217
217
|
return "unknown"
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
def valid_review_date(value):
|
|
221
|
+
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value or ""):
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
return parse_plan_date(value)
|
|
225
|
+
except ValueError:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def legacy_review_method(kind):
|
|
230
|
+
"""六列旧记录用类型显式表达自验;其他历史类型仍按独立事实解释。"""
|
|
231
|
+
return "自验" if kind == "复核基线复用" or "自验" in kind else "独立"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def phase_precedes(plan_text, candidate, current):
|
|
235
|
+
"""只允许路线图中更早的阶段;标准数字阶段可在精简路线图中回退比较。"""
|
|
236
|
+
roadmap = [row[0] for row in phase_roadmap_rows(plan_text)]
|
|
237
|
+
if candidate in roadmap and current in roadmap:
|
|
238
|
+
return roadmap.index(candidate) < roadmap.index(current)
|
|
239
|
+
candidate_match = re.fullmatch(r"阶段\s*(\d+)", candidate or "")
|
|
240
|
+
current_match = re.fullmatch(r"阶段\s*(\d+)", current or "")
|
|
241
|
+
return bool(candidate_match and current_match
|
|
242
|
+
and int(candidate_match.group(1)) < int(current_match.group(1)))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def legacy_review_state(plan_text, phase):
|
|
246
|
+
"""在不改写六列历史的前提下应用统一单次复核闭环。"""
|
|
247
|
+
result = {"passed": False, "pending_action": None, "issues": [], "blockers": []}
|
|
248
|
+
issues, blockers = result["issues"], result["blockers"]
|
|
249
|
+
rows = review_history_rows(plan_text)
|
|
250
|
+
known_phases = {row[0] for row in phase_roadmap_rows(plan_text)}
|
|
251
|
+
valid_rows = []
|
|
252
|
+
previous_dates = {}
|
|
253
|
+
for row in rows:
|
|
254
|
+
if len(row) != 6:
|
|
255
|
+
kind = row[1] if len(row) > 1 else ""
|
|
256
|
+
row_phase = row[2] if len(row) > 2 else ""
|
|
257
|
+
if ("准入" in kind or "readiness" in kind.lower() or "自验" in kind
|
|
258
|
+
or kind == "复核基线复用") and (not row_phase or row_phase == phase):
|
|
259
|
+
issues.append("旧独立复核记录必须使用日期、类型、阶段、结论、证据、复核者六列")
|
|
260
|
+
continue
|
|
261
|
+
kind, conclusion = row[1], row[3]
|
|
262
|
+
if not (
|
|
263
|
+
"准入" in kind
|
|
264
|
+
or "readiness" in kind.lower()
|
|
265
|
+
or "自验" in kind
|
|
266
|
+
or kind == "复核基线复用"
|
|
267
|
+
or (("完成验收" in kind or "完成复核" in kind) and review_outcome(conclusion) == "passed")
|
|
268
|
+
):
|
|
269
|
+
continue
|
|
270
|
+
row_phase = row[2]
|
|
271
|
+
if row_phase != phase and row_phase not in known_phases and not re.fullmatch(r"阶段\s*\d+", row_phase):
|
|
272
|
+
continue
|
|
273
|
+
valid_rows.append(row)
|
|
274
|
+
parsed = valid_review_date(row[0])
|
|
275
|
+
previous = previous_dates.get(row_phase)
|
|
276
|
+
if parsed and previous and parsed < previous:
|
|
277
|
+
issues.append(f"旧独立复核记录 {row_phase} 日期逆序,无法确认最新结果")
|
|
278
|
+
if parsed:
|
|
279
|
+
previous_dates[row_phase] = parsed
|
|
280
|
+
|
|
281
|
+
def qualified(row):
|
|
282
|
+
return (len(row) == 6 and not any(is_placeholder(value) for value in row)
|
|
283
|
+
and valid_review_date(row[0]) is not None)
|
|
284
|
+
|
|
285
|
+
def completed_independent(row):
|
|
286
|
+
conclusion = row[3].strip()
|
|
287
|
+
return qualified(row) and legacy_review_method(row[1]) == "独立" and (
|
|
288
|
+
review_outcome(conclusion) == "passed" or
|
|
289
|
+
conclusion.startswith(("未通过", "不通过", "失败", "不满足", "拒绝", "证据冲突"))
|
|
290
|
+
and review_outcome(conclusion) == "failed"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
prior_states = {}
|
|
294
|
+
for row in valid_rows:
|
|
295
|
+
if row[2] == phase or not phase_precedes(plan_text, row[2], phase):
|
|
296
|
+
continue
|
|
297
|
+
state = prior_states.setdefault(row[2], {"reviewed": False, "unresolved": False})
|
|
298
|
+
method = legacy_review_method(row[1])
|
|
299
|
+
outcome = review_outcome(row[3])
|
|
300
|
+
if method == "独立":
|
|
301
|
+
state["reviewed"] = completed_independent(row)
|
|
302
|
+
if outcome == "failed":
|
|
303
|
+
state["unresolved"] = True
|
|
304
|
+
elif (row[1] == "修复自验" and state["reviewed"] and outcome == "passed"
|
|
305
|
+
and qualified(row)):
|
|
306
|
+
state["unresolved"] = False
|
|
307
|
+
reusable_prior_phases = {
|
|
308
|
+
source_phase for source_phase, state in prior_states.items()
|
|
309
|
+
if state["reviewed"] and not state["unresolved"]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
current = [row for row in valid_rows if row[2] == phase]
|
|
313
|
+
reviewed = False
|
|
314
|
+
unresolved = None
|
|
315
|
+
for row in current:
|
|
316
|
+
method = legacy_review_method(row[1])
|
|
317
|
+
outcome = review_outcome(row[3])
|
|
318
|
+
if outcome == "unknown":
|
|
319
|
+
issues.append("旧独立复核记录结论为空或未知")
|
|
320
|
+
if method == "独立":
|
|
321
|
+
reviewed = completed_independent(row)
|
|
322
|
+
if outcome == "failed":
|
|
323
|
+
unresolved = row[3]
|
|
324
|
+
elif row[1] == "复核基线复用":
|
|
325
|
+
referenced = [source for source in reusable_prior_phases if source in row[4]]
|
|
326
|
+
if not referenced:
|
|
327
|
+
issues.append("旧格式复核基线复用须在证据中指向已解决的前阶段独立基线")
|
|
328
|
+
elif outcome == "passed" and qualified(row):
|
|
329
|
+
reviewed = True
|
|
330
|
+
elif row[1] == "修复自验":
|
|
331
|
+
if not reviewed:
|
|
332
|
+
issues.append("旧格式修复自验缺少已完成的独立复核基线")
|
|
333
|
+
elif outcome == "passed" and qualified(row):
|
|
334
|
+
unresolved = None
|
|
335
|
+
if outcome == "passed" and not qualified(row):
|
|
336
|
+
issues.append("旧独立复核记录通过缺少有效日期、类型、证据或身份")
|
|
337
|
+
|
|
338
|
+
if unresolved:
|
|
339
|
+
blockers.append("旧独立复核记录存在未修复的独立发现:" + unresolved)
|
|
340
|
+
if current and review_outcome(current[-1][3]) == "failed":
|
|
341
|
+
blockers.append("旧独立复核记录最新结果:" + current[-1][3])
|
|
342
|
+
|
|
343
|
+
review = key_value_table(fixed_section(plan_text, "最新独立准入复核"))
|
|
344
|
+
outcome = review_outcome(review.get("结论", ""))
|
|
345
|
+
latest_matches = bool(current) and all(
|
|
346
|
+
review.get(field) == current[-1][index]
|
|
347
|
+
for field, index in [("日期", 0), ("阶段", 2), ("结论", 3), ("证据", 4), ("复核者", 5)]
|
|
348
|
+
)
|
|
349
|
+
result["passed"] = outcome == "passed" and latest_matches and not issues and not blockers
|
|
350
|
+
if outcome == "pending" and not issues and not blockers:
|
|
351
|
+
method = legacy_review_method(current[-1][1]) if current else "独立"
|
|
352
|
+
result["pending_action"] = "verify" if method == "自验" else "independent_review"
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
|
|
220
356
|
def risk_review_state(plan_text, phase):
|
|
221
|
-
"""
|
|
357
|
+
"""显式风险策略的共享事实判定;历史策略名不再改变单次复核行为。"""
|
|
222
358
|
summary_section = fixed_section(top_level_section(plan_text, "当前阶段"), "阶段准入摘要")
|
|
223
359
|
policies = [row for row in markdown_table_rows(summary_section) if row[0] == "复核策略"]
|
|
224
360
|
result = {"enabled": bool(policies), "passed": False, "pending_action": None,
|
|
@@ -226,13 +362,12 @@ def risk_review_state(plan_text, phase):
|
|
|
226
362
|
if not policies:
|
|
227
363
|
return result
|
|
228
364
|
issues, blockers = result["issues"], result["blockers"]
|
|
229
|
-
single_review = len(policies) == 1 and len(policies[0]) == 2 and policies[0][1] == "单次独立复核"
|
|
230
365
|
if len(policies) != 1 or len(policies[0]) != 2 or policies[0][1] not in {"风险分流", "单次独立复核"}:
|
|
231
366
|
issues.append("复核策略必须唯一且为 风险分流/单次独立复核;空值、未知值或重复声明不能准入")
|
|
232
|
-
risks = {"低风险", "高影响"
|
|
367
|
+
risks = {"低风险", "高影响", "高风险"}
|
|
233
368
|
summary = key_value_table(summary_section)
|
|
234
369
|
if "最新独立准入复核" in summary:
|
|
235
|
-
issues.append("
|
|
370
|
+
issues.append("显式复核策略的阶段准入摘要应以 最新阶段复核 替代 最新独立准入复核")
|
|
236
371
|
if is_placeholder(summary.get("最新阶段复核")):
|
|
237
372
|
issues.append("阶段准入摘要缺少有效的 最新阶段复核 链接")
|
|
238
373
|
|
|
@@ -251,8 +386,6 @@ def risk_review_state(plan_text, phase):
|
|
|
251
386
|
issues.append("最新阶段复核风险必须为 " + "/".join(sorted(risks)) + "/待判断")
|
|
252
387
|
if review.get("风险") == "待判断":
|
|
253
388
|
issues.append("最新阶段复核风险待判断,不能准入")
|
|
254
|
-
if not single_review and review.get("方式") == "自验" and review.get("风险") != "低风险":
|
|
255
|
-
issues.append("自验只适用于低风险,不能代替高影响独立复核")
|
|
256
389
|
if is_placeholder(review.get("风险依据")):
|
|
257
390
|
issues.append("最新阶段复核风险依据为空或占位")
|
|
258
391
|
if outcome == "unknown":
|
|
@@ -260,13 +393,7 @@ def risk_review_state(plan_text, phase):
|
|
|
260
393
|
if outcome == "failed" and review.get("阶段") == phase:
|
|
261
394
|
blockers.append("最新阶段复核:" + review["结论"])
|
|
262
395
|
|
|
263
|
-
|
|
264
|
-
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
|
|
265
|
-
return None
|
|
266
|
-
try:
|
|
267
|
-
return parse_plan_date(value)
|
|
268
|
-
except ValueError:
|
|
269
|
-
return None
|
|
396
|
+
valid_date = valid_review_date
|
|
270
397
|
|
|
271
398
|
def qualified(row, legacy=False):
|
|
272
399
|
# 独立基线须有完整身份和证据;记录的结论另行判断。
|
|
@@ -288,7 +415,7 @@ def risk_review_state(plan_text, phase):
|
|
|
288
415
|
def known_other_phase(value):
|
|
289
416
|
return value != phase and (value in known_phases or re.fullmatch(r"阶段\s*\d+", value))
|
|
290
417
|
|
|
291
|
-
def
|
|
418
|
+
def history_rows(title, width):
|
|
292
419
|
section = fixed_section(plan_text, title)
|
|
293
420
|
rows = markdown_table_rows(section)
|
|
294
421
|
expected = (["日期", "类型", "阶段", "方式", "风险", "结论", "证据", "复核者"]
|
|
@@ -297,32 +424,58 @@ def risk_review_state(plan_text, phase):
|
|
|
297
424
|
issues.append(f"{title} 必须使用固定的 {width} 列表头")
|
|
298
425
|
if rows.count(expected) > 1:
|
|
299
426
|
issues.append(f"{title} 存在重复表头,无法确认记录结构")
|
|
300
|
-
|
|
301
|
-
|
|
427
|
+
valid = []
|
|
428
|
+
previous_dates = {}
|
|
302
429
|
for row in rows:
|
|
303
430
|
if row == expected:
|
|
304
431
|
continue
|
|
305
432
|
if len(row) > 2 and row[2] != phase:
|
|
306
|
-
if known_other_phase(row[2]):
|
|
433
|
+
if not known_other_phase(row[2]):
|
|
434
|
+
issues.append(f"{title} 记录阶段为空或未知,不能忽略")
|
|
307
435
|
continue
|
|
308
|
-
|
|
309
|
-
continue
|
|
310
|
-
if len(row) != width or len(row) < 3 or row[2] != phase:
|
|
436
|
+
if len(row) != width or len(row) < 3 or (row[2] != phase and not known_other_phase(row[2])):
|
|
311
437
|
issues.append(f"{title} 记录字段不足、列数不符或阶段不明")
|
|
312
438
|
continue
|
|
313
|
-
|
|
439
|
+
valid.append(row)
|
|
314
440
|
if is_placeholder(row[1]):
|
|
315
441
|
issues.append(f"{title} 类型为空或占位")
|
|
316
442
|
parsed = valid_date(row[0])
|
|
317
443
|
if not is_placeholder(row[0]) and parsed is None:
|
|
318
444
|
issues.append(f"{title} 日期不合法:{row[0]}")
|
|
445
|
+
previous_date = previous_dates.get(row[2])
|
|
319
446
|
if parsed and previous_date and parsed < previous_date:
|
|
320
|
-
issues.append(f"{title}
|
|
447
|
+
issues.append(f"{title} {row[2]} 记录日期逆序,无法确认最新结果")
|
|
321
448
|
if parsed:
|
|
322
|
-
|
|
323
|
-
return
|
|
449
|
+
previous_dates[row[2]] = parsed
|
|
450
|
+
return valid
|
|
451
|
+
|
|
452
|
+
all_history = history_rows("阶段复核记录", 8)
|
|
453
|
+
history = [row for row in all_history if row[2] == phase]
|
|
454
|
+
|
|
455
|
+
prior_states = {}
|
|
456
|
+
for row in all_history:
|
|
457
|
+
if (row[2] == phase or not phase_precedes(plan_text, row[2], phase)
|
|
458
|
+
or row[3] not in {"自验", "独立"}):
|
|
459
|
+
continue
|
|
460
|
+
state = prior_states.setdefault(row[2], {"reviewed_high": False, "unresolved": False})
|
|
461
|
+
if row[3] == "独立":
|
|
462
|
+
if completed(row):
|
|
463
|
+
state["reviewed_high"] = row[4] in {"高风险", "高影响"}
|
|
464
|
+
else:
|
|
465
|
+
state["reviewed_high"] = False
|
|
466
|
+
row_outcome = review_outcome(row[5])
|
|
467
|
+
if row_outcome == "failed":
|
|
468
|
+
state["unresolved"] = True
|
|
469
|
+
elif (row[1] == "修复自验" and state["reviewed_high"]
|
|
470
|
+
and review_outcome(row[5]) == "passed"
|
|
471
|
+
and not any(is_placeholder(value) for value in row)
|
|
472
|
+
and valid_date(row[0]) is not None):
|
|
473
|
+
state["unresolved"] = False
|
|
474
|
+
reusable_prior_phases = {
|
|
475
|
+
source_phase for source_phase, state in prior_states.items()
|
|
476
|
+
if state["reviewed_high"] and not state["unresolved"]
|
|
477
|
+
}
|
|
324
478
|
|
|
325
|
-
history = current_history("阶段复核记录", 8)
|
|
326
479
|
reviewed = False
|
|
327
480
|
reviewed_high = False
|
|
328
481
|
repair_date = None
|
|
@@ -330,31 +483,36 @@ def risk_review_state(plan_text, phase):
|
|
|
330
483
|
for row in history:
|
|
331
484
|
if row[3] not in {"自验", "独立"} or row[4] not in risks | {"待判断"}:
|
|
332
485
|
issues.append("阶段复核记录方式或风险未知")
|
|
333
|
-
if
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
486
|
+
if row[3] == "独立":
|
|
487
|
+
if completed(row):
|
|
488
|
+
reviewed = True
|
|
489
|
+
reviewed_high = row[4] in {"高风险", "高影响"}
|
|
490
|
+
else:
|
|
491
|
+
reviewed = reviewed_high = False
|
|
492
|
+
if review_outcome(row[5]) == "failed":
|
|
493
|
+
unresolved_single = row[5]
|
|
494
|
+
elif review_outcome(row[5]) == "passed" and completed(row):
|
|
495
|
+
# 后续独立通过可形成新范围基线,但不能清除既有独立发现。
|
|
496
|
+
pass
|
|
497
|
+
elif row[3] == "自验":
|
|
498
|
+
if row[1] == "复核基线复用":
|
|
499
|
+
referenced = [source for source in reusable_prior_phases if source in row[6]]
|
|
500
|
+
if row[4] not in {"高风险", "高影响"} or not referenced:
|
|
501
|
+
issues.append("复核基线复用须在证据中指向已解决的前阶段高风险/高影响独立基线")
|
|
502
|
+
elif (review_outcome(row[5]) == "passed"
|
|
503
|
+
and not any(is_placeholder(value) for value in row)
|
|
504
|
+
and valid_date(row[0]) is not None):
|
|
505
|
+
reviewed = reviewed_high = True
|
|
506
|
+
elif row[4] in {"高风险", "高影响"} and not reviewed_high:
|
|
507
|
+
issues.append("高风险/高影响自验缺少同范围独立复核基线或明确的跨阶段复用记录")
|
|
508
|
+
if row[1] == "修复自验":
|
|
509
|
+
if not reviewed:
|
|
510
|
+
issues.append("修复自验缺少已完成的独立复核基线")
|
|
511
|
+
elif (review_outcome(row[5]) == "passed" and row[4] in risks
|
|
512
|
+
and not any(is_placeholder(value) for value in row)
|
|
513
|
+
and valid_date(row[0]) is not None):
|
|
345
514
|
unresolved_single = None
|
|
346
515
|
repair_date = valid_date(row[0])
|
|
347
|
-
elif row[3] == "自验":
|
|
348
|
-
if row[4] in {"高风险", "高影响"} and not reviewed_high:
|
|
349
|
-
issues.append("高风险/高影响自验缺少本阶段已完成的独立复核基线")
|
|
350
|
-
if row[1] == "修复自验":
|
|
351
|
-
if not reviewed:
|
|
352
|
-
issues.append("修复自验缺少已完成的独立复核基线")
|
|
353
|
-
elif (review_outcome(row[5]) == "passed" and row[4] in risks
|
|
354
|
-
and not any(is_placeholder(value) for value in row)
|
|
355
|
-
and valid_date(row[0]) is not None):
|
|
356
|
-
unresolved_single = None
|
|
357
|
-
repair_date = valid_date(row[0])
|
|
358
516
|
if review_outcome(row[5]) == "unknown":
|
|
359
517
|
issues.append("阶段复核记录结论为空或未知")
|
|
360
518
|
if review_outcome(row[5]) == "passed" and (
|
|
@@ -377,21 +535,11 @@ def risk_review_state(plan_text, phase):
|
|
|
377
535
|
if (outcome == "passed" or not is_placeholder(review.get("日期"))) and valid_date(review.get("日期", "")) is None:
|
|
378
536
|
issues.append("最新阶段复核日期不合法")
|
|
379
537
|
|
|
380
|
-
|
|
381
|
-
unresolved = None
|
|
382
|
-
for row in independent:
|
|
383
|
-
row_outcome = review_outcome(row[5])
|
|
384
|
-
if row_outcome == "failed":
|
|
385
|
-
unresolved = row[5]
|
|
386
|
-
elif row_outcome == "passed" and qualified(row):
|
|
387
|
-
unresolved = None
|
|
388
|
-
if unresolved:
|
|
389
|
-
blockers.append("阶段复核记录存在未解除的独立失败:" + unresolved)
|
|
390
|
-
if single_review and unresolved_single:
|
|
538
|
+
if unresolved_single:
|
|
391
539
|
blockers.append("阶段复核记录存在未修复的独立发现:" + unresolved_single)
|
|
392
540
|
|
|
393
541
|
# 旧独立记录仍是独立事实源;完成失败也不能被新自验覆盖。
|
|
394
|
-
legacy_history =
|
|
542
|
+
legacy_history = [row for row in history_rows("独立复核记录", 6) if row[2] == phase]
|
|
395
543
|
legacy_section = fixed_section(plan_text, "最新独立准入复核")
|
|
396
544
|
legacy = key_value_table(legacy_section)
|
|
397
545
|
legacy_current = legacy.get("阶段") == phase
|
|
@@ -410,19 +558,19 @@ def risk_review_state(plan_text, phase):
|
|
|
410
558
|
unresolved = None
|
|
411
559
|
for row in legacy_history:
|
|
412
560
|
row_outcome = review_outcome(row[3])
|
|
413
|
-
if
|
|
561
|
+
if not qualified(row, legacy=True):
|
|
414
562
|
issues.append("旧独立复核记录缺少有效日期、类型、证据或身份,迁移不能掩盖")
|
|
415
563
|
if row_outcome == "failed":
|
|
416
564
|
unresolved = row[3]
|
|
417
565
|
elif row_outcome == "passed" and qualified(row, legacy=True):
|
|
418
|
-
|
|
566
|
+
pass
|
|
419
567
|
elif row_outcome == "passed":
|
|
420
568
|
issues.append("旧独立通过记录缺少有效日期、类型、证据或身份")
|
|
421
569
|
elif row_outcome == "unknown":
|
|
422
570
|
issues.append("旧独立复核记录结论为空或未知")
|
|
423
571
|
# 显式迁移后保留旧结论;新记录中的有效修复自验可解除旧发现。
|
|
424
572
|
legacy_dates = [valid_date(row[0]) for row in legacy_history]
|
|
425
|
-
legacy_repaired = (
|
|
573
|
+
legacy_repaired = (repair_date is not None and bool(legacy_dates)
|
|
426
574
|
and all(date is not None and date <= repair_date for date in legacy_dates)
|
|
427
575
|
and not unresolved_single)
|
|
428
576
|
if unresolved and not legacy_repaired:
|
|
@@ -557,10 +705,12 @@ def check_phase_structure(plan_name, data, plan_text, strict, warnings, errors,
|
|
|
557
705
|
history = [
|
|
558
706
|
row
|
|
559
707
|
for row in review_history_rows(plan_text)
|
|
560
|
-
if len(row)
|
|
708
|
+
if len(row) == 6
|
|
561
709
|
and row[2] == current_phase
|
|
562
710
|
and (
|
|
563
711
|
"准入" in row[1]
|
|
712
|
+
or "自验" in row[1]
|
|
713
|
+
or row[1] == "复核基线复用"
|
|
564
714
|
or ("完成验收" in row[1] and row[3].startswith("通过"))
|
|
565
715
|
or ("完成复核" in row[1] and row[3].startswith("通过"))
|
|
566
716
|
or "readiness" in row[1].lower()
|
|
@@ -590,6 +740,16 @@ def check_phase_structure(plan_name, data, plan_text, strict, warnings, errors,
|
|
|
590
740
|
strict,
|
|
591
741
|
f"{plan_name}: 最新独立准入复核与历史记录最后一条结论冲突",
|
|
592
742
|
)
|
|
743
|
+
legacy_state = legacy_review_state(plan_text, current_phase)
|
|
744
|
+
for issue in legacy_state["issues"] + legacy_state["blockers"]:
|
|
745
|
+
readiness_issue(warnings, errors, strict, f"{plan_name}: {issue}")
|
|
746
|
+
if not legacy_state["passed"] and not legacy_state["issues"] and not legacy_state["blockers"]:
|
|
747
|
+
readiness_issue(
|
|
748
|
+
warnings,
|
|
749
|
+
errors,
|
|
750
|
+
strict,
|
|
751
|
+
f"{plan_name}: 旧格式当前阶段记录未满足统一单次复核准入",
|
|
752
|
+
)
|
|
593
753
|
|
|
594
754
|
|
|
595
755
|
def markdown_list_items(section):
|
|
@@ -1798,6 +1958,8 @@ def phase_gate(plan_map_text, plan_name, data, plan_text):
|
|
|
1798
1958
|
if review_state["enabled"]:
|
|
1799
1959
|
blockers.extend(review_state["blockers"])
|
|
1800
1960
|
else:
|
|
1961
|
+
legacy_state = legacy_review_state(plan_text, data["phase"])
|
|
1962
|
+
blockers.extend(legacy_state["blockers"])
|
|
1801
1963
|
review = key_value_table(fixed_section(plan_text, "最新独立准入复核"))
|
|
1802
1964
|
conclusion = review.get("结论", "").strip()
|
|
1803
1965
|
if review.get("阶段") == data["phase"] and conclusion.startswith(("未通过", "不通过", "失败", "不满足", "拒绝")):
|
|
@@ -1837,8 +1999,7 @@ def current_summary(plan_text):
|
|
|
1837
1999
|
|
|
1838
2000
|
|
|
1839
2001
|
def current_review_passes(plan_text, phase):
|
|
1840
|
-
|
|
1841
|
-
return review.get("阶段") == phase and review.get("结论", "").startswith("通过")
|
|
2002
|
+
return legacy_review_state(plan_text, phase)["passed"]
|
|
1842
2003
|
|
|
1843
2004
|
|
|
1844
2005
|
def structured_next_action(plan_text):
|
|
@@ -2202,11 +2363,14 @@ def workset_payload(root, include_history=False, strict=False):
|
|
|
2202
2363
|
"当前阶段复核声明无法确定"),
|
|
2203
2364
|
}
|
|
2204
2365
|
elif not current_review_passes(plan_text, data["phase"]):
|
|
2366
|
+
legacy_state = legacy_review_state(plan_text, data["phase"])
|
|
2367
|
+
kind = legacy_state["pending_action"] or "independent_review"
|
|
2205
2368
|
readiness = "design"
|
|
2206
2369
|
action = {
|
|
2207
2370
|
"state": "known",
|
|
2208
|
-
"kind":
|
|
2209
|
-
"reason": "
|
|
2371
|
+
"kind": kind,
|
|
2372
|
+
"reason": ("当前阶段待自验" if kind == "verify" else
|
|
2373
|
+
"当前阶段尚无可用的单次独立复核基线"),
|
|
2210
2374
|
}
|
|
2211
2375
|
else:
|
|
2212
2376
|
readiness = "design"
|
|
@@ -19,8 +19,6 @@ VALID_STATUSES = {
|
|
|
19
19
|
"已废弃",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
CLAUDE_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
23
|
-
CLAUDE_SECTION_END = "<!-- plan-governance:end -->"
|
|
24
22
|
AGENTS_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
25
23
|
AGENTS_SECTION_END = "<!-- plan-governance:end -->"
|
|
26
24
|
|
|
@@ -60,7 +58,7 @@ def agent_rules_body():
|
|
|
60
58
|
|
|
61
59
|
最小执行边界:
|
|
62
60
|
|
|
63
|
-
-
|
|
61
|
+
- 普通一次性任务直接处理;已有计划按共享 verification 验证。新旧计划均为普通改动自验,高风险或高影响同范围独立复核一次,修复后自验。
|
|
64
62
|
- 只实施已授权、已准入的当前阶段。每个阶段有自身 Step 0、验证/完成和失败边界;未解决问题不能记为完成,不可用或超时不能冒充已检查。已有授权不重复索取,新增高影响外部动作按实际授权处理。
|
|
65
63
|
- 首次读取 `docs/PLAN_MAP.md`、当前相关 `docs/plans/*.md` 及适用契约/ADR/migration;恢复先核对当前工作集、最近证据和实际 diff,缺失或冲突再展开。
|
|
66
64
|
- 地图维护状态、阶段、关系、阻塞与证据入口;计划记录本次差异及验证,现行契约优先复用 Schema/OpenAPI。新事实不写回历史草案,变化同步地图及相关引用。
|
|
@@ -72,10 +70,6 @@ def managed_section(begin, end):
|
|
|
72
70
|
return f"{begin}\n{agent_rules_body()}{end}\n"
|
|
73
71
|
|
|
74
72
|
|
|
75
|
-
def claude_md_section():
|
|
76
|
-
return managed_section(CLAUDE_SECTION_BEGIN, CLAUDE_SECTION_END)
|
|
77
|
-
|
|
78
|
-
|
|
79
73
|
def agents_md_section():
|
|
80
74
|
return managed_section(AGENTS_SECTION_BEGIN, AGENTS_SECTION_END)
|
|
81
75
|
|
|
@@ -105,16 +99,6 @@ def update_managed_file(root, filename, section, begin, end):
|
|
|
105
99
|
return target
|
|
106
100
|
|
|
107
101
|
|
|
108
|
-
def update_claude_md(root):
|
|
109
|
-
return update_managed_file(
|
|
110
|
-
root,
|
|
111
|
-
"CLAUDE.md",
|
|
112
|
-
claude_md_section(),
|
|
113
|
-
CLAUDE_SECTION_BEGIN,
|
|
114
|
-
CLAUDE_SECTION_END,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
102
|
def update_agents_md(root):
|
|
119
103
|
return update_managed_file(
|
|
120
104
|
root,
|
|
@@ -126,7 +110,7 @@ def update_agents_md(root):
|
|
|
126
110
|
|
|
127
111
|
|
|
128
112
|
def update_agent_rules(root):
|
|
129
|
-
return [
|
|
113
|
+
return [update_agents_md(root)]
|
|
130
114
|
|
|
131
115
|
|
|
132
116
|
def plan_map_content(plan_slug, title, status, phase):
|
|
@@ -303,19 +287,16 @@ def parse_args(argv):
|
|
|
303
287
|
parser.add_argument("--status", default="设计中", choices=sorted(VALID_STATUSES), help="初始状态。")
|
|
304
288
|
parser.add_argument("--phase", default="阶段 0", help="当前阶段名称。")
|
|
305
289
|
parser.add_argument("--copy-checker", action="store_true", help="复制检查脚本到目标仓库 scripts/。")
|
|
306
|
-
parser.add_argument("--update-claude-md", action="store_true", help="创建或更新目标仓库 CLAUDE.md 中的计划治理规则。")
|
|
307
290
|
parser.add_argument("--update-agents-md", action="store_true", help="创建或更新目标仓库 AGENTS.md 中的计划治理规则。")
|
|
308
|
-
parser.add_argument("--update-agent-rules", action="store_true", help="
|
|
309
|
-
parser.add_argument("--update-claude-md-only", action="store_true", help="只创建或更新 CLAUDE.md,不初始化或覆盖 docs/。")
|
|
291
|
+
parser.add_argument("--update-agent-rules", action="store_true", help="创建或更新目标仓库 AGENTS.md 中的计划治理规则。")
|
|
310
292
|
parser.add_argument("--update-agents-md-only", action="store_true", help="只创建或更新 AGENTS.md,不初始化或覆盖 docs/。")
|
|
311
|
-
parser.add_argument("--update-agent-rules-only", action="store_true", help="只创建或更新
|
|
293
|
+
parser.add_argument("--update-agent-rules-only", action="store_true", help="只创建或更新 AGENTS.md,不初始化或覆盖 docs/。")
|
|
312
294
|
parser.add_argument("--upgrade-existing", action="store_true", help="升级已有项目的辅助文件:刷新检查脚本和代理规则,不覆盖 docs/。")
|
|
313
295
|
parser.add_argument("--migrate-plan-map-last-updated", action="store_true", help="将旧五列表 PLAN_MAP.md 迁移为包含最后更新的六列表。")
|
|
314
296
|
parser.add_argument("--last-updated-date", default=date.today().isoformat(), help="迁移 PLAN_MAP.md 时填入的最后更新日期,默认今天。")
|
|
315
297
|
parser.add_argument("--force", action="store_true", help="允许覆盖已存在的治理文件。")
|
|
316
298
|
args = parser.parse_args(argv)
|
|
317
299
|
only_modes = [
|
|
318
|
-
args.update_claude_md_only,
|
|
319
300
|
args.update_agents_md_only,
|
|
320
301
|
args.update_agent_rules_only,
|
|
321
302
|
args.upgrade_existing,
|
|
@@ -332,12 +313,6 @@ def main(argv=None):
|
|
|
332
313
|
args = parse_args(argv or sys.argv[1:])
|
|
333
314
|
root = Path(args.root).expanduser().resolve()
|
|
334
315
|
|
|
335
|
-
if args.update_claude_md_only:
|
|
336
|
-
target = update_claude_md(root)
|
|
337
|
-
print(f"已写入:{target}")
|
|
338
|
-
print("CLAUDE.md 已更新;未修改 docs/。")
|
|
339
|
-
return 0
|
|
340
|
-
|
|
341
316
|
if args.update_agents_md_only:
|
|
342
317
|
target = update_agents_md(root)
|
|
343
318
|
print(f"已写入:{target}")
|
|
@@ -390,11 +365,8 @@ def main(argv=None):
|
|
|
390
365
|
created.append(copy_checker(root, args.force))
|
|
391
366
|
if args.update_agent_rules:
|
|
392
367
|
created.extend(update_agent_rules(root))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
created.append(update_claude_md(root))
|
|
396
|
-
if args.update_agents_md:
|
|
397
|
-
created.append(update_agents_md(root))
|
|
368
|
+
elif args.update_agents_md:
|
|
369
|
+
created.append(update_agents_md(root))
|
|
398
370
|
|
|
399
371
|
for path in created:
|
|
400
372
|
print(f"已写入:{path}")
|