@cyning/harness 2.8.0 → 2.10.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.10.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,8 @@
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
+ # 只读展示:npx @cyning/harness lifecycle show [--json]
5
6
  # Schema:schema/lifecycle.v1.schema.json
6
7
 
7
8
  version: "1"
@@ -34,7 +35,7 @@ transitions:
34
35
  from: ["draft", "in_progress"]
35
36
  to: in_progress
36
37
  hat: "30"
37
- description: "执行帽开工(verify --task 聚合)"
38
+ description: "执行帽开工(verify --task 与全量模式聚合 · v2.9+)"
38
39
  guards:
39
40
  - id: HG-AUDIT-R1
40
41
  command_or_check: "gate-check · human_gate HG-AUDIT-R1=approved"
@@ -46,9 +47,10 @@ transitions:
46
47
  command_or_check: "verify/audit · test_strategy vs 测试文件存在"
47
48
  severity: block
48
49
  - id: reviews_retention
49
- command_or_check: "verify · findReview R<n> 存在性(v2.5+)"
50
+ command_or_check: "verify --task 与全量 · findReview R<n> 存在性(v2.5+ · 全量 v2.9+)"
50
51
  severity: block
51
52
  allow_flag: "--allow-no-review"
53
+ note: "全量模式扫描双路径 active;不跑 lint / D5"
52
54
  - id: task_lint
53
55
  command_or_check: "verify --task · lintTaskFile(v2.7+)"
54
56
  severity: warn
