@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.
- package/CHANGELOG.md +42 -0
- package/README.md +65 -11
- package/bin/harness.js +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +135 -0
- package/docs/ONBOARDING.md +32 -0
- package/docs/README.md +2 -2
- package/docs/RELEASE_v1.0.0.md +91 -0
- package/docs/RELEASE_v1.0.1.md +87 -0
- package/docs/ROADMAP_TO_AGENT_GOVERNANCE.md +209 -0
- package/docs/USER_GUIDE_v1.0_zh.md +284 -0
- package/docs/methodology/AUDIT_doc_consistency_2026-06-15_zh.md +5 -3
- package/docs/methodology/README.md +7 -6
- package/docs/methodology/ROADMAP_v1_zh.md +45 -12
- package/docs/methodology/execution/PILOT_EVIDENCE_B2_v1_zh.md +45 -37
- package/docs/methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md +25 -23
- package/docs/methodology/graph/HARNESS_GRAPH_MODEL_dialogue_archive_v1_zh.md +2 -2
- package/docs/methodology/graph/HGM_UPGRADE_OUTLINE_v1_zh.md +248 -0
- package/docs/methodology/graph/README.md +2 -2
- package/docs/methodology/product/DESIGN_ONTOLOGY_v1_zh.md +2 -2
- package/docs/methodology/prompts/PROMPT_article_theory_roundtable_v1_zh.md +3 -3
- package/docs/methodology/prompts/PROMPT_doc_consistency_audit_v1_zh.md +4 -4
- package/examples/compliance_bench/README.md +81 -0
- package/examples/compliance_bench/S1_r1_pending/task.md +30 -0
- package/examples/compliance_bench/S2_r1_no_review/task.md +31 -0
- package/examples/compliance_bench/S3_r1_with_review/reviews/s3_r1_with_review_audit_R1_20260616.md +18 -0
- package/examples/compliance_bench/S3_r1_with_review/task.md +31 -0
- package/examples/compliance_bench/S4_sync_domain/profile.json +16 -0
- package/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +1 -1
- package/harness/templates/QUICKREF_v1_zh.md +47 -0
- package/ide/adapters/AGENTS.md.fragment.example +1 -0
- package/ide/adapters/CLAUDE.md.fragment.example +1 -0
- package/ide/adapters/cursor-harness-starter.mdc.example +1 -0
- package/lib/audit.js +166 -0
- package/lib/cli.js +245 -5
- package/lib/package-scripts.js +65 -0
- package/lib/paths.js +27 -0
- package/lib/verify.js +103 -0
- package/ontology.yaml +2 -2
- package/package.json +1 -1
- package/schema/invoke_index.v1.schema.json +53 -0
- package/wizard/README.md +4 -0
- package/wizard/compliance-bench.sh +251 -0
- package/wizard/gate-check.sh +139 -19
- package/wizard/harness-sync.sh +16 -3
- package/wizard/install.sh +5 -3
- package/wizard/lib/generate-invoke-index.js +60 -0
- 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 (
|
|
17
|
+
console.log(`@cyning/harness · cyning-harness CLI (v1.0.1)
|
|
13
18
|
|
|
14
19
|
用法:
|
|
15
|
-
npx @cyning/harness
|
|
16
|
-
npx @cyning/harness
|
|
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
|
-
|
|
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) =>
|
|
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
|
+
}
|