@cyning/harness 0.4.0 → 1.0.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +65 -11
  3. package/bin/harness.js +1 -1
  4. package/docs/ARCHITECTURE.md +1 -1
  5. package/docs/BENCHMARK_REPORT_TEMPLATE.md +135 -0
  6. package/docs/ONBOARDING.md +32 -0
  7. package/docs/README.md +2 -2
  8. package/docs/RELEASE_v1.0.0.md +91 -0
  9. package/docs/RELEASE_v1.0.1.md +87 -0
  10. package/docs/ROADMAP_TO_AGENT_GOVERNANCE.md +209 -0
  11. package/docs/USER_GUIDE_v1.0_zh.md +284 -0
  12. package/docs/methodology/AUDIT_doc_consistency_2026-06-15_zh.md +5 -3
  13. package/docs/methodology/README.md +7 -6
  14. package/docs/methodology/ROADMAP_v1_zh.md +45 -12
  15. package/docs/methodology/execution/PILOT_EVIDENCE_B2_v1_zh.md +45 -37
  16. package/docs/methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md +25 -23
  17. package/docs/methodology/graph/HARNESS_GRAPH_MODEL_dialogue_archive_v1_zh.md +2 -2
  18. package/docs/methodology/graph/HGM_UPGRADE_OUTLINE_v1_zh.md +248 -0
  19. package/docs/methodology/graph/README.md +2 -2
  20. package/docs/methodology/product/DESIGN_ONTOLOGY_v1_zh.md +2 -2
  21. package/docs/methodology/prompts/PROMPT_article_theory_roundtable_v1_zh.md +3 -3
  22. package/docs/methodology/prompts/PROMPT_doc_consistency_audit_v1_zh.md +4 -4
  23. package/examples/compliance_bench/README.md +81 -0
  24. package/examples/compliance_bench/S1_r1_pending/task.md +30 -0
  25. package/examples/compliance_bench/S2_r1_no_review/task.md +31 -0
  26. package/examples/compliance_bench/S3_r1_with_review/reviews/s3_r1_with_review_audit_R1_20260616.md +18 -0
  27. package/examples/compliance_bench/S3_r1_with_review/task.md +31 -0
  28. package/examples/compliance_bench/S4_sync_domain/profile.json +16 -0
  29. package/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +1 -1
  30. package/harness/templates/QUICKREF_v1_zh.md +47 -0
  31. package/ide/adapters/AGENTS.md.fragment.example +1 -0
  32. package/ide/adapters/CLAUDE.md.fragment.example +1 -0
  33. package/ide/adapters/cursor-harness-starter.mdc.example +1 -0
  34. package/lib/audit.js +166 -0
  35. package/lib/cli.js +245 -5
  36. package/lib/package-scripts.js +65 -0
  37. package/lib/paths.js +27 -0
  38. package/lib/verify.js +103 -0
  39. package/ontology.yaml +2 -2
  40. package/package.json +1 -1
  41. package/schema/invoke_index.v1.schema.json +53 -0
  42. package/wizard/README.md +4 -0
  43. package/wizard/compliance-bench.sh +251 -0
  44. package/wizard/gate-check.sh +139 -19
  45. package/wizard/harness-sync.sh +16 -3
  46. package/wizard/install.sh +5 -3
  47. package/wizard/lib/generate-invoke-index.js +60 -0
  48. package/wizard/upgrade.sh +7 -2
