@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.
@@ -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
@@ -1,7 +1,10 @@
1
- # Task lifecycle · 最小真值(方向二文档先行 · v1)
1
+ # Task lifecycle · 最小真值(方向二 · v1)
2
2
  #
3
- # 只读登记:状态 / 转移 / 守卫。不由引擎执行。
4
- # CLI:npx @cyning/harness lifecycle show [--json]
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
- function runTestCheck(target, taskFile) {
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 { loadLifecycle, formatLifecycleShow } = await import('./lifecycle.js');
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(`用法: npx @cyning/harness lifecycle show [--json]
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
- 只读展示产品包 harness/lifecycle.yaml(状态 / 转移 / 守卫登记)。
514
- 不做转移执行、不写盘(方向二文档先行 · v2.7+)。
527
+ show · 只读展示 harness/lifecycle.yaml(登记真值 · v2.7+)
528
+ dry-run · 转移资格判定(引擎消费 yaml · v2.10+ · 旁路报告 · 非 runner / 非 G7)
515
529
  `);
516
530
  return;
517
531
  }
518
- if (sub !== 'show') {
519
- const err = new Error(`lifecycle 子命令未知: ${sub}\n用法: lifecycle show [--json]`);
520
- err.exitCode = 1;
521
- throw err;
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
- const json = rest.includes('--json');
524
- const unknown = rest.filter((a) => a !== '--json');
525
- if (unknown.length > 0) {
526
- const err = new Error(`lifecycle show 未知参数: ${unknown.join(' ')}`);
527
- err.exitCode = 1;
528
- throw err;
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
- const { data } = loadLifecycle();
531
- if (json) {
532
- console.log(JSON.stringify(data, null, 2));
533
- } else {
534
- console.log(formatLifecycleShow(data));
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
+ }