@cyning/harness 2.9.0 → 2.11.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/CHANGELOG.md +35 -0
- package/docs/ONBOARDING.md +15 -2
- package/docs/USER_GUIDE_v1.0_zh.md +8 -2
- package/docs/rethink/2026-07-mechanization-rate/03_coverage_matrix.md +4 -2
- package/docs/rethink/2026-07-mechanization-rate/04_next_steps.md +4 -3
- package/docs/spec/SPEC-discipline-coverage-yaml_v1.md +204 -0
- package/docs/spec/SPEC-discipline-show_v1.md +89 -0
- package/docs/spec/SPEC-lifecycle-engine-min_v1.md +246 -0
- package/docs/spec/SPEC-lifecycle-guard-expand_v1.md +100 -0
- package/docs/spec/SPEC-post-g4-debt-retro_v1.md +237 -0
- package/docs/spec/SPEC-verify-full-reviews-gate_v1.md +1 -1
- package/harness/discipline-coverage.yaml +289 -0
- package/harness/lifecycle.yaml +6 -3
- package/lib/audit.js +5 -1
- package/lib/cli.js +128 -20
- package/lib/discipline-coverage.js +148 -0
- package/lib/lifecycle.js +255 -0
- package/package.json +1 -1
- package/schema/discipline-coverage.v1.schema.json +55 -0
- package/schema/lifecycle.v1.schema.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Discipline coverage · Starter 子集机械化率资产(方向一)
|
|
2
|
+
#
|
|
3
|
+
# 运行时真值:本文件(随 npm 包分发)。
|
|
4
|
+
# 历史叙事:docs/rethink/2026-07-mechanization-rate/03_coverage_matrix.md
|
|
5
|
+
# 维护:新闸合入时更新 statements[].status / gaps[] / as_of_package_version
|
|
6
|
+
# 禁止:把本文件当「每周全量重盘」流程;不做 audit --discipline UI(本波)
|
|
7
|
+
|
|
8
|
+
version: "1"
|
|
9
|
+
as_of_package_version: "2.11.0"
|
|
10
|
+
scope: "Starter · harness/prompts (10/22/30/40 + FRAGMENT)+ TEMPLATE_invoke"
|
|
11
|
+
source_audit: "rethink/2026-07-mechanization-rate · 02 转录 + 03 聚合 · 状态已按 G1–G4/N1–N4 刷新(非第二轮通读盘点)"
|
|
12
|
+
|
|
13
|
+
gaps:
|
|
14
|
+
- id: G1
|
|
15
|
+
title: task md 结构 lint
|
|
16
|
+
status: closed
|
|
17
|
+
closed_in: "2.3.0"
|
|
18
|
+
note: "task lint E1–E7"
|
|
19
|
+
- id: G2
|
|
20
|
+
title: reviews 留档闸
|
|
21
|
+
status: closed
|
|
22
|
+
closed_in: "2.5.0"
|
|
23
|
+
note: "verify --task + close 检查 6;N4 全量 2.9.0"
|
|
24
|
+
- id: G3
|
|
25
|
+
title: 文本纪律(绝对路径)
|
|
26
|
+
status: closed
|
|
27
|
+
closed_in: "2.3.0"
|
|
28
|
+
note: "E6;invoke 预写 approved grep 仍缓做"
|
|
29
|
+
- id: G4
|
|
30
|
+
title: 思考轮结构
|
|
31
|
+
status: closed
|
|
32
|
+
closed_in: "2.6.0"
|
|
33
|
+
note: "task lint E8–E10 / W4–W5"
|
|
34
|
+
- id: G6
|
|
35
|
+
title: git 行为层
|
|
36
|
+
status: deferred
|
|
37
|
+
closed_in: null
|
|
38
|
+
note: "方向三"
|
|
39
|
+
- id: G7
|
|
40
|
+
title: 执行证据(真跑过)
|
|
41
|
+
status: deferred
|
|
42
|
+
closed_in: null
|
|
43
|
+
note: "方向二 runner;跟 lifecycle dry-run 之后"
|
|
44
|
+
- id: N2-C
|
|
45
|
+
title: verify --task lint → block
|
|
46
|
+
status: deferred
|
|
47
|
+
closed_in: null
|
|
48
|
+
note: "现 warn-only(2.7.0)· 须 FAIL 率下降"
|
|
49
|
+
- id: SPEC-reviews
|
|
50
|
+
title: SPEC 审查文闸
|
|
51
|
+
status: closed
|
|
52
|
+
closed_in: "2.8.0"
|
|
53
|
+
note: "verify --spec"
|
|
54
|
+
- id: FULL-reviews
|
|
55
|
+
title: 裸 verify 全量 reviews + 双路径
|
|
56
|
+
status: closed
|
|
57
|
+
closed_in: "2.9.0"
|
|
58
|
+
note: "N4"
|
|
59
|
+
- id: LIFECYCLE-ENGINE
|
|
60
|
+
title: lifecycle dry-run 最小引擎
|
|
61
|
+
status: closed
|
|
62
|
+
closed_in: "2.10.0"
|
|
63
|
+
note: "Epic T2 · 旁路报告"
|
|
64
|
+
|
|
65
|
+
statements:
|
|
66
|
+
- id: A1
|
|
67
|
+
source: 30-execute-code.md
|
|
68
|
+
summary: HG pending → 拒开工、禁改码
|
|
69
|
+
status: mechanical
|
|
70
|
+
mechanism: "M1 gate-check + M2 verify"
|
|
71
|
+
gap: null
|
|
72
|
+
notes: null
|
|
73
|
+
mechanism_quality: null
|
|
74
|
+
- id: A2
|
|
75
|
+
source: 30-execute-code.md
|
|
76
|
+
summary: 开工前首输出 GATE_VERIFY 闸扫描表
|
|
77
|
+
status: partial
|
|
78
|
+
mechanism: "M2 提供判定;Agent 是否先输出表无检查"
|
|
79
|
+
gap: null
|
|
80
|
+
notes: null
|
|
81
|
+
mechanism_quality: null
|
|
82
|
+
- id: A3
|
|
83
|
+
source: 30-execute-code.md
|
|
84
|
+
summary: 真值在 task 表;声称 vs 表 pending → STOP
|
|
85
|
+
status: partial
|
|
86
|
+
mechanism: "M1/M2 读表"
|
|
87
|
+
gap: null
|
|
88
|
+
notes: "用户声称侧不可机械"
|
|
89
|
+
mechanism_quality: null
|
|
90
|
+
- id: A4
|
|
91
|
+
source: 30-execute-code.md
|
|
92
|
+
summary: test_strategy required → 先可失败测试再改实现
|
|
93
|
+
status: partial
|
|
94
|
+
mechanism: "M3 D5 探测测试文件存在"
|
|
95
|
+
gap: null
|
|
96
|
+
notes: "先红后绿顺序无机制 · mechanism_quality 预留"
|
|
97
|
+
mechanism_quality: null
|
|
98
|
+
- id: A5
|
|
99
|
+
source: 30-execute-code.md
|
|
100
|
+
summary: 运行验证命令 + 回填自检结论
|
|
101
|
+
status: mechanical
|
|
102
|
+
mechanism: "M5 非占位符检查"
|
|
103
|
+
gap: G7
|
|
104
|
+
notes: "命令真跑过仍靠 40 诚实"
|
|
105
|
+
mechanism_quality: null
|
|
106
|
+
- id: A6
|
|
107
|
+
source: 30-execute-code.md
|
|
108
|
+
summary: invoke 快照落盘 by-task/<slug>/
|
|
109
|
+
status: mechanical
|
|
110
|
+
mechanism: "M5 close + M6 lint-done"
|
|
111
|
+
gap: null
|
|
112
|
+
notes: null
|
|
113
|
+
mechanism_quality: null
|
|
114
|
+
- id: A7
|
|
115
|
+
source: 30-execute-code.md
|
|
116
|
+
summary: 归档只能 task close PASS 后进行
|
|
117
|
+
status: mechanical
|
|
118
|
+
mechanism: "M5"
|
|
119
|
+
gap: null
|
|
120
|
+
notes: "手动 mv 旁路由 M6 兜底"
|
|
121
|
+
mechanism_quality: null
|
|
122
|
+
- id: A8
|
|
123
|
+
source: 30-execute-code.md
|
|
124
|
+
summary: HG-GRAPH-MODULES pending → 禁改码
|
|
125
|
+
status: mechanical
|
|
126
|
+
mechanism: "M1 gate-check --graph"
|
|
127
|
+
gap: null
|
|
128
|
+
notes: null
|
|
129
|
+
mechanism_quality: null
|
|
130
|
+
- id: A9
|
|
131
|
+
source: 30-execute-code.md
|
|
132
|
+
summary: 缺验收/failure_paths/必读 → 仅输出阻塞清单
|
|
133
|
+
status: mechanical
|
|
134
|
+
mechanism: "task lint E1–E7(G1 · v2.3.0)"
|
|
135
|
+
gap: G1
|
|
136
|
+
notes: "02 原 ❌ · 已刷新"
|
|
137
|
+
mechanism_quality: null
|
|
138
|
+
- id: A10
|
|
139
|
+
source: 30-execute-code.md
|
|
140
|
+
summary: 交接物 commit 仅本轮路径、禁 git add -A
|
|
141
|
+
status: prompt-only
|
|
142
|
+
mechanism: null
|
|
143
|
+
gap: G6
|
|
144
|
+
notes: null
|
|
145
|
+
mechanism_quality: null
|
|
146
|
+
- id: B1
|
|
147
|
+
source: 40-self-check.md
|
|
148
|
+
summary: 逐条对照验收标准标记 pass/fail
|
|
149
|
+
status: partial
|
|
150
|
+
mechanism: "M5 查无未勾选"
|
|
151
|
+
gap: null
|
|
152
|
+
notes: null
|
|
153
|
+
mechanism_quality: null
|
|
154
|
+
- id: B2
|
|
155
|
+
source: 40-self-check.md
|
|
156
|
+
summary: 必须运行验证命令并摘要退出码
|
|
157
|
+
status: prompt-only
|
|
158
|
+
mechanism: null
|
|
159
|
+
gap: G7
|
|
160
|
+
notes: "真跑过无证据"
|
|
161
|
+
mechanism_quality: null
|
|
162
|
+
- id: B3
|
|
163
|
+
source: 40-self-check.md
|
|
164
|
+
summary: 必须回填自检结论(执行者)
|
|
165
|
+
status: mechanical
|
|
166
|
+
mechanism: "M5"
|
|
167
|
+
gap: null
|
|
168
|
+
notes: null
|
|
169
|
+
mechanism_quality: null
|
|
170
|
+
- id: B4
|
|
171
|
+
source: 40-self-check.md
|
|
172
|
+
summary: 禁止改 tasks/reviews/invokes(S2)
|
|
173
|
+
status: prompt-only
|
|
174
|
+
mechanism: null
|
|
175
|
+
gap: G6
|
|
176
|
+
notes: null
|
|
177
|
+
mechanism_quality: null
|
|
178
|
+
- id: B5
|
|
179
|
+
source: 40-self-check.md
|
|
180
|
+
summary: 不凭记忆声称测过
|
|
181
|
+
status: prompt-only
|
|
182
|
+
mechanism: null
|
|
183
|
+
gap: null
|
|
184
|
+
notes: "诚实纪律 · 设计上留 Prompt"
|
|
185
|
+
mechanism_quality: null
|
|
186
|
+
- id: C1
|
|
187
|
+
source: 22-task-audit.md
|
|
188
|
+
summary: 必须落盘 reviews task_*_audit_R*
|
|
189
|
+
status: mechanical
|
|
190
|
+
mechanism: "findReview · verify/close(G2 · v2.5+;全量 v2.9)"
|
|
191
|
+
gap: G2
|
|
192
|
+
notes: "02 原 ❌ · 已刷新;内容质量仍 prompt-only"
|
|
193
|
+
mechanism_quality: null
|
|
194
|
+
- id: C2
|
|
195
|
+
source: 22-task-audit.md
|
|
196
|
+
summary: HG-AUDIT-R1 pending 时禁止附 30 Prompt
|
|
197
|
+
status: partial
|
|
198
|
+
mechanism: "存在性闸;审查文内容无检查"
|
|
199
|
+
gap: G2
|
|
200
|
+
notes: null
|
|
201
|
+
mechanism_quality: null
|
|
202
|
+
- id: C3
|
|
203
|
+
source: 22-task-audit.md
|
|
204
|
+
summary: 思考轮审查不通过 → 退回 10
|
|
205
|
+
status: prompt-only
|
|
206
|
+
mechanism: null
|
|
207
|
+
gap: null
|
|
208
|
+
notes: "流程纪律"
|
|
209
|
+
mechanism_quality: null
|
|
210
|
+
- id: C4
|
|
211
|
+
source: 22-task-audit.md
|
|
212
|
+
summary: 零阻塞写已核对项;终轮写签收
|
|
213
|
+
status: prompt-only
|
|
214
|
+
mechanism: null
|
|
215
|
+
gap: null
|
|
216
|
+
notes: "审查文结构无 lint"
|
|
217
|
+
mechanism_quality: null
|
|
218
|
+
- id: D1
|
|
219
|
+
source: 10-requirements.md
|
|
220
|
+
summary: task 必含验收/failure_paths/非范围/依赖
|
|
221
|
+
status: mechanical
|
|
222
|
+
mechanism: "task lint(G1)"
|
|
223
|
+
gap: G1
|
|
224
|
+
notes: "02 原 ❌ · 已刷新"
|
|
225
|
+
mechanism_quality: null
|
|
226
|
+
- id: D2
|
|
227
|
+
source: 10-requirements.md
|
|
228
|
+
summary: 不写绝对本机路径
|
|
229
|
+
status: mechanical
|
|
230
|
+
mechanism: "task lint E6(G3)"
|
|
231
|
+
gap: G3
|
|
232
|
+
notes: "02 原 ❌ · 已刷新"
|
|
233
|
+
mechanism_quality: null
|
|
234
|
+
- id: D3
|
|
235
|
+
source: 10-requirements.md
|
|
236
|
+
summary: 预置 R0–R5 + 思考轮控制表
|
|
237
|
+
status: mechanical
|
|
238
|
+
mechanism: "task lint E8–E10(G4 · 条件触发)"
|
|
239
|
+
gap: G4
|
|
240
|
+
notes: "02 原 ❌ · 已刷新;内容质量仍 20 帽"
|
|
241
|
+
mechanism_quality: null
|
|
242
|
+
- id: D4
|
|
243
|
+
source: 10-requirements.md
|
|
244
|
+
summary: 缺验收/failure_paths → 仅输出阻塞清单
|
|
245
|
+
status: mechanical
|
|
246
|
+
mechanism: "task lint(G1)"
|
|
247
|
+
gap: G1
|
|
248
|
+
notes: null
|
|
249
|
+
mechanism_quality: null
|
|
250
|
+
- id: E1
|
|
251
|
+
source: FRAGMENT_30
|
|
252
|
+
summary: invoke 中禁止预写 HG-AUDIT-R1 approved
|
|
253
|
+
status: prompt-only
|
|
254
|
+
mechanism: null
|
|
255
|
+
gap: G3
|
|
256
|
+
notes: "SPEC R2 缓做 · 误报风险"
|
|
257
|
+
mechanism_quality: null
|
|
258
|
+
- id: E2
|
|
259
|
+
source: FRAGMENT_30
|
|
260
|
+
summary: 用户确认 approved = 须核验非事实
|
|
261
|
+
status: partial
|
|
262
|
+
mechanism: "M2 读表"
|
|
263
|
+
gap: null
|
|
264
|
+
notes: null
|
|
265
|
+
mechanism_quality: null
|
|
266
|
+
- id: F1
|
|
267
|
+
source: TEMPLATE_invoke.md
|
|
268
|
+
summary: 落盘路径 by-task/<task_slug>/
|
|
269
|
+
status: mechanical
|
|
270
|
+
mechanism: "M5/M6"
|
|
271
|
+
gap: null
|
|
272
|
+
notes: null
|
|
273
|
+
mechanism_quality: null
|
|
274
|
+
- id: F2
|
|
275
|
+
source: TEMPLATE_invoke.md
|
|
276
|
+
summary: 同帽追问不新增 invoke;打回用 _r2
|
|
277
|
+
status: prompt-only
|
|
278
|
+
mechanism: null
|
|
279
|
+
gap: null
|
|
280
|
+
notes: "命名纪律 · 可留 prompt"
|
|
281
|
+
mechanism_quality: null
|
|
282
|
+
- id: F3
|
|
283
|
+
source: TEMPLATE_invoke.md
|
|
284
|
+
summary: 落盘 + task 回填后再 commit
|
|
285
|
+
status: prompt-only
|
|
286
|
+
mechanism: null
|
|
287
|
+
gap: G6
|
|
288
|
+
notes: null
|
|
289
|
+
mechanism_quality: null
|
package/harness/lifecycle.yaml
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# Task lifecycle ·
|
|
1
|
+
# Task lifecycle · 最小真值(方向二 · v1)
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# 登记真值:状态 / 转移 / 守卫。yaml 本身不是引擎。
|
|
4
|
+
# 由 `lifecycle dry-run` 引擎消费(v2.10+ · 资格判定旁路报告 · 不写盘)。
|
|
5
|
+
# v2.11+ to_30 已接线:HG-AUDIT-R1 · HG-TASK-DRAFT · reviews_retention · audit_D5 · task_lint
|
|
6
|
+
# (close_* / 可选 to_00 仍可 unevaluated)
|
|
7
|
+
# 只读展示:npx @cyning/harness lifecycle show [--json]
|
|
5
8
|
# Schema:schema/lifecycle.v1.schema.json
|
|
6
9
|
|
|
7
10
|
version: "1"
|
package/lib/audit.js
CHANGED
|
@@ -45,7 +45,11 @@ function runGateCheck(target, taskFile) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* D5 · test_strategy vs 测试/CI 制品(供 audit / lifecycle dry-run 复用)。
|
|
50
|
+
* @returns {{ ok: boolean, skipped?: boolean, reason: string }}
|
|
51
|
+
*/
|
|
52
|
+
export function runTestCheck(target, taskFile) {
|
|
49
53
|
// 未指定 task 文件时,D5 跳过(在 active task 上跑 gate-check 已覆盖 D3)
|
|
50
54
|
if (!taskFile) {
|
|
51
55
|
return { ok: true, skipped: true, reason: '未指定 --task,跳过 D5' };
|
package/lib/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ function usage(version = 'unknown') {
|
|
|
43
43
|
npx @cyning/harness gate-check [--target PATH] [--task FILE] [--graph] [--json]
|
|
44
44
|
npx @cyning/harness verify [--target PATH] [--task FILE | --spec FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH]
|
|
45
45
|
npx @cyning/harness lifecycle show [--json]
|
|
46
|
+
npx @cyning/harness discipline show [--json]
|
|
46
47
|
npx @cyning/harness sync index [--target PATH]
|
|
47
48
|
npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
|
|
48
49
|
npx @cyning/harness task close --file PATH [--target PATH] [--yes] [--allow-unchecked]
|
|
@@ -63,7 +64,8 @@ function usage(version = 'unknown') {
|
|
|
63
64
|
audit ICVO 机械审计:复用 gate-check · D5 测试声明检查 · S5 git-clean 提醒
|
|
64
65
|
gate-check 调用 wizard/gate-check.sh(业务仓无需 clone 即可用)
|
|
65
66
|
verify 30 前聚合(--task)或 SPEC→00 审查文闸(--spec · v2.8+)
|
|
66
|
-
lifecycle 只读展示 harness/lifecycle.yaml(状态/转移/守卫 · v2.7+)
|
|
67
|
+
lifecycle 只读展示 harness/lifecycle.yaml(状态/转移/守卫 · v2.7+;dry-run v2.10+)
|
|
68
|
+
discipline 只读展示 harness/discipline-coverage.yaml(机械化率资产 · v2.11+)
|
|
67
69
|
sync index 生成 .cyning-harness/invoke_index.json
|
|
68
70
|
task check 校验 task.harness.v1.json sidecar · --no-circular 检测 depends_on 环
|
|
69
71
|
task close 受闸归档:5 项机械校验(invoke/自检结论/勾选/slug/状态)后 mv active→done(v2.2+)
|
|
@@ -134,6 +136,10 @@ export async function runCli(argv) {
|
|
|
134
136
|
await cmdLifecycle(rest);
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
139
|
+
if (cmd === 'discipline') {
|
|
140
|
+
await cmdDiscipline(rest);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
137
143
|
if (cmd === 'sync') {
|
|
138
144
|
await cmdSync(rest);
|
|
139
145
|
return;
|
|
@@ -505,34 +511,136 @@ async function cmdVerify(args) {
|
|
|
505
511
|
}
|
|
506
512
|
|
|
507
513
|
async function cmdLifecycle(args) {
|
|
508
|
-
const {
|
|
514
|
+
const {
|
|
515
|
+
loadLifecycle,
|
|
516
|
+
formatLifecycleShow,
|
|
517
|
+
dryRunTransition,
|
|
518
|
+
formatLifecycleDryRun,
|
|
519
|
+
} = await import('./lifecycle.js');
|
|
509
520
|
const [sub, ...rest] = args;
|
|
510
521
|
if (!sub || sub === '--help' || sub === '-h' || args.includes('--help') || args.includes('-h')) {
|
|
511
|
-
console.log(`用法:
|
|
522
|
+
console.log(`用法:
|
|
523
|
+
npx @cyning/harness lifecycle show [--json]
|
|
524
|
+
npx @cyning/harness lifecycle dry-run --transition ID --from STATE [--task PATH] [--json]
|
|
525
|
+
[--allow-no-review] [--allow-lint-fail] [--allow-no-spec-review]
|
|
512
526
|
|
|
513
|
-
|
|
514
|
-
|
|
527
|
+
show · 只读展示 harness/lifecycle.yaml(登记真值 · v2.7+)
|
|
528
|
+
dry-run · 转移资格判定(引擎消费 yaml · v2.10+ · 旁路报告 · 非 runner / 非 G7)
|
|
515
529
|
`);
|
|
516
530
|
return;
|
|
517
531
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
532
|
+
|
|
533
|
+
if (sub === 'show') {
|
|
534
|
+
const json = rest.includes('--json');
|
|
535
|
+
const unknown = rest.filter((a) => a !== '--json');
|
|
536
|
+
if (unknown.length > 0) {
|
|
537
|
+
const err = new Error(`lifecycle show 未知参数: ${unknown.join(' ')}`);
|
|
538
|
+
err.exitCode = 1;
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
const { data } = loadLifecycle();
|
|
542
|
+
if (json) {
|
|
543
|
+
console.log(JSON.stringify(data, null, 2));
|
|
544
|
+
} else {
|
|
545
|
+
console.log(formatLifecycleShow(data));
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
522
548
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
549
|
+
|
|
550
|
+
if (sub === 'dry-run') {
|
|
551
|
+
let remaining = rest;
|
|
552
|
+
const { value: transitionId, rest: r1 } = takeOption(remaining, '--transition');
|
|
553
|
+
remaining = r1;
|
|
554
|
+
const { value: fromState, rest: r2 } = takeOption(remaining, '--from');
|
|
555
|
+
remaining = r2;
|
|
556
|
+
const { value: taskPath, rest: r3 } = takeOption(remaining, '--task');
|
|
557
|
+
remaining = r3;
|
|
558
|
+
const json = remaining.includes('--json');
|
|
559
|
+
const allowNoReview = remaining.includes('--allow-no-review');
|
|
560
|
+
const allowLintFail = remaining.includes('--allow-lint-fail');
|
|
561
|
+
const allowNoSpecReview = remaining.includes('--allow-no-spec-review');
|
|
562
|
+
remaining = remaining.filter(
|
|
563
|
+
(a) =>
|
|
564
|
+
a !== '--json' &&
|
|
565
|
+
a !== '--allow-no-review' &&
|
|
566
|
+
a !== '--allow-lint-fail' &&
|
|
567
|
+
a !== '--allow-no-spec-review',
|
|
568
|
+
);
|
|
569
|
+
if (remaining.length > 0) {
|
|
570
|
+
const err = new Error(`lifecycle dry-run 未知参数: ${remaining.join(' ')}`);
|
|
571
|
+
err.exitCode = 1;
|
|
572
|
+
throw err;
|
|
573
|
+
}
|
|
574
|
+
if (!transitionId || !fromState) {
|
|
575
|
+
const err = new Error(
|
|
576
|
+
'lifecycle dry-run 须 --transition <id> 与 --from <state>\n提示: lifecycle show',
|
|
577
|
+
);
|
|
578
|
+
err.exitCode = 1;
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const report = dryRunTransition({
|
|
583
|
+
transitionId,
|
|
584
|
+
fromState,
|
|
585
|
+
taskPath,
|
|
586
|
+
flags: { allowNoReview, allowLintFail, allowNoSpecReview },
|
|
587
|
+
cwd: process.cwd(),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
if (json) {
|
|
591
|
+
console.log(JSON.stringify(report, null, 2));
|
|
592
|
+
} else {
|
|
593
|
+
console.log(formatLifecycleDryRun(report));
|
|
594
|
+
}
|
|
595
|
+
if (report.exitCode && report.exitCode !== 0) {
|
|
596
|
+
process.exitCode = report.exitCode;
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
529
599
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
600
|
+
|
|
601
|
+
const err = new Error(
|
|
602
|
+
`lifecycle 子命令未知: ${sub}\n用法: lifecycle show [--json] · lifecycle dry-run --transition ID --from STATE`,
|
|
603
|
+
);
|
|
604
|
+
err.exitCode = 1;
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function cmdDiscipline(args) {
|
|
609
|
+
const { loadDisciplineCoverage, formatDisciplineShow } = await import(
|
|
610
|
+
'./discipline-coverage.js'
|
|
611
|
+
);
|
|
612
|
+
const [sub, ...rest] = args;
|
|
613
|
+
if (!sub || sub === '--help' || sub === '-h' || args.includes('--help') || args.includes('-h')) {
|
|
614
|
+
console.log(`用法:
|
|
615
|
+
npx @cyning/harness discipline show [--json]
|
|
616
|
+
|
|
617
|
+
show · 只读展示 harness/discipline-coverage.yaml(机械化率资产 · v2.11+ · 非 audit UI)
|
|
618
|
+
`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (sub === 'show') {
|
|
623
|
+
const json = rest.includes('--json');
|
|
624
|
+
const unknown = rest.filter((a) => a !== '--json');
|
|
625
|
+
if (unknown.length > 0) {
|
|
626
|
+
const err = new Error(`discipline show 未知参数: ${unknown.join(' ')}`);
|
|
627
|
+
err.exitCode = 1;
|
|
628
|
+
throw err;
|
|
629
|
+
}
|
|
630
|
+
const { data } = loadDisciplineCoverage();
|
|
631
|
+
if (json) {
|
|
632
|
+
console.log(JSON.stringify(data, null, 2));
|
|
633
|
+
} else {
|
|
634
|
+
console.log(formatDisciplineShow(data));
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
535
637
|
}
|
|
638
|
+
|
|
639
|
+
const err = new Error(
|
|
640
|
+
`discipline 子命令未知: ${sub ?? '(空)'}\n用法: discipline show [--json]`,
|
|
641
|
+
);
|
|
642
|
+
err.exitCode = 1;
|
|
643
|
+
throw err;
|
|
536
644
|
}
|
|
537
645
|
|
|
538
646
|
async function cmdSync(args) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { resolveHarnessRoot } from './paths.js';
|
|
5
|
+
|
|
6
|
+
const STATUSES = new Set(['mechanical', 'partial', 'prompt-only']);
|
|
7
|
+
const GAP_STATUSES = new Set(['open', 'closed', 'deferred']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 轻量结构校验(对齐 schema/discipline-coverage.v1.schema.json 必填约束)。
|
|
11
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
12
|
+
*/
|
|
13
|
+
export function validateDisciplineCoverage(data) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
if (!data || typeof data !== 'object') {
|
|
16
|
+
return { ok: false, errors: ['根须为 object'] };
|
|
17
|
+
}
|
|
18
|
+
if (typeof data.version !== 'string' || !data.version.trim()) {
|
|
19
|
+
errors.push('缺 version');
|
|
20
|
+
}
|
|
21
|
+
if (typeof data.as_of_package_version !== 'string' || !data.as_of_package_version.trim()) {
|
|
22
|
+
errors.push('缺 as_of_package_version');
|
|
23
|
+
}
|
|
24
|
+
if (typeof data.scope !== 'string' || !data.scope.trim()) {
|
|
25
|
+
errors.push('缺 scope');
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(data.statements) || data.statements.length === 0) {
|
|
28
|
+
errors.push('缺 statements[] 或为空');
|
|
29
|
+
} else {
|
|
30
|
+
data.statements.forEach((s, i) => {
|
|
31
|
+
if (!s || typeof s.id !== 'string' || !s.id.trim()) {
|
|
32
|
+
errors.push(`statements[${i}] 缺 id`);
|
|
33
|
+
}
|
|
34
|
+
if (!s || typeof s.source !== 'string') {
|
|
35
|
+
errors.push(`statements[${i}] 缺 source`);
|
|
36
|
+
}
|
|
37
|
+
if (!s || typeof s.summary !== 'string') {
|
|
38
|
+
errors.push(`statements[${i}] 缺 summary`);
|
|
39
|
+
}
|
|
40
|
+
if (!s || !STATUSES.has(s.status)) {
|
|
41
|
+
errors.push(`statements[${i}] status 非法: ${s?.status}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (data.gaps != null) {
|
|
46
|
+
if (!Array.isArray(data.gaps)) {
|
|
47
|
+
errors.push('gaps 须为 array');
|
|
48
|
+
} else {
|
|
49
|
+
data.gaps.forEach((g, i) => {
|
|
50
|
+
if (!g || typeof g.id !== 'string') errors.push(`gaps[${i}] 缺 id`);
|
|
51
|
+
if (!g || typeof g.title !== 'string') errors.push(`gaps[${i}] 缺 title`);
|
|
52
|
+
if (!g || !GAP_STATUSES.has(g.status)) {
|
|
53
|
+
errors.push(`gaps[${i}] status 非法: ${g?.status}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { ok: errors.length === 0, errors };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 加载并校验 harness/discipline-coverage.yaml。
|
|
63
|
+
*/
|
|
64
|
+
export function loadDisciplineCoverage(options = {}) {
|
|
65
|
+
const harnessRoot = options.harnessRoot ?? resolveHarnessRoot();
|
|
66
|
+
const filePath =
|
|
67
|
+
options.filePath ??
|
|
68
|
+
path.join(harnessRoot, 'harness', 'discipline-coverage.yaml');
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
const e = new Error(`discipline-coverage.yaml 不存在: ${filePath}`);
|
|
72
|
+
e.exitCode = 1;
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let data;
|
|
77
|
+
try {
|
|
78
|
+
data = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const e = new Error(`discipline-coverage.yaml 解析失败: ${err.message}`);
|
|
81
|
+
e.exitCode = 1;
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const v = validateDisciplineCoverage(data);
|
|
86
|
+
if (!v.ok) {
|
|
87
|
+
const e = new Error(
|
|
88
|
+
`discipline-coverage.yaml 校验失败:\n - ${v.errors.join('\n - ')}`,
|
|
89
|
+
);
|
|
90
|
+
e.exitCode = 1;
|
|
91
|
+
e.validationErrors = v.errors;
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { data, filePath, harnessRoot };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 人读摘要(镜像 lifecycle show · 只读)。
|
|
100
|
+
*/
|
|
101
|
+
export function formatDisciplineShow(data) {
|
|
102
|
+
const byStatus = {};
|
|
103
|
+
for (const s of data.statements ?? []) {
|
|
104
|
+
byStatus[s.status] = (byStatus[s.status] ?? 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
const gapBy = {};
|
|
107
|
+
for (const g of data.gaps ?? []) {
|
|
108
|
+
gapBy[g.status] = (gapBy[g.status] ?? 0) + 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lines = [
|
|
112
|
+
`discipline-coverage v${data.version}`,
|
|
113
|
+
`as_of: ${data.as_of_package_version}`,
|
|
114
|
+
`scope: ${data.scope}`,
|
|
115
|
+
'',
|
|
116
|
+
`## statements (${data.statements?.length ?? 0})`,
|
|
117
|
+
...Object.entries(byStatus)
|
|
118
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
119
|
+
.map(([k, n]) => `- ${k}: ${n}`),
|
|
120
|
+
'',
|
|
121
|
+
`## gaps (${data.gaps?.length ?? 0})`,
|
|
122
|
+
];
|
|
123
|
+
if ((data.gaps?.length ?? 0) === 0) {
|
|
124
|
+
lines.push('- (无)');
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(
|
|
127
|
+
...Object.entries(gapBy)
|
|
128
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
129
|
+
.map(([k, n]) => `- ${k}: ${n}`),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.push('', '## sample statements(最多 12)');
|
|
134
|
+
for (const s of (data.statements ?? []).slice(0, 12)) {
|
|
135
|
+
const sum =
|
|
136
|
+
s.summary.length > 72 ? `${s.summary.slice(0, 69)}…` : s.summary;
|
|
137
|
+
lines.push(`- [${s.status}] ${s.id}: ${sum}`);
|
|
138
|
+
}
|
|
139
|
+
if ((data.statements?.length ?? 0) > 12) {
|
|
140
|
+
lines.push(`- … +${data.statements.length - 12} more`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
lines.push(
|
|
144
|
+
'',
|
|
145
|
+
'注: SoT = harness/discipline-coverage.yaml · show 只读 · 非 audit --discipline UI',
|
|
146
|
+
);
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|