package/lib/audit.js ADDED
@@ -0,0 +1,166 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolveHarnessRootForTarget } from './paths.js';
5
+ import { loadTaskSidecar, validateTaskSidecar } from './task.js';
6
+
7
+ /**
8
+ * 在目标仓运行 ICVO audit(D3/D5/S5 子集)。
9
+ */
10
+ export function auditTarget(target, options = {}) {
11
+ const taskFile = options.taskFile;
12
+
13
+ const gate = runGateCheck(target, taskFile);
14
+ const testCheck = runTestCheck(target, taskFile);
15
+ const gitCheck = runGitCleanCheck(target);
16
+
17
+ const ok = gate.ok && testCheck.ok;
18
+
19
+ return {
20
+ ok,
21
+ target,
22
+ taskFile,
23
+ gate,
24
+ test: testCheck,
25
+ git: gitCheck,
26
+ };
27
+ }
28
+
29
+ function runGateCheck(target, taskFile) {
30
+ const harnessRoot = resolveHarnessRootForTarget(target);
31
+ const script = path.join(harnessRoot, 'wizard', 'gate-check.sh');
32
+ const args = [script, '--target', target];
33
+ if (taskFile) args.push('--task', taskFile);
34
+
35
+ const result = spawnSync('bash', args, {
36
+ encoding: 'utf8',
37
+ env: { ...process.env, CYNING_HARNESS: harnessRoot },
38
+ });
39
+
40
+ return {
41
+ ok: result.status === 0,
42
+ status: result.status,
43
+ stdout: result.stdout,
44
+ stderr: result.stderr,
45
+ };
46
+ }
47
+
48
+ function runTestCheck(target, taskFile) {
49
+ // 未指定 task 文件时,D5 跳过(在 active task 上跑 gate-check 已覆盖 D3)
50
+ if (!taskFile) {
51
+ return { ok: true, skipped: true, reason: '未指定 --task,跳过 D5' };
52
+ }
53
+
54
+ const absTask = path.isAbsolute(taskFile) ? taskFile : path.join(target, taskFile);
55
+ if (!fs.existsSync(absTask)) {
56
+ return { ok: true, skipped: true, reason: 'task 文件不存在,跳过 D5' };
57
+ }
58
+
59
+ const sidecar = resolveTaskSidecar(absTask, target);
60
+ if (!sidecar) {
61
+ return { ok: true, skipped: true, reason: '未找到 task sidecar,跳过 D5' };
62
+ }
63
+
64
+ let data;
65
+ try {
66
+ data = loadTaskSidecar(sidecar);
67
+ } catch (err) {
68
+ return { ok: false, reason: `sidecar 解析失败: ${err.message}` };
69
+ }
70
+
71
+ const validation = validateTaskSidecar(data, path.basename(sidecar));
72
+ if (!validation.ok) {
73
+ return { ok: false, reason: validation.errors.join('; ') };
74
+ }
75
+
76
+ if (data.test_strategy !== 'required') {
77
+ return {
78
+ ok: true,
79
+ skipped: true,
80
+ reason: `test_strategy=${data.test_strategy},无需 D5 强检查`,
81
+ };
82
+ }
83
+
84
+ const hasTest = hasTestArtifacts(target);
85
+ if (!hasTest) {
86
+ return {
87
+ ok: false,
88
+ reason: 'test_strategy=required 但目标仓未声明测试路径或 CI 引用',
89
+ };
90
+ }
91
+
92
+ return { ok: true, reason: 'test_strategy=required 且检测到测试/CI 制品' };
93
+ }
94
+
95
+ function resolveTaskSidecar(taskMarkdownPath, target) {
96
+ const dir = path.dirname(taskMarkdownPath);
97
+ const base = path.basename(taskMarkdownPath, '.md');
98
+
99
+ // 1. 同目录 <name>.harness.json
100
+ const sameDir = path.join(dir, `${base}.harness.json`);
101
+ if (fs.existsSync(sameDir)) return sameDir;
102
+
103
+ // 2. 目标仓根 task.harness.v1.json(legacy / 单 task 仓)
104
+ const rootSidecar = path.join(target, 'task.harness.v1.json');
105
+ if (fs.existsSync(rootSidecar)) return rootSidecar;
106
+
107
+ return null;
108
+ }
109
+
110
+ function hasTestArtifacts(target) {
111
+ const probes = [
112
+ 'test',
113
+ 'tests',
114
+ 'spec',
115
+ 'specs',
116
+ '__tests__',
117
+ 'jest.config.js',
118
+ 'jest.config.ts',
119
+ 'vitest.config.js',
120
+ 'vitest.config.ts',
121
+ 'playwright.config.js',
122
+ 'playwright.config.ts',
123
+ 'cypress.config.js',
124
+ 'pytest.ini',
125
+ 'pyproject.toml',
126
+ 'setup.py',
127
+ ];
128
+
129
+ for (const p of probes) {
130
+ const full = path.join(target, p);
131
+ if (fs.existsSync(full)) return true;
132
+ }
133
+
134
+ // 任意 *.test.js / *.spec.js / *.test.ts / *.spec.ts
135
+ const jsTests = spawnSync(
136
+ 'find',
137
+ [target, '-maxdepth', '3', '-type', 'f', '(', '-name', '*.test.js', '-o', '-name', '*.spec.js', '-o', '-name', '*.test.ts', '-o', '-name', '*.spec.ts', '-o', '-name', '*_test.py', '-o', '-name', 'test_*.py', ')'],
138
+ { encoding: 'utf8' },
139
+ );
140
+ if (jsTests.status === 0 && jsTests.stdout.trim()) return true;
141
+
142
+ // CI 工作流引用
143
+ const ciDir = path.join(target, '.github', 'workflows');
144
+ if (fs.existsSync(ciDir)) {
145
+ const files = fs.readdirSync(ciDir);
146
+ if (files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'))) return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ function runGitCleanCheck(target) {
153
+ const result = spawnSync('git', ['status', '--porcelain'], {
154
+ encoding: 'utf8',
155
+ cwd: target,
156
+ });
157
+
158
+ const dirty = result.status === 0 && result.stdout.trim().length > 0;
159
+
160
+ return {
161
+ ok: true, // audit 不因此失败,仅 warn
162
+ dirty,
163
+ stdout: result.stdout,
164
+ warning: dirty ? '工作区未 clean(S5):建议 commit 后再执行 apply' : null,
165
+ };
166
+ }
package/lib/cli.js CHANGED
@@ -1,26 +1,41 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
1
3
  import { readManifest, isNewerVersion } from './manifest.js';
2
4
  import {
3
5
  resolveHarnessRoot,
6
+ resolveHarnessRootForTarget,
4
7
  resolvePackageVersion,
5
8
  resolveTarget,
6
9
  wizardScript,
7
10
  } from './paths.js';
8
11
  import { runBash } from './run.js';
9
12
  import { checkTaskFile } from './task.js';
13
+ import { auditTarget } from './audit.js';
14
+ import { mergePackageScripts } from './package-scripts.js';
10
15
 
11
16
  function usage() {
12
- console.log(`@cyning/harness · cyning-harness CLI (v0.3+)
17
+ console.log(`@cyning/harness · cyning-harness CLI (v1.0.1)
13
18
 
14
19
  用法:
15
- npx @cyning/harness init [--preset NAME] [--ide LIST] [--target PATH] [--yes]
16
- npx @cyning/harness upgrade [--target PATH] [--ide LIST] [--yes] [--force] [--gate-check]
20
+ npx @cyning/harness --version | -V
21
+ npx @cyning/harness --help | -h
22
+ npx @cyning/harness init [--preset NAME] [--ide LIST] [--target PATH] [--yes] [--with-scripts] [--pm pnpm|npm|yarn|auto]
23
+ npx @cyning/harness upgrade [--target PATH] [--ide LIST] [--yes] [--force] [--gate-check] [--with-scripts] [--pm pnpm|npm|yarn|auto]
17
24
  npx @cyning/harness check [--target PATH]
25
+ npx @cyning/harness audit [--target PATH] [--task FILE]
26
+ npx @cyning/harness gate-check [--target PATH] [--task FILE] [--graph] [--json]
27
+ npx @cyning/harness verify [--target PATH] [--task FILE] [--graph]
28
+ npx @cyning/harness sync index [--target PATH]
18
29
  npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
19
30
 
20
31
  说明:
21
32
  init 首次接入 · 内部调用 wizard/install.sh + harness-sync.sh apply
22
33
  upgrade 升级已接入仓 · 等价 wizard/upgrade.sh --yes
23
34
  check 读 manifest.json · 对比当前包版本是否有可升级版本
35
+ audit ICVO 机械审计:复用 gate-check · D5 测试声明检查 · S5 git-clean 提醒
36
+ gate-check 调用 wizard/gate-check.sh(业务仓无需 clone 即可用)
37
+ verify 30 前聚合:gate-check + audit D5 + S5 warn + 可选 --graph
38
+ sync index 生成 .cyning-harness/invoke_index.json
24
39
  task check 校验 task.harness.v1.json sidecar · --no-circular 检测 depends_on 环
25
40
 
26
41
  环境:
@@ -48,6 +63,11 @@ export async function runCli(argv) {
48
63
  return;
49
64
  }
50
65
 
66
+ if (argv.includes('--version') || argv.includes('-V')) {
67
+ console.log(pkgVersion);
68
+ return;
69
+ }
70
+
51
71
  const [cmd, ...rest] = argv;
52
72
 
53
73
  if (cmd === 'init') {
@@ -62,6 +82,22 @@ export async function runCli(argv) {
62
82
  await cmdCheck(rest, harnessRoot, pkgVersion);
63
83
  return;
64
84
  }
85
+ if (cmd === 'audit') {
86
+ await cmdAudit(rest);
87
+ return;
88
+ }
89
+ if (cmd === 'gate-check') {
90
+ await cmdGateCheck(rest);
91
+ return;
92
+ }
93
+ if (cmd === 'verify') {
94
+ await cmdVerify(rest);
95
+ return;
96
+ }
97
+ if (cmd === 'sync') {
98
+ await cmdSync(rest);
99
+ return;
100
+ }
65
101
  if (cmd === 'task') {
66
102
  await cmdTask(rest);
67
103
  return;
@@ -74,13 +110,16 @@ export async function runCli(argv) {
74
110
 
75
111
  async function cmdInit(args, harnessRoot, pkgVersion) {
76
112
  const yes = args.includes('--yes');
77
- let rest = args.filter((a) => a !== '--yes');
113
+ const withScripts = args.includes('--with-scripts');
114
+ let rest = args.filter((a) => a !== '--yes' && a !== '--with-scripts');
78
115
  const { value: preset, rest: r1 } = takeOption(rest, '--preset');
79
116
  rest = r1;
80
117
  const { value: ide, rest: r2 } = takeOption(rest, '--ide');
81
118
  rest = r2;
82
119
  const { value: targetArg, rest: r3 } = takeOption(rest, '--target');
83
120
  rest = r3;
121
+ const { value: pm, rest: r4 } = takeOption(rest, '--pm');
122
+ rest = r4;
84
123
 
85
124
  if (rest.length > 0) {
86
125
  const err = new Error(`init 未知参数: ${rest.join(' ')}`);
@@ -102,6 +141,18 @@ async function cmdInit(args, harnessRoot, pkgVersion) {
102
141
  inherit: true,
103
142
  });
104
143
 
144
+ if (withScripts) {
145
+ const ps = mergePackageScripts(target, pkgVersion, pm || 'auto');
146
+ if (ps.skipped) {
147
+ console.log(`warn: ${ps.reason}`);
148
+ } else if (ps.changed) {
149
+ console.log(`scripts: ${ps.reason}(pm: ${ps.pm})`);
150
+ console.log('提示: 运行 `pnpm install` 或对应 install 使 devDep 生效');
151
+ } else {
152
+ console.log(`scripts: ${ps.reason}`);
153
+ }
154
+ }
155
+
105
156
  console.log(`manifest: ${target}/.cyning-harness/manifest.json`);
106
157
  if (!yes) {
107
158
  console.log('init 完成。');
@@ -113,13 +164,23 @@ async function cmdUpgrade(args, harnessRoot, pkgVersion) {
113
164
  const force = args.includes('--force');
114
165
  const gateCheck = args.includes('--gate-check');
115
166
  const yes = args.includes('--yes') || !checkOnly;
167
+ const withScripts = args.includes('--with-scripts');
116
168
  let rest = args.filter(
117
- (a) => !['--check', '--force', '--gate-check', '--yes'].includes(a),
169
+ (a) =>
170
+ ![
171
+ '--check',
172
+ '--force',
173
+ '--gate-check',
174
+ '--yes',
175
+ '--with-scripts',
176
+ ].includes(a),
118
177
  );
119
178
  const { value: ide, rest: r1 } = takeOption(rest, '--ide');
120
179
  rest = r1;
121
180
  const { value: targetArg, rest: r2 } = takeOption(rest, '--target');
122
181
  rest = r2;
182
+ const { value: pm, rest: r3 } = takeOption(rest, '--pm');
183
+ rest = r3;
123
184
 
124
185
  if (rest.length > 0) {
125
186
  const err = new Error(`upgrade 未知参数: ${rest.join(' ')}`);
@@ -149,6 +210,18 @@ async function cmdUpgrade(args, harnessRoot, pkgVersion) {
149
210
  },
150
211
  inherit: true,
151
212
  });
213
+
214
+ if (withScripts) {
215
+ const ps = mergePackageScripts(target, pkgVersion, pm || 'auto');
216
+ if (ps.skipped) {
217
+ console.log(`warn: ${ps.reason}`);
218
+ } else if (ps.changed) {
219
+ console.log(`scripts: ${ps.reason}(pm: ${ps.pm})`);
220
+ console.log('提示: 运行 `pnpm install` 或对应 install 使 devDep 生效');
221
+ } else {
222
+ console.log(`scripts: ${ps.reason}`);
223
+ }
224
+ }
152
225
  }
153
226
 
154
227
  async function cmdCheck(args, harnessRoot, pkgVersion) {
@@ -186,6 +259,173 @@ async function cmdCheck(args, harnessRoot, pkgVersion) {
186
259
  }
187
260
  }
188
261
 
262
+ async function cmdAudit(args) {
263
+ let rest = args;
264
+ const { value: targetArg, rest: r1 } = takeOption(rest, '--target');
265
+ rest = r1;
266
+ const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
267
+ rest = r2;
268
+
269
+ if (rest.length > 0) {
270
+ const err = new Error(`audit 未知参数: ${rest.join(' ')}`);
271
+ err.exitCode = 1;
272
+ throw err;
273
+ }
274
+
275
+ const target = resolveTarget(process.cwd(), targetArg);
276
+ const result = auditTarget(target, { taskFile });
277
+
278
+ console.log(`目标: ${target}`);
279
+ if (taskFile) console.log(`task: ${taskFile}`);
280
+ console.log(`audit: ${result.ok ? 'PASS' : 'FAIL'}`);
281
+
282
+ // D3 gate-check
283
+ console.log(` gate-check: ${result.gate.ok ? 'PASS' : 'FAIL'}`);
284
+ if (!result.gate.ok && result.gate.stdout) {
285
+ for (const line of result.gate.stdout.split('\n')) {
286
+ if (line.trim()) console.log(` ${line}`);
287
+ }
288
+ }
289
+
290
+ // D5 test
291
+ console.log(` test-check: ${result.test.ok ? 'PASS' : 'FAIL'}`);
292
+ if (result.test.reason) console.log(` ${result.test.reason}`);
293
+
294
+ // S5 git-clean
295
+ console.log(` git-clean: ${result.git.dirty ? 'DIRTY' : 'CLEAN'}`);
296
+ if (result.git.warning) console.log(` ${result.git.warning}`);
297
+
298
+ if (!result.ok) {
299
+ const err = new Error('ICVO audit 未通过');
300
+ err.exitCode = 2;
301
+ throw err;
302
+ }
303
+ }
304
+
305
+ async function cmdGateCheck(args) {
306
+ if (args.includes('--help') || args.includes('-h')) {
307
+ console.log(`用法: npx @cyning/harness gate-check [--target PATH] [--task FILE] [--graph] [--json]`);
308
+ return;
309
+ }
310
+
311
+ let rest = args;
312
+ const { value: targetArg, rest: r1 } = takeOption(rest, '--target');
313
+ rest = r1;
314
+ const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
315
+ rest = r2;
316
+ const graph = rest.includes('--graph');
317
+ const json = rest.includes('--json');
318
+ rest = rest.filter((a) => a !== '--graph' && a !== '--json');
319
+
320
+ if (rest.length > 0) {
321
+ const err = new Error(`gate-check 未知参数: ${rest.join(' ')}`);
322
+ err.exitCode = 1;
323
+ throw err;
324
+ }
325
+
326
+ const target = resolveTarget(process.cwd(), targetArg);
327
+ const harnessRoot = resolveHarnessRootForTarget(target);
328
+ const script = wizardScript(harnessRoot, 'gate-check.sh');
329
+ const childArgs = ['--target', target];
330
+ if (taskFile) childArgs.push('--task', taskFile);
331
+ if (graph) childArgs.push('--graph');
332
+ if (json) childArgs.push('--json');
333
+
334
+ const result = spawnSync('bash', [script, ...childArgs], {
335
+ stdio: 'inherit',
336
+ env: { ...process.env, CYNING_HARNESS: harnessRoot },
337
+ });
338
+
339
+ const exitCode = result.status ?? 1;
340
+ if (exitCode !== 0) {
341
+ const err = new Error('');
342
+ err.exitCode = exitCode;
343
+ throw err;
344
+ }
345
+ }
346
+
347
+ async function cmdVerify(args) {
348
+ // D3 · implemented in lib/verify.js
349
+ const { verifyTarget } = await import('./verify.js');
350
+ if (args.includes('--help') || args.includes('-h')) {
351
+ console.log(`用法: npx @cyning/harness verify [--target PATH] [--task FILE] [--graph]`);
352
+ return;
353
+ }
354
+
355
+ let rest = args;
356
+ const { value: targetArg, rest: r1 } = takeOption(rest, '--target');
357
+ rest = r1;
358
+ const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
359
+ rest = r2;
360
+ const graph = rest.includes('--graph');
361
+ rest = rest.filter((a) => a !== '--graph');
362
+
363
+ if (rest.length > 0) {
364
+ const err = new Error(`verify 未知参数: ${rest.join(' ')}`);
365
+ err.exitCode = 1;
366
+ throw err;
367
+ }
368
+
369
+ const target = resolveTarget(process.cwd(), targetArg);
370
+ const result = verifyTarget(target, { taskFile, graph });
371
+
372
+ // forward gate-check/audit/graph stdout if any
373
+ if (result.stdout) {
374
+ process.stdout.write(result.stdout);
375
+ }
376
+
377
+ const summary = taskFile
378
+ ? `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''} · ${path.basename(taskFile)}`
379
+ : `VERIFY: ${result.ok ? 'PASS' : 'BLOCKED'}${result.reason ? ` · ${result.reason}` : ''}`;
380
+ console.log(summary);
381
+
382
+ if (!result.ok) {
383
+ const err = new Error('');
384
+ err.exitCode = result.exitCode ?? 2;
385
+ throw err;
386
+ }
387
+ }
388
+
389
+ async function cmdSync(args) {
390
+ if (args.includes('--help') || args.includes('-h')) {
391
+ console.log(`用法: npx @cyning/harness sync index [--target PATH]`);
392
+ return;
393
+ }
394
+
395
+ const [sub, ...rest] = args;
396
+ if (sub !== 'index') {
397
+ const err = new Error(`sync 子命令未知: ${sub ?? '(空)'}\n用法: sync index [--target PATH]`);
398
+ err.exitCode = 1;
399
+ throw err;
400
+ }
401
+
402
+ let remaining = rest;
403
+ const { value: targetArg, rest: r1 } = takeOption(remaining, '--target');
404
+ remaining = r1;
405
+
406
+ if (remaining.length > 0) {
407
+ const err = new Error(`sync index 未知参数: ${remaining.join(' ')}`);
408
+ err.exitCode = 1;
409
+ throw err;
410
+ }
411
+
412
+ const target = resolveTarget(process.cwd(), targetArg);
413
+ const harnessRoot = resolveHarnessRootForTarget(target);
414
+ const script = wizardScript(harnessRoot, 'harness-sync.sh');
415
+
416
+ const result = spawnSync('bash', [script, '--index', '--target', target], {
417
+ stdio: 'inherit',
418
+ env: { ...process.env, CYNING_HARNESS: harnessRoot },
419
+ });
420
+
421
+ const exitCode = result.status ?? 1;
422
+ if (exitCode !== 0) {
423
+ const err = new Error('');
424
+ err.exitCode = exitCode;
425
+ throw err;
426
+ }
427
+ }
428
+
189
429
  async function cmdTask(args) {
190
430
  const [sub, ...rest] = args;
191
431
  if (sub !== 'check') {
@@ -0,0 +1,65 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_SCRIPTS = {
5
+ 'harness:verify': 'harness verify',
6
+ 'harness:gate': 'harness gate-check',
7
+ 'harness:audit': 'harness audit',
8
+ };
9
+
10
+ function detectPackageManager(target) {
11
+ if (fs.existsSync(path.join(target, 'pnpm-lock.yaml'))) return 'pnpm';
12
+ if (fs.existsSync(path.join(target, 'yarn.lock'))) return 'yarn';
13
+ if (fs.existsSync(path.join(target, 'package-lock.json'))) return 'npm';
14
+ return 'npm';
15
+ }
16
+
17
+ /**
18
+ * 在 target 仓的 package.json 中 merge devDep + scripts(不覆盖已有 key)。
19
+ * @returns {{ok: boolean, skipped?: boolean, reason?: string, changed: boolean}}
20
+ */
21
+ export function mergePackageScripts(target, version, pm = 'auto') {
22
+ const pkgPath = path.join(target, 'package.json');
23
+ if (!fs.existsSync(pkgPath)) {
24
+ return {
25
+ ok: true,
26
+ skipped: true,
27
+ reason: '未找到 package.json,跳过 --with-scripts',
28
+ changed: false,
29
+ };
30
+ }
31
+
32
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
33
+ const original = JSON.stringify(pkg);
34
+
35
+ if (!pkg.devDependencies) pkg.devDependencies = {};
36
+ if (pkg.devDependencies['@cyning/harness'] !== version) {
37
+ pkg.devDependencies['@cyning/harness'] = version;
38
+ }
39
+
40
+ if (!pkg.scripts) pkg.scripts = {};
41
+ for (const [key, value] of Object.entries(DEFAULT_SCRIPTS)) {
42
+ if (!Object.prototype.hasOwnProperty.call(pkg.scripts, key)) {
43
+ pkg.scripts[key] = value;
44
+ }
45
+ }
46
+
47
+ if (JSON.stringify(pkg) !== original) {
48
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
49
+ return {
50
+ ok: true,
51
+ skipped: false,
52
+ changed: true,
53
+ pm: pm === 'auto' ? detectPackageManager(target) : pm,
54
+ reason: `已写入 @cyning/harness@${version} 与 scripts`,
55
+ };
56
+ }
57
+
58
+ return {
59
+ ok: true,
60
+ skipped: false,
61
+ changed: false,
62
+ pm: pm === 'auto' ? detectPackageManager(target) : pm,
63
+ reason: 'package.json 已包含所需 devDep/scripts',
64
+ };
65
+ }
package/lib/paths.js CHANGED
@@ -12,6 +12,33 @@ export function resolveHarnessRoot() {
12
12
  return path.resolve(__dirname, '..');
13
13
  }
14
14
 
15
+ /**
16
+ * 对指定业务仓 target 解析可用产品包根。
17
+ * 顺序:env CYNING_HARNESS → target/.cyning-harness/local.json → npm 包根
18
+ */
19
+ export function resolveHarnessRootForTarget(target) {
20
+ if (process.env.CYNING_HARNESS) {
21
+ return path.resolve(process.env.CYNING_HARNESS);
22
+ }
23
+
24
+ const localJson = path.join(target, '.cyning-harness', 'local.json');
25
+ if (fs.existsSync(localJson)) {
26
+ try {
27
+ const data = JSON.parse(fs.readFileSync(localJson, 'utf8'));
28
+ if (data.cyning_harness_root) {
29
+ const candidate = path.resolve(target, data.cyning_harness_root);
30
+ if (fs.existsSync(path.join(candidate, 'wizard', 'gate-check.sh'))) {
31
+ return candidate;
32
+ }
33
+ }
34
+ } catch {
35
+ // fall through
36
+ }
37
+ }
38
+
39
+ return path.resolve(__dirname, '..');
40
+ }
41
+
15
42
  export function resolvePackageVersion(harnessRoot) {
16
43
  if (process.env.HARNESS_VERSION) {
17
44
  return process.env.HARNESS_VERSION;
package/lib/verify.js ADDED
@@ -0,0 +1,103 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolveHarnessRootForTarget, wizardScript } from './paths.js';
5
+ import { auditTarget } from './audit.js';
6
+
7
+ /**
8
+ * 30 前聚合验证:gate-check + audit D5(仅 --task)+ S5 git-clean warn + 可选 --graph
9
+ */
10
+ export function verifyTarget(target, options = {}) {
11
+ const { taskFile, graph } = options;
12
+ const harnessRoot = resolveHarnessRootForTarget(target);
13
+
14
+ // 1. gate-check(含 --graph)
15
+ const gateResult = runGateCheck(target, taskFile, graph, harnessRoot);
16
+ if (!gateResult.ok) {
17
+ return {
18
+ ok: false,
19
+ exitCode: 2,
20
+ reason: gateResult.reason,
21
+ stdout: gateResult.stdout,
22
+ };
23
+ }
24
+
25
+ // 2. audit D5(仅当指定 task)
26
+ if (taskFile) {
27
+ const audit = auditTarget(target, { taskFile });
28
+ if (!audit.ok) {
29
+ return {
30
+ ok: false,
31
+ exitCode: 2,
32
+ reason: audit.test.ok ? 'gate-check 或 audit 未通过' : audit.test.reason,
33
+ stdout: gateResult.stdout,
34
+ };
35
+ }
36
+ }
37
+
38
+ // 3. S5 git-clean:warn 不挡
39
+ const gitResult = runGitCleanCheck(target);
40
+ if (gitResult.dirty) {
41
+ // 把 warn 拼到 stdout 里让 CLI 透传
42
+ const warn = `WARN: 工作区未 clean(S5):建议 commit 后再执行 apply\n`;
43
+ gateResult.stdout += warn;
44
+ }
45
+
46
+ return {
47
+ ok: true,
48
+ exitCode: 0,
49
+ stdout: gateResult.stdout,
50
+ };
51
+ }
52
+
53
+ function runGateCheck(target, taskFile, graph, harnessRoot) {
54
+ const script = wizardScript(harnessRoot, 'gate-check.sh');
55
+ const args = [script, '--target', target];
56
+ if (taskFile) args.push('--task', taskFile);
57
+ if (graph) args.push('--graph');
58
+
59
+ const result = spawnSync('bash', args, {
60
+ encoding: 'utf8',
61
+ env: { ...process.env, CYNING_HARNESS: harnessRoot },
62
+ });
63
+
64
+ if (result.status !== 0) {
65
+ return {
66
+ ok: false,
67
+ status: result.status,
68
+ stdout: result.stdout,
69
+ reason: extractBlockReason(result.stdout, taskFile),
70
+ };
71
+ }
72
+
73
+ return {
74
+ ok: true,
75
+ status: 0,
76
+ stdout: result.stdout,
77
+ };
78
+ }
79
+
80
+ function extractBlockReason(stdout) {
81
+ const lines = stdout.split('\n');
82
+ for (const line of lines) {
83
+ if (line.includes('→ 30 不可开工')) {
84
+ return line.trim().replace(/^→\s*/, '');
85
+ }
86
+ }
87
+ for (const line of lines) {
88
+ if (line.includes('❌ 拒 30')) {
89
+ return line.trim();
90
+ }
91
+ }
92
+ return 'gate-check blocked';
93
+ }
94
+
95
+ function runGitCleanCheck(target) {
96
+ const result = spawnSync('git', ['status', '--porcelain'], {
97
+ encoding: 'utf8',
98
+ cwd: target,
99
+ });
100
+
101
+ const dirty = result.status === 0 && result.stdout.trim().length > 0;
102
+ return { dirty };
103
+ }