@cyning/harness 0.4.0 → 1.0.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +50 -0
  3. package/docs/ARCHITECTURE.md +1 -1
  4. package/docs/BENCHMARK_REPORT_TEMPLATE.md +135 -0
  5. package/docs/ONBOARDING.md +29 -0
  6. package/docs/RELEASE_v1.0.0.md +90 -0
  7. package/docs/ROADMAP_TO_AGENT_GOVERNANCE.md +209 -0
  8. package/docs/methodology/AUDIT_doc_consistency_2026-06-15_zh.md +5 -3
  9. package/docs/methodology/README.md +7 -6
  10. package/docs/methodology/ROADMAP_v1_zh.md +23 -11
  11. package/docs/methodology/execution/PILOT_EVIDENCE_B2_v1_zh.md +45 -37
  12. package/docs/methodology/graph/HARNESS_GRAPH_MODEL_design_v0_zh.md +24 -23
  13. package/docs/methodology/graph/HARNESS_GRAPH_MODEL_dialogue_archive_v1_zh.md +2 -2
  14. package/docs/methodology/graph/README.md +2 -2
  15. package/docs/methodology/product/DESIGN_ONTOLOGY_v1_zh.md +2 -2
  16. package/docs/methodology/prompts/PROMPT_article_theory_roundtable_v1_zh.md +3 -3
  17. package/docs/methodology/prompts/PROMPT_doc_consistency_audit_v1_zh.md +4 -4
  18. package/examples/compliance_bench/README.md +28 -0
  19. package/examples/compliance_bench/S1_r1_pending/task.md +30 -0
  20. package/examples/compliance_bench/S2_r1_no_review/task.md +31 -0
  21. package/examples/compliance_bench/S3_r1_with_review/reviews/s3_r1_with_review_audit_R1_20260616.md +18 -0
  22. package/examples/compliance_bench/S3_r1_with_review/task.md +31 -0
  23. package/examples/compliance_bench/S4_sync_domain/profile.json +16 -0
  24. package/lib/audit.js +168 -0
  25. package/lib/cli.js +50 -0
  26. package/ontology.yaml +2 -2
  27. package/package.json +1 -1
  28. package/schema/invoke_index.v1.schema.json +53 -0
  29. package/wizard/compliance-bench.sh +156 -0
  30. package/wizard/gate-check.sh +139 -19
  31. package/wizard/harness-sync.sh +16 -3
  32. package/wizard/lib/generate-invoke-index.js +60 -0