package/lib/cli.js CHANGED
@@ -400,7 +400,8 @@ async function cmdVerify(args) {
400
400
  if (args.includes('--help') || args.includes('-h')) {
401
401
  console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE | --spec FILE] [--graph] [--json] [--agent-hint] [--workspace-root PATH] [--allow-no-review] [--allow-lint-fail] [--allow-no-spec-review]
402
402
 
403
- 无 --task/--spec:扫描 docs/tasks/active/task_*.md 并逐项列出闸表(同 gate-check);不跑 task lint。
403
+ 无 --task/--spec:扫描 docs/tasks/active ∪ docs/harness/tasks/active(同 basename 优先 Starter);
404
+ 闸表 + 全量 reviews 存在性(v2.9+)· 不跑 task lint / D5 · --allow-no-review。
404
405
  --task:30 前聚合(gate + audit + reviews + lint WARN)· --allow-no-review / --allow-lint-fail。
405
406
  --spec:SPEC→00 前查审查文存在性(v2.8+ · 与 --task 互斥)· --allow-no-spec-review。
406
407
  推荐:--spec <产品仓 SPEC> --workspace-root <含 docs/harness/reviews 的工作区根>
@@ -504,34 +505,98 @@ async function cmdVerify(args) {
504
505
  }
505
506
 
506
507
  async function cmdLifecycle(args) {
507
- const { loadLifecycle, formatLifecycleShow } = await import('./lifecycle.js');
508
+ const {
509
+ loadLifecycle,
510
+ formatLifecycleShow,
511
+ dryRunTransition,
512
+ formatLifecycleDryRun,
513
+ } = await import('./lifecycle.js');
508
514
  const [sub, ...rest] = args;
509
515
  if (!sub || sub === '--help' || sub === '-h' || args.includes('--help') || args.includes('-h')) {
510
- console.log(`用法: npx @cyning/harness lifecycle show [--json]
516
+ console.log(`用法:
517
+ npx @cyning/harness lifecycle show [--json]
518
+ npx @cyning/harness lifecycle dry-run --transition ID --from STATE [--task PATH] [--json]
519
+ [--allow-no-review] [--allow-lint-fail] [--allow-no-spec-review]
511
520
 
512
- 只读展示产品包 harness/lifecycle.yaml(状态 / 转移 / 守卫登记)。
513
- 不做转移执行、不写盘(方向二文档先行 · v2.7+)。
521
+ show · 只读展示 harness/lifecycle.yaml(登记真值 · v2.7+)
522
+ dry-run · 转移资格判定(引擎消费 yaml · v2.10+ · 旁路报告 · 非 runner / 非 G7)
514
523
  `);
515
524
  return;
516
525
  }
517
- if (sub !== 'show') {
518
- const err = new Error(`lifecycle 子命令未知: ${sub}\n用法: lifecycle show [--json]`);
519
- err.exitCode = 1;
520
- throw err;
521
- }
522
- const json = rest.includes('--json');
523
- const unknown = rest.filter((a) => a !== '--json');
524
- if (unknown.length > 0) {
525
- const err = new Error(`lifecycle show 未知参数: ${unknown.join(' ')}`);
526
- err.exitCode = 1;
527
- throw err;
526
+
527
+ if (sub === 'show') {
528
+ const json = rest.includes('--json');
529
+ const unknown = rest.filter((a) => a !== '--json');
530
+ if (unknown.length > 0) {
531
+ const err = new Error(`lifecycle show 未知参数: ${unknown.join(' ')}`);
532
+ err.exitCode = 1;
533
+ throw err;
534
+ }
535
+ const { data } = loadLifecycle();
536
+ if (json) {
537
+ console.log(JSON.stringify(data, null, 2));
538
+ } else {
539
+ console.log(formatLifecycleShow(data));
540
+ }
541
+ return;
528
542
  }
529
- const { data } = loadLifecycle();
530
- if (json) {
531
- console.log(JSON.stringify(data, null, 2));
532
- } else {
533
- console.log(formatLifecycleShow(data));
543
+
544
+ if (sub === 'dry-run') {
545
+ let remaining = rest;
546
+ const { value: transitionId, rest: r1 } = takeOption(remaining, '--transition');
547
+ remaining = r1;
548
+ const { value: fromState, rest: r2 } = takeOption(remaining, '--from');
549
+ remaining = r2;
550
+ const { value: taskPath, rest: r3 } = takeOption(remaining, '--task');
551
+ remaining = r3;
552
+ const json = remaining.includes('--json');
553
+ const allowNoReview = remaining.includes('--allow-no-review');
554
+ const allowLintFail = remaining.includes('--allow-lint-fail');
555
+ const allowNoSpecReview = remaining.includes('--allow-no-spec-review');
556
+ remaining = remaining.filter(
557
+ (a) =>
558
+ a !== '--json' &&
559
+ a !== '--allow-no-review' &&
560
+ a !== '--allow-lint-fail' &&
561
+ a !== '--allow-no-spec-review',
562
+ );
563
+ if (remaining.length > 0) {
564
+ const err = new Error(`lifecycle dry-run 未知参数: ${remaining.join(' ')}`);
565
+ err.exitCode = 1;
566
+ throw err;
567
+ }
568
+ if (!transitionId || !fromState) {
569
+ const err = new Error(
570
+ 'lifecycle dry-run 须 --transition <id> 与 --from <state>\n提示: lifecycle show',
571
+ );
572
+ err.exitCode = 1;
573
+ throw err;
574
+ }
575
+
576
+ const report = dryRunTransition({
577
+ transitionId,
578
+ fromState,
579
+ taskPath,
580
+ flags: { allowNoReview, allowLintFail, allowNoSpecReview },
581
+ cwd: process.cwd(),
582
+ });
583
+
584
+ if (json) {
585
+ console.log(JSON.stringify(report, null, 2));
586
+ } else {
587
+ console.log(formatLifecycleDryRun(report));
588
+ }
589
+ if (report.exitCode && report.exitCode !== 0) {
590
+ process.exitCode = report.exitCode;
591
+ }
592
+ return;
534
593
  }
594
+
595
+ const err = new Error(
596
+ `lifecycle 子命令未知: ${sub}\n用法: lifecycle show [--json] · lifecycle dry-run --transition ID --from STATE`,
597
+ );
598
+ err.exitCode = 1;
599
+ throw err;
535
600
  }
536
601
 
537
602
  async function cmdSync(args) {
@@ -0,0 +1,96 @@
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
+ }