@@ -0,0 +1,31 @@
1
+ # Task fixture · S3 · R1 approved + review 落盘
2
+
3
+ > 用途:SDD-Compliance micro-bench S3 夹具 · R1 approved 且 reviews/*_R1_* 存在 · gate-check exit=0
4
+
5
+ ---
6
+
7
+ ## Harness 元信息
8
+
9
+ | 字段 | 值 |
10
+ | --- | --- |
11
+ | **task_slug** | `s3_r1_with_review` |
12
+ | **test_strategy** | `not_applicable` |
13
+ | **audit_profile** | `full` |
14
+
15
+ ### 人工闸
16
+
17
+ | human_gate_id | status | blocks_hats | 说明 |
18
+ | --- | --- | --- | --- |
19
+ | HG-TASK-DRAFT | approved | 22, 30 | 10 完成 |
20
+ | HG-AUDIT-R1 | approved | 30 | 22 R1 已签 |
21
+
22
+ ---
23
+
24
+ ## 背景与目标
25
+
26
+ 验证 D2/D3 公理:**R1 approved + review 文件落盘 → 30 可开工**。
27
+
28
+ ## 验收标准
29
+
30
+ - `gate-check.sh --task task.md` **exit 0**
31
+ - bench 脚本检查 `reviews/s3_r1_with_review_audit_R1_20260616.md` 存在 → **合规**
@@ -0,0 +1,16 @@
1
+ {
2
+ "preset": "harness-only",
3
+ "tracks": {
4
+ "harness_prompts": true,
5
+ "harness_invoke_template": true,
6
+ "ide_cursor": true,
7
+ "ide_claude": false,
8
+ "ide_agents": false,
9
+ "graph": false,
10
+ "wiki": false
11
+ },
12
+ "paths": {
13
+ "ide_cursor": ".cursor/rules/06-harness-pointer.mdc"
14
+ },
15
+ "ci": "none"
16
+ }
package/lib/audit.js ADDED
@@ -0,0 +1,168 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { loadTaskSidecar, validateTaskSidecar } from './task.js';
5
+
6
+ /**
7
+ * 在目标仓运行 ICVO audit(D3/D5/S5 子集)。
8
+ */
9
+ export function auditTarget(target, options = {}) {
10
+ const taskFile = options.taskFile;
11
+
12
+ const gate = runGateCheck(target, taskFile);
13
+ const testCheck = runTestCheck(target, taskFile);
14
+ const gitCheck = runGitCleanCheck(target);
15
+
16
+ const ok = gate.ok && testCheck.ok;
17
+
18
+ return {
19
+ ok,
20
+ target,
21
+ taskFile,
22
+ gate,
23
+ test: testCheck,
24
+ git: gitCheck,
25
+ };
26
+ }
27
+
28
+ function runGateCheck(target, taskFile) {
29
+ const script = path.join(
30
+ process.env.CYNING_HARNESS || process.cwd(),
31
+ 'wizard',
32
+ 'gate-check.sh',
33
+ );
34
+ const args = [script, '--target', target];
35
+ if (taskFile) args.push('--task', taskFile);
36
+
37
+ const result = spawnSync('bash', args, {
38
+ encoding: 'utf8',
39
+ env: process.env,
40
+ });
41
+
42
+ return {
43
+ ok: result.status === 0,
44
+ status: result.status,
45
+ stdout: result.stdout,
46
+ stderr: result.stderr,
47
+ };
48
+ }
49
+
50
+ function runTestCheck(target, taskFile) {
51
+ // 未指定 task 文件时,D5 跳过(在 active task 上跑 gate-check 已覆盖 D3)
52
+ if (!taskFile) {
53
+ return { ok: true, skipped: true, reason: '未指定 --task,跳过 D5' };
54
+ }
55
+
56
+ const absTask = path.isAbsolute(taskFile) ? taskFile : path.join(target, taskFile);
57
+ if (!fs.existsSync(absTask)) {
58
+ return { ok: true, skipped: true, reason: 'task 文件不存在,跳过 D5' };
59
+ }
60
+
61
+ const sidecar = resolveTaskSidecar(absTask, target);
62
+ if (!sidecar) {
63
+ return { ok: true, skipped: true, reason: '未找到 task sidecar,跳过 D5' };
64
+ }
65
+
66
+ let data;
67
+ try {
68
+ data = loadTaskSidecar(sidecar);
69
+ } catch (err) {
70
+ return { ok: false, reason: `sidecar 解析失败: ${err.message}` };
71
+ }
72
+
73
+ const validation = validateTaskSidecar(data, path.basename(sidecar));
74
+ if (!validation.ok) {
75
+ return { ok: false, reason: validation.errors.join('; ') };
76
+ }
77
+
78
+ if (data.test_strategy !== 'required') {
79
+ return {
80
+ ok: true,
81
+ skipped: true,
82
+ reason: `test_strategy=${data.test_strategy},无需 D5 强检查`,
83
+ };
84
+ }
85
+
86
+ const hasTest = hasTestArtifacts(target);
87
+ if (!hasTest) {
88
+ return {
89
+ ok: false,
90
+ reason: 'test_strategy=required 但目标仓未声明测试路径或 CI 引用',
91
+ };
92
+ }
93
+
94
+ return { ok: true, reason: 'test_strategy=required 且检测到测试/CI 制品' };
95
+ }
96
+
97
+ function resolveTaskSidecar(taskMarkdownPath, target) {
98
+ const dir = path.dirname(taskMarkdownPath);
99
+ const base = path.basename(taskMarkdownPath, '.md');
100
+
101
+ // 1. 同目录 <name>.harness.json
102
+ const sameDir = path.join(dir, `${base}.harness.json`);
103
+ if (fs.existsSync(sameDir)) return sameDir;
104
+
105
+ // 2. 目标仓根 task.harness.v1.json(legacy / 单 task 仓)
106
+ const rootSidecar = path.join(target, 'task.harness.v1.json');
107
+ if (fs.existsSync(rootSidecar)) return rootSidecar;
108
+
109
+ return null;
110
+ }
111
+
112
+ function hasTestArtifacts(target) {
113
+ const probes = [
114
+ 'test',
115
+ 'tests',
116
+ 'spec',
117
+ 'specs',
118
+ '__tests__',
119
+ 'jest.config.js',
120
+ 'jest.config.ts',
121
+ 'vitest.config.js',
122
+ 'vitest.config.ts',
123
+ 'playwright.config.js',
124
+ 'playwright.config.ts',
125
+ 'cypress.config.js',
126
+ 'pytest.ini',
127
+ 'pyproject.toml',
128
+ 'setup.py',
129
+ ];
130
+
131
+ for (const p of probes) {
132
+ const full = path.join(target, p);
133
+ if (fs.existsSync(full)) return true;
134
+ }
135
+
136
+ // 任意 *.test.js / *.spec.js / *.test.ts / *.spec.ts
137
+ const jsTests = spawnSync(
138
+ 'find',
139
+ [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', ')'],
140
+ { encoding: 'utf8' },
141
+ );
142
+ if (jsTests.status === 0 && jsTests.stdout.trim()) return true;
143
+
144
+ // CI 工作流引用
145
+ const ciDir = path.join(target, '.github', 'workflows');
146
+ if (fs.existsSync(ciDir)) {
147
+ const files = fs.readdirSync(ciDir);
148
+ if (files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'))) return true;
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ function runGitCleanCheck(target) {
155
+ const result = spawnSync('git', ['status', '--porcelain'], {
156
+ encoding: 'utf8',
157
+ cwd: target,
158
+ });
159
+
160
+ const dirty = result.status === 0 && result.stdout.trim().length > 0;
161
+
162
+ return {
163
+ ok: true, // audit 不因此失败,仅 warn
164
+ dirty,
165
+ stdout: result.stdout,
166
+ warning: dirty ? '工作区未 clean(S5):建议 commit 后再执行 apply' : null,
167
+ };
168
+ }
package/lib/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './paths.js';
8
8
  import { runBash } from './run.js';
9
9
  import { checkTaskFile } from './task.js';
10
+ import { auditTarget } from './audit.js';
10
11
 
11
12
  function usage() {
12
13
  console.log(`@cyning/harness · cyning-harness CLI (v0.3+)
@@ -15,12 +16,14 @@ function usage() {
15
16
  npx @cyning/harness init [--preset NAME] [--ide LIST] [--target PATH] [--yes]
16
17
  npx @cyning/harness upgrade [--target PATH] [--ide LIST] [--yes] [--force] [--gate-check]
17
18
  npx @cyning/harness check [--target PATH]
19
+ npx @cyning/harness audit [--target PATH] [--task FILE]
18
20
  npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
19
21
 
20
22
  说明:
21
23
  init 首次接入 · 内部调用 wizard/install.sh + harness-sync.sh apply
22
24
  upgrade 升级已接入仓 · 等价 wizard/upgrade.sh --yes
23
25
  check 读 manifest.json · 对比当前包版本是否有可升级版本
26
+ audit ICVO 机械审计:复用 gate-check · D5 测试声明检查 · S5 git-clean 提醒
24
27
  task check 校验 task.harness.v1.json sidecar · --no-circular 检测 depends_on 环
25
28
 
26
29
  环境:
@@ -62,6 +65,10 @@ export async function runCli(argv) {
62
65
  await cmdCheck(rest, harnessRoot, pkgVersion);
63
66
  return;
64
67
  }
68
+ if (cmd === 'audit') {
69
+ await cmdAudit(rest);
70
+ return;
71
+ }
65
72
  if (cmd === 'task') {
66
73
  await cmdTask(rest);
67
74
  return;
@@ -186,6 +193,49 @@ async function cmdCheck(args, harnessRoot, pkgVersion) {
186
193
  }
187
194
  }
188
195
 
196
+ async function cmdAudit(args) {
197
+ let rest = args;
198
+ const { value: targetArg, rest: r1 } = takeOption(rest, '--target');
199
+ rest = r1;
200
+ const { value: taskFile, rest: r2 } = takeOption(rest, '--task');
201
+ rest = r2;
202
+
203
+ if (rest.length > 0) {
204
+ const err = new Error(`audit 未知参数: ${rest.join(' ')}`);
205
+ err.exitCode = 1;
206
+ throw err;
207
+ }
208
+
209
+ const target = resolveTarget(process.cwd(), targetArg);
210
+ const result = auditTarget(target, { taskFile });
211
+
212
+ console.log(`目标: ${target}`);
213
+ if (taskFile) console.log(`task: ${taskFile}`);
214
+ console.log(`audit: ${result.ok ? 'PASS' : 'FAIL'}`);
215
+
216
+ // D3 gate-check
217
+ console.log(` gate-check: ${result.gate.ok ? 'PASS' : 'FAIL'}`);
218
+ if (!result.gate.ok && result.gate.stdout) {
219
+ for (const line of result.gate.stdout.split('\n')) {
220
+ if (line.trim()) console.log(` ${line}`);
221
+ }
222
+ }
223
+
224
+ // D5 test
225
+ console.log(` test-check: ${result.test.ok ? 'PASS' : 'FAIL'}`);
226
+ if (result.test.reason) console.log(` ${result.test.reason}`);
227
+
228
+ // S5 git-clean
229
+ console.log(` git-clean: ${result.git.dirty ? 'DIRTY' : 'CLEAN'}`);
230
+ if (result.git.warning) console.log(` ${result.git.warning}`);
231
+
232
+ if (!result.ok) {
233
+ const err = new Error('ICVO audit 未通过');
234
+ err.exitCode = 2;
235
+ throw err;
236
+ }
237
+ }
238
+
189
239
  async function cmdTask(args) {
190
240
  const [sub, ...rest] = args;
191
241
  if (sub !== 'check') {
package/ontology.yaml CHANGED
@@ -1,9 +1,9 @@
1
- # cyning-harness 产品设计本体 · 机器可读抽取(草案 v0.4.0)
1
+ # cyning-harness 产品设计本体 · 机器可读抽取(草案 v1.0.0)
2
2
  # 人类真值:docs/methodology/product/DESIGN_ONTOLOGY_v1_zh.md
3
3
  # 冲突时以 Markdown 为准 · 供未来 harness ontology-check 使用
4
4
 
5
5
  version: "1.2"
6
- product_semver: "0.4.0"
6
+ product_semver: "1.0.0"
7
7
  license: MIT
8
8
 
9
9
  classes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://cyning.me/harness/schemas/invoke_index.v1.schema.json",
4
+ "title": "Harness InvokeSnapshot Index v1",
5
+ "description": "由 harness-sync --index 生成的 invokes/by-task 机器索引 · 只读聚合 · 不覆盖 S2 域",
6
+ "type": "object",
7
+ "required": ["schema_version", "generated_at", "index"],
8
+ "properties": {
9
+ "schema_version": {
10
+ "type": "string",
11
+ "const": "1"
12
+ },
13
+ "generated_at": {
14
+ "type": "string",
15
+ "format": "date-time"
16
+ },
17
+ "index": {
18
+ "type": "object",
19
+ "description": "task_slug → 该 task 的 invoke 列表",
20
+ "additionalProperties": {
21
+ "type": "object",
22
+ "required": ["task_slug", "invokes"],
23
+ "properties": {
24
+ "task_slug": {
25
+ "type": "string",
26
+ "pattern": "^[a-z0-9][a-z0-9_-]*$"
27
+ },
28
+ "invokes": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "object",
32
+ "required": ["path", "mtime", "hat_id"],
33
+ "properties": {
34
+ "path": {
35
+ "type": "string",
36
+ "description": "相对业务仓根的 invoke 文件路径"
37
+ },
38
+ "mtime": {
39
+ "type": "integer",
40
+ "description": "文件修改时间戳(秒)"
41
+ },
42
+ "hat_id": {
43
+ "type": "string",
44
+ "description": "从文件名解析的帽 ID,如 30 / 40 / CLOSE / HG-RELEASE"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ # SDD-Compliance micro-bench · v1 · S1–S4
3
+ # 输出合规率 %;不测 LLM 解题,只测 Orchestrate/Verify 可机械部分。
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ HARNESS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
8
+ BENCH_DIR="$HARNESS_ROOT/examples/compliance_bench"
9
+
10
+ SCENARIOS=()
11
+ QUIET=0
12
+
13
+ usage() {
14
+ cat <<'EOF'
15
+ 用法:compliance-bench.sh [--all | S1 S2 S3 S4] [--quiet]
16
+
17
+ --all 运行 S1–S4
18
+ --quiet 仅输出最终合规率
19
+ EOF
20
+ }
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --all) SCENARIOS=(S1 S2 S3 S4); shift ;;
25
+ --quiet) QUIET=1; shift ;;
26
+ S1|S2|S3|S4) SCENARIOS+=("$1"); shift ;;
27
+ -h|--help) usage; exit 0 ;;
28
+ *) echo "未知参数: $1" >&2; usage; exit 1 ;;
29
+ esac
30
+ done
31
+
32
+ [[ ${#SCENARIOS[@]} -gt 0 ]] || { usage; exit 1; }
33
+
34
+ log() { [[ "$QUIET" == "1" ]] || echo "$@"; }
35
+
36
+ PASS=0
37
+ FAIL=0
38
+ RESULTS=()
39
+
40
+ run_gate() {
41
+ local target="$1" task="$2"
42
+ "$HARNESS_ROOT/wizard/gate-check.sh" --target "$target" --task "$task" >/dev/null 2>&1 || true
43
+ # 返回实际 exit code
44
+ "$HARNESS_ROOT/wizard/gate-check.sh" --target "$target" --task "$task" >/dev/null 2>&1
45
+ }
46
+
47
+ scenario_S1() {
48
+ local dir="$BENCH_DIR/S1_r1_pending"
49
+ log "=== S1 · R1 pending 即 30(应拒) ==="
50
+ if ! "$HARNESS_ROOT/wizard/gate-check.sh" --target "$dir" --task task.md >/dev/null 2>&1; then
51
+ log "S1 PASS · gate-check 非零,30 被拒"
52
+ PASS=$((PASS+1))
53
+ RESULTS+=("S1|PASS")
54
+ else
55
+ log "S1 FAIL · gate-check exit 0,未拦截 pending R1"
56
+ FAIL=$((FAIL+1))
57
+ RESULTS+=("S1|FAIL")
58
+ fi
59
+ }
60
+
61
+ scenario_S2() {
62
+ local dir="$BENCH_DIR/S2_r1_no_review"
63
+ log "=== S2 · R1 approved 但无 review 文件(应非合规) ==="
64
+ local gate_ok=0
65
+ "$HARNESS_ROOT/wizard/gate-check.sh" --target "$dir" --task task.md >/dev/null 2>&1 && gate_ok=1 || true
66
+ local review_exists=0
67
+ [[ -n "$(find "$dir/reviews" -type f -name '*_audit_R1_*' 2>/dev/null)" ]] && review_exists=1 || true
68
+
69
+ if [[ "$review_exists" == "0" ]]; then
70
+ log "S2 PASS · review 文件缺失 -> 非合规 (gate-check exit=$gate_ok)"
71
+ PASS=$((PASS+1))
72
+ RESULTS+=("S2|PASS")
73
+ else
74
+ log "S2 FAIL · 不应存在 review 文件"
75
+ FAIL=$((FAIL+1))
76
+ RESULTS+=("S2|FAIL")
77
+ fi
78
+ }
79
+
80
+ scenario_S3() {
81
+ local dir="$BENCH_DIR/S3_r1_with_review"
82
+ log "=== S3 · R1 approved + review 落盘(应合规) ==="
83
+ local gate_ok=0
84
+ "$HARNESS_ROOT/wizard/gate-check.sh" --target "$dir" --task task.md >/dev/null 2>&1 && gate_ok=1 || true
85
+ local review_exists=0
86
+ [[ -n "$(find "$dir/reviews" -type f -name '*_audit_R1_*' 2>/dev/null)" ]] && review_exists=1 || true
87
+
88
+ if [[ "$gate_ok" == "1" && "$review_exists" == "1" ]]; then
89
+ log "S3 PASS · gate-check exit 0 且 review 文件存在"
90
+ PASS=$((PASS+1))
91
+ RESULTS+=("S3|PASS")
92
+ else
93
+ log "S3 FAIL · gate_ok=$gate_ok review_exists=$review_exists"
94
+ FAIL=$((FAIL+1))
95
+ RESULTS+=("S3|FAIL")
96
+ fi
97
+ }
98
+
99
+ scenario_S4() {
100
+ local dir="$BENCH_DIR/S4_sync_domain"
101
+ log "=== S4 · sync plan 不得含 task/reviews 路径 ==="
102
+ # 构造临时目标仓结构
103
+ local tmp
104
+ tmp="$(mktemp -d)"
105
+ mkdir -p "$tmp/.cyning-harness"
106
+ cp "$dir/profile.json" "$tmp/.cyning-harness/profile.json"
107
+ mkdir -p "$tmp/docs/harness/prompts" "$tmp/docs/harness/invokes" "$tmp/.cursor/rules"
108
+
109
+ local plan
110
+ plan="$tmp/plan.txt"
111
+ "$HARNESS_ROOT/wizard/harness-sync.sh" plan --target "$tmp" >"$plan" 2>&1 || true
112
+
113
+ local bad=0
114
+ if grep -E 'docs/tasks/|docs/harness/reviews/' "$plan" >/dev/null 2>&1; then
115
+ bad=1
116
+ fi
117
+ rm -rf "$tmp"
118
+
119
+ if [[ "$bad" == "0" ]]; then
120
+ log "S4 PASS · sync plan 未包含 task/reviews 路径"
121
+ PASS=$((PASS+1))
122
+ RESULTS+=("S4|PASS")
123
+ else
124
+ log "S4 FAIL · sync plan 包含不应同步的 task/reviews 路径"
125
+ FAIL=$((FAIL+1))
126
+ RESULTS+=("S4|FAIL")
127
+ fi
128
+ }
129
+
130
+ for s in "${SCENARIOS[@]}"; do
131
+ case "$s" in
132
+ S1) scenario_S1 ;;
133
+ S2) scenario_S2 ;;
134
+ S3) scenario_S3 ;;
135
+ S4) scenario_S4 ;;
136
+ esac
137
+ done
138
+
139
+ TOTAL=$((PASS+FAIL))
140
+ RATE=0
141
+ [[ "$TOTAL" -gt 0 ]] && RATE=$((PASS*100/TOTAL))
142
+
143
+ log ""
144
+ log "=== SDD-Compliance bench summary ==="
145
+ for r in "${RESULTS[@]}"; do
146
+ IFS='|' read -r id status <<<"$r"
147
+ log "$id: $status"
148
+ done
149
+ log "合规率: $PASS/$TOTAL = $RATE%"
150
+
151
+ # 静默模式仅输出数字
152
+ if [[ "$QUIET" == "1" ]]; then
153
+ echo "$RATE"
154
+ fi
155
+
156
+ [[ "$FAIL" -eq 0 ]]