svharness 0.14.2 → 0.14.5

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/README.md CHANGED
@@ -109,7 +109,7 @@ Harness 是一个 **项目本地的知识层**,由两大部分组成:
109
109
  ├── requirements/ # 正式需求(参与条目化)
110
110
  │ ├── raw/ # S20_collect_inputs 用户入口
111
111
  │ ├── md/ # convert 产物
112
- │ └── yaml/ # S40 条目化产出
112
+ │ └── yaml/ # S40 条目化产出(含 schema.json)
113
113
  ├── references/ # 参考资料(不参与条目化)
114
114
  │ ├── raw/
115
115
  │ ├── md/
@@ -185,8 +185,8 @@ harness-build-{skill|rule}-<semantic-name>
185
185
  | **S10_wiki** | 构建 baseline wiki(仅当有 baseline 时出现,项目经验底座) | `baseline/wiki/` |
186
186
  | **S20_collect_inputs** | 添加相关文件:需求/参考文档 + 额外运行期资源征询(`--extra-skills` 单入口,可混放 skills/rules) | `requirements/raw/` + `references/raw/` + `agent-env/_incoming/skills/` + `task_list.md` |
187
187
  | **S30_convert_docs** | 非 Markdown 原始文档转 Markdown | `requirements/md/` + `references/md/` |
188
- | **S40_extract_requirements** | 条目化需求 + 覆盖率门禁 | `requirements/yaml/*.yaml` + `requirements/coverage-report.yaml` |
189
- | **S50_generate_specs** | 生成规格 + REQ→spec 覆盖 | `specs/<layer>/<module>.<layer>.yaml` + schema 校验 + 覆盖率结果 |
188
+ | **S40_extract_requirements** | 条目化需求 + 覆盖率门禁 + 保真门禁(`title/source_excerpt/description`) | `requirements/yaml/*.yaml` + `requirements/yaml/schema.json` + `requirements/coverage-report.yaml` |
189
+ | **S50_generate_specs** | 生成规格 + REQ→spec 覆盖 + 深度门禁(非 index-only) | `specs/<layer>/<module>.<layer>.yaml` + schema 校验 + 覆盖率结果 |
190
190
  | **S60_process_references** | references 处理(`svharness convert` + 结构化索引 + 用户确认落地) | `references/md/` |
191
191
  | **S61_confirm_baseline_extraction** | 确认是否自动从 baseline 提取 skills/rules(表单确认) | `.harness-build-state.yaml`(`phases.S61_confirm_baseline_extraction.baseline_auto_extract`) |
192
192
  | **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
@@ -426,6 +426,10 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
426
426
  - **门禁**:`overall_score >= 5.0`(C 级)、`critical_count == 0`、报告 frontmatter `gate_pass: true`、用户表单确认
427
427
 
428
428
  `doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass` 一致。数量覆盖率阻断仍由 S40/S50 表单门禁负责。
429
+ 同时新增:
430
+
431
+ - `check-requirements-fidelity`:检查 `source_excerpt` 缺失、`description` 缩略与占位语义。
432
+ - `check-specs-depth`:检查 index-only 信号、enum 缺失 `enum_values`、behavior 空 `guard/action`。
429
433
 
430
434
  ### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
431
435
 
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkRequirementsFidelity = checkRequirementsFidelity;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const utils_1 = require("./utils");
11
+ const TRUNCATION_HINT_RE = /(详见原文|见上文|同前|tbd|placeholder|…|\.{3,})/i;
12
+ const MIN_EXCERPT_LEN = 20;
13
+ const WARN_RATIO = 0.5;
14
+ const ERROR_RATIO = 0.25;
15
+ function normalizeLength(v) {
16
+ if (typeof v !== 'string')
17
+ return 0;
18
+ return v.replace(/\s+/g, '').length;
19
+ }
20
+ async function checkRequirementsFidelity(ctx) {
21
+ const id = 'requirements-fidelity';
22
+ const findings = [];
23
+ const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
24
+ const reqYamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
25
+ for (const yamlFile of reqYamlFiles) {
26
+ let parsed;
27
+ try {
28
+ parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(yamlFile, 'utf8'));
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ const items = parsed?.items;
34
+ if (!Array.isArray(items))
35
+ continue;
36
+ for (const [idx, item] of items.entries()) {
37
+ const obj = (item ?? {});
38
+ const itemId = typeof obj.id === 'string' ? obj.id : `#${idx + 1}`;
39
+ const excerpt = typeof obj.source_excerpt === 'string' ? obj.source_excerpt : '';
40
+ const desc = typeof obj.description === 'string' ? obj.description : '';
41
+ const excerptLen = normalizeLength(excerpt);
42
+ const descLen = normalizeLength(desc);
43
+ const ratio = excerptLen > 0 ? descLen / excerptLen : 1;
44
+ const itemPath = `${(0, utils_1.rel)(ctx.harnessRoot, yamlFile)}:${itemId}`;
45
+ if (excerptLen === 0) {
46
+ findings.push({
47
+ checkId: id,
48
+ severity: 'error',
49
+ message: 'source_excerpt 为空,无法证明需求保留了源文档信息',
50
+ path: itemPath,
51
+ });
52
+ }
53
+ else if (excerptLen < MIN_EXCERPT_LEN) {
54
+ findings.push({
55
+ checkId: id,
56
+ severity: 'warning',
57
+ message: `source_excerpt 过短(${excerptLen}),建议补充完整原文摘录`,
58
+ path: itemPath,
59
+ });
60
+ }
61
+ if (!desc.trim()) {
62
+ findings.push({
63
+ checkId: id,
64
+ severity: 'error',
65
+ message: 'description 为空,缺少完整需求陈述',
66
+ path: itemPath,
67
+ });
68
+ }
69
+ else if (ratio < ERROR_RATIO) {
70
+ findings.push({
71
+ checkId: id,
72
+ severity: ctx.mode === 'pre-seal' ? 'error' : 'warning',
73
+ message: `description 相对 source_excerpt 明显缩略(ratio=${ratio.toFixed(2)})`,
74
+ path: itemPath,
75
+ });
76
+ }
77
+ else if (ratio < WARN_RATIO) {
78
+ findings.push({
79
+ checkId: id,
80
+ severity: 'warning',
81
+ message: `description 可能缩略 source_excerpt(ratio=${ratio.toFixed(2)})`,
82
+ path: itemPath,
83
+ });
84
+ }
85
+ if (TRUNCATION_HINT_RE.test(excerpt) || TRUNCATION_HINT_RE.test(desc)) {
86
+ findings.push({
87
+ checkId: id,
88
+ severity: 'warning',
89
+ message: '检测到截断/占位语义(如 详见原文、TBD、placeholder)',
90
+ path: itemPath,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return { id, title: '需求保真度', findings };
96
+ }
@@ -7,6 +7,8 @@ exports.checkRequirements = checkRequirements;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const ajv_1 = __importDefault(require("ajv"));
11
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
10
12
  const utils_1 = require("./utils");
11
13
  async function checkRequirements(ctx) {
12
14
  const id = 'requirements';
@@ -35,6 +37,24 @@ async function checkRequirements(ctx) {
35
37
  }
36
38
  }
37
39
  else {
40
+ const schemaPath = node_path_1.default.join(yamlDir, 'schema.json');
41
+ let validate;
42
+ if (await fs_extra_1.default.pathExists(schemaPath)) {
43
+ try {
44
+ const schema = JSON.parse(await fs_extra_1.default.readFile(schemaPath, 'utf8'));
45
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
46
+ (0, ajv_formats_1.default)(ajv);
47
+ validate = ajv.compile(schema);
48
+ }
49
+ catch (err) {
50
+ findings.push({
51
+ checkId: id,
52
+ severity: 'error',
53
+ message: `requirements/yaml/schema.json 无效:${err.message}`,
54
+ path: 'requirements/yaml/schema.json',
55
+ });
56
+ }
57
+ }
38
58
  for (const file of yamlFiles) {
39
59
  try {
40
60
  const raw = await fs_extra_1.default.readFile(file, 'utf8');
@@ -47,7 +67,19 @@ async function checkRequirements(ctx) {
47
67
  });
48
68
  continue;
49
69
  }
50
- js_yaml_1.default.load(raw);
70
+ const parsed = js_yaml_1.default.load(raw);
71
+ if (validate && !validate(parsed)) {
72
+ const detail = (validate.errors ?? [])
73
+ .slice(0, 3)
74
+ .map((e) => `${e.instancePath || '/'} ${e.message}`)
75
+ .join('; ');
76
+ findings.push({
77
+ checkId: id,
78
+ severity: 'error',
79
+ message: `requirements schema 校验失败:${detail}`,
80
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
81
+ });
82
+ }
51
83
  }
52
84
  catch (err) {
53
85
  findings.push({
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkSpecsDepth = checkSpecsDepth;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const utils_1 = require("./utils");
11
+ function asArray(v) {
12
+ return Array.isArray(v) ? v : [];
13
+ }
14
+ async function getEnabledLayers(harnessRoot) {
15
+ const { doc } = await (0, utils_1.readHarnessYaml)(harnessRoot);
16
+ const layers = asArray(doc?.specs?.layers);
17
+ const enabled = new Set();
18
+ for (const l of layers) {
19
+ const name = l?.name;
20
+ if (name === 'signals' || name === 'behavior' || name === 'interfaces' || name === 'ui') {
21
+ enabled.add(name);
22
+ }
23
+ }
24
+ if (enabled.size === 0) {
25
+ enabled.add('signals');
26
+ enabled.add('behavior');
27
+ enabled.add('interfaces');
28
+ enabled.add('ui');
29
+ }
30
+ return enabled;
31
+ }
32
+ async function getDepthThresholds(harnessRoot) {
33
+ const defaults = {
34
+ warnThreshold: 0.5,
35
+ failThreshold: 0.8,
36
+ };
37
+ const { doc } = await (0, utils_1.readHarnessYaml)(harnessRoot);
38
+ const arch = doc?.arch?.trim();
39
+ const candidates = [
40
+ arch ? node_path_1.default.join(harnessRoot, 'agent-env', 'review-profiles', `${arch}.yaml`) : '',
41
+ node_path_1.default.join(harnessRoot, 'agent-env', 'review-profiles', '_default.yaml'),
42
+ ].filter(Boolean);
43
+ for (const file of candidates) {
44
+ if (!(await fs_extra_1.default.pathExists(file)))
45
+ continue;
46
+ try {
47
+ const parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(file, 'utf8'));
48
+ const heuristics = (parsed?.heuristics ?? {});
49
+ const fail = Number(heuristics.index_signal_id_ratio_threshold);
50
+ if (!Number.isNaN(fail) && fail > 0 && fail <= 1) {
51
+ return {
52
+ warnThreshold: Math.max(0.1, Math.min(0.95, fail * 0.7)),
53
+ failThreshold: fail,
54
+ };
55
+ }
56
+ }
57
+ catch {
58
+ // ignore invalid profile and fall back
59
+ }
60
+ }
61
+ return defaults;
62
+ }
63
+ async function scanSignalsDepth(ctx, findings, warnThreshold, failThreshold) {
64
+ const signalsDir = node_path_1.default.join(ctx.harnessRoot, 'specs', 'signals');
65
+ const files = (await (0, utils_1.listRealFilesRecursive)(signalsDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
66
+ let totalSignals = 0;
67
+ let indexOnlySignals = 0;
68
+ for (const file of files) {
69
+ let parsed;
70
+ try {
71
+ parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(file, 'utf8'));
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ const signals = asArray(parsed?.signals);
77
+ for (const sig of signals) {
78
+ totalSignals++;
79
+ const id = typeof sig.id === 'string' ? sig.id : '';
80
+ const hasDescription = typeof sig.description === 'string' && sig.description.trim().length >= 8;
81
+ const hasEnumValues = Array.isArray(sig.enum_values) && sig.enum_values.length > 0;
82
+ const hasLength = typeof sig.length_bytes === 'number';
83
+ const isEnum = sig.type === 'enum';
84
+ const maybeIndexOnly = /^CMDID_[A-Z0-9_]+$/.test(id) && !hasDescription && !hasLength && (!isEnum || !hasEnumValues);
85
+ if (maybeIndexOnly) {
86
+ indexOnlySignals++;
87
+ }
88
+ if (isEnum && !hasEnumValues) {
89
+ findings.push({
90
+ checkId: 'specs-depth',
91
+ severity: 'warning',
92
+ message: `enum 信号缺少 enum_values(${id || 'unknown-id'})`,
93
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
94
+ });
95
+ }
96
+ }
97
+ }
98
+ if (totalSignals === 0)
99
+ return;
100
+ const ratio = indexOnlySignals / totalSignals;
101
+ if (ratio >= failThreshold) {
102
+ findings.push({
103
+ checkId: 'specs-depth',
104
+ severity: 'error',
105
+ message: `signals 索引式占比过高(${indexOnlySignals}/${totalSignals}=${ratio.toFixed(2)})`,
106
+ path: 'specs/signals',
107
+ });
108
+ }
109
+ else if (ratio >= warnThreshold) {
110
+ findings.push({
111
+ checkId: 'specs-depth',
112
+ severity: 'warning',
113
+ message: `signals 索引式占比偏高(${indexOnlySignals}/${totalSignals}=${ratio.toFixed(2)})`,
114
+ path: 'specs/signals',
115
+ });
116
+ }
117
+ }
118
+ async function scanBehaviorDepth(ctx, findings) {
119
+ const behaviorDir = node_path_1.default.join(ctx.harnessRoot, 'specs', 'behavior');
120
+ const files = (await (0, utils_1.listRealFilesRecursive)(behaviorDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
121
+ for (const file of files) {
122
+ let parsed;
123
+ try {
124
+ parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(file, 'utf8'));
125
+ }
126
+ catch {
127
+ continue;
128
+ }
129
+ const transitions = asArray(parsed?.transitions);
130
+ for (const t of transitions) {
131
+ const trigger = typeof t.trigger === 'string' ? t.trigger : 'unknown-trigger';
132
+ const guard = typeof t.guard === 'string' ? t.guard.trim() : '';
133
+ const action = typeof t.action === 'string' ? t.action.trim() : '';
134
+ if (!guard || /^见需求|^tbd|placeholder/i.test(guard)) {
135
+ findings.push({
136
+ checkId: 'specs-depth',
137
+ severity: 'warning',
138
+ message: `transition.guard 为空或占位(${trigger})`,
139
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
140
+ });
141
+ }
142
+ if (!action || /^见需求|^tbd|placeholder/i.test(action)) {
143
+ findings.push({
144
+ checkId: 'specs-depth',
145
+ severity: 'warning',
146
+ message: `transition.action 为空或占位(${trigger})`,
147
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
148
+ });
149
+ }
150
+ }
151
+ }
152
+ }
153
+ async function checkSpecsDepth(ctx) {
154
+ const id = 'specs-depth';
155
+ const findings = [];
156
+ const enabledLayers = await getEnabledLayers(ctx.harnessRoot);
157
+ const { warnThreshold, failThreshold } = await getDepthThresholds(ctx.harnessRoot);
158
+ if (enabledLayers.has('signals')) {
159
+ await scanSignalsDepth(ctx, findings, warnThreshold, failThreshold);
160
+ }
161
+ if (enabledLayers.has('behavior')) {
162
+ await scanBehaviorDepth(ctx, findings);
163
+ }
164
+ return { id, title: 'Specs 深度启发式', findings };
165
+ }
@@ -45,7 +45,9 @@ const check_empty_dirs_1 = require("./check-empty-dirs");
45
45
  const check_convert_pairing_1 = require("./check-convert-pairing");
46
46
  const check_requirements_1 = require("./check-requirements");
47
47
  const check_requirements_coverage_1 = require("./check-requirements-coverage");
48
+ const check_requirements_fidelity_1 = require("./check-requirements-fidelity");
48
49
  const check_specs_1 = require("./check-specs");
50
+ const check_specs_depth_1 = require("./check-specs-depth");
49
51
  const check_references_1 = require("./check-references");
50
52
  const check_skills_tasks_1 = require("./check-skills-tasks");
51
53
  const check_memory_1 = require("./check-memory");
@@ -74,7 +76,9 @@ async function runDoctorChecks(input) {
74
76
  check_convert_pairing_1.checkConvertPairing,
75
77
  check_requirements_1.checkRequirements,
76
78
  check_requirements_coverage_1.checkRequirementsCoverage,
79
+ check_requirements_fidelity_1.checkRequirementsFidelity,
77
80
  check_specs_1.checkSpecs,
81
+ check_specs_depth_1.checkSpecsDepth,
78
82
  check_references_1.checkReferences,
79
83
  check_skills_tasks_1.checkSkillsTasks,
80
84
  check_memory_1.checkMemory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svharness",
3
- "version": "0.14.2",
3
+ "version": "0.14.5",
4
4
  "description": "CLI scaffolder for SDD-Driven Agent-Agnostic Coding Framework (harness)",
5
5
  "bin": {
6
6
  "svharness": "./bin/cli.js",
@@ -6,7 +6,7 @@
6
6
 
7
7
  以下产出**必须**使用中文撰写:
8
8
 
9
- - `requirements/yaml/*.requirements.yaml` 中 `description`、`acceptance` 字段
9
+ - `requirements/yaml/*.requirements.yaml` 中 `title`、`source_excerpt`、`description`、`acceptance` 字段
10
10
  - `specs/**/*.yaml` 中所有描述性字段(如 `description`、`note`、`summary`、`purpose`)
11
11
  - `agent-env/rules/**/*.md` 或 `*.mdc` 中正文段落、正反例注释、检测说明
12
12
  - `agent-env/skills/*/SKILL.md` 正文及 description
@@ -32,7 +32,7 @@
32
32
  ## 正反例
33
33
 
34
34
  正例:
35
- > `description: "根据 P_TMRAVA 信号显示或隐藏预约充电设置入口"`
35
+ > `description: " P_TMRAVA=1 且车辆处于驻车状态时显示预约充电入口;当 P_TMRAVA=0 或车速>0 时隐藏入口。"`
36
36
 
37
37
  反例(严禁):
38
38
  > `description: "Show or hide the scheduled charging settings entry based on P_TMRAVA signal"`
@@ -14,12 +14,15 @@
14
14
  3. **S30_convert_docs** — 将 `requirements/raw/` 和 `references/raw/` 中的非 Markdown 原始文档统一转为 Markdown,产物落到对应 `md/` 目录。准入:S20_collect_inputs DONE;退出:每份源文档在对应 `md/` 目录中有同基名的 `.md` 文件。
15
15
  4. **S40_extract_requirements** — 由 `harness-build-skill-spec-builder` 将 raw 条目化到 `requirements/yaml/`。退出:
16
16
  - 已生成 `requirements/coverage-report.yaml`;
17
+ - `requirements/yaml/schema.json`(若存在)校验通过;
17
18
  - `unmapped` 为空,或已全部完成 waiver 与用户确认;
18
- - 每条需求具备稳定 id 与可追溯锚点(允许有备案聚合,但不得无依据粗化)。
19
+ - 每条需求具备稳定 id 与可追溯锚点(允许有备案聚合,但不得无依据粗化);
20
+ - 每条需求包含 `title`、`source_excerpt`、`description`,且不存在明显缩略风险(门禁策略可由 doctor/审查报告提供证据)。
19
21
  5. **S50_generate_specs** — 生成 `specs/{signals,ui,behavior,interfaces}/*.yaml`。退出:
20
22
  - 全部通过 `specs/*/schema.json` 校验;
21
23
  - 已产出 spec 覆盖率结果(建议 `specs/coverage-report.yaml`);
22
- - 每条 REQ 有 spec 映射或显式 N/A 理由。
24
+ - 每条 REQ 有 spec 映射或显式 N/A 理由;
25
+ - 深度启发式无 Critical(如 index-only signals、空 guard、断裂 bound_* 引用)。
23
26
  6. **S60_process_references**(阶段叙事:**references 处理**)— 对 S20 中的 references 执行处理与结构化:
24
27
  - 必须执行 `svharness convert`,不得跳过转换直接抽取;
25
28
  - 必须产出结构化索引(规制约束 / skills 候选 / signals / manuals,缺项需显式标注);
@@ -21,10 +21,16 @@
21
21
 
22
22
  1. **需求可追溯**(若存在 requirements/yaml):每条在已声明 spec 层有映射或 waiver。
23
23
  2. **覆盖率完整**:`requirements/coverage-report.yaml` 与(若存在)`specs/coverage-report.yaml` 可解析;`unmapped` 闭环。
24
- 3. **描述完整性**:无 TBD/placeholder 占位语义。
24
+ 3. **描述完整性**:无 TBD/placeholder 占位语义,且 `description` 不得明显弱于 `source_excerpt`。
25
25
  4. **references / wiki / agent-env**(若目录存在):与 S60、baseline 策略一致。
26
26
  5. **流程合规**、**中文与 agent-agnostic** 扫尾。
27
27
 
28
+ 补充抽检要求(至少随机 N=5 条或按项目规模调整):
29
+
30
+ - 抽检 `requirements/yaml`:确认 `source_excerpt` 保留原文约束(条件/数值/枚举),`description` 未删减关键语义。
31
+ - 抽检 `specs/signals`:识别 index-only 信号(仅 id/name/type 且缺少协议/枚举语义)。
32
+ - 抽检 `specs/behavior`:`transitions[].guard`/`action` 为空或占位语义应计入风险。
33
+
28
34
  ### Part B — 深度质量(按 specs.layers 启用)
29
35
 
30
36
  - 加载 `agent-env/review-profiles/<arch>.yaml` 或 `_default.yaml`。
@@ -23,7 +23,13 @@
23
23
  - `aggregation_reason`
24
24
  - 用户表单确认
25
25
 
26
- 4. **覆盖率报告是 S40 必备产物**
26
+ 4. **原文优先,禁止缩略**
27
+ - `source_excerpt` 必须是可追溯锚点对应的原文摘录,禁止写成「见上文」「同前」「TBD」。
28
+ - `description` 必须覆盖 `source_excerpt` 里的条件、数值、枚举、时序,不得退化为标题短语。
29
+ - 推荐使用长度启发式审查:`description` 明显短于 `source_excerpt`(如 < 50%)时标记风险并复核。
30
+ - `title` 仅用于索引,禁止用 `title` 替代完整需求描述。
31
+
32
+ 5. **覆盖率报告是 S40 必备产物**
27
33
  - 必须生成 `requirements/coverage-report.yaml`。
28
34
  - 报告至少包含:
29
35
  - `extraction_strategy`
@@ -41,3 +47,5 @@
41
47
  - 禁止跳过源清点(inventory)直接写 `requirements/yaml`。
42
48
  - 禁止仅凭少量摘要条目宣称「提取完成」。
43
49
  - 禁止在未说明依据的情况下选择提取策略。
50
+ - 禁止 `source_excerpt` 留空或使用占位语。
51
+ - 禁止将多行表格仅提取第一列后声称完成条目化。
@@ -20,10 +20,13 @@ specs/
20
20
  - 字段增减/重命名时,`schema.json` 与对应 yaml 必须**同一提交**更新,不得分两步。
21
21
  - yaml 中不得出现任何具体 agent 名(qoder/codechat/cursor/claude-code/opencode 等)— 违反 agent-agnostic 铁律。
22
22
 
23
- ## requirements 追溯字段建议(S40/S50 配套)
23
+ ## requirements 追溯字段要求(S40/S50 配套)
24
24
 
25
- 为提升 requirements/specs 完整性,`requirements/yaml/*.requirements.yaml` 的条目建议包含:
25
+ 为提升 requirements/specs 完整性,`requirements/yaml/*.requirements.yaml` 的条目必须包含:
26
26
 
27
+ - `title`: 条目短索引标题(仅用于导航与表格展示)
28
+ - `source_excerpt`: 源文档原文摘录(可多行)
29
+ - `description`: 完整需求陈述(不得弱于 source_excerpt)
27
30
  - `source_anchor`: 可追踪源锚点(如 `SyRD-*`、章节+行号)
28
31
  - `source_file`: 源文件路径(通常在 `requirements/raw/`)
29
32
  - `source_section`: 源章节或表格区域
@@ -32,12 +35,13 @@ specs/
32
35
  - `waived`: 是否作为门禁豁免项
33
36
  - `waived_reason`: 豁免原因(waived 为 true 时必填)
34
37
 
35
- 以上为推荐字段,不改变四域 specs schema 强约束。
38
+ 以上要求与 `requirements/yaml/schema.json` 保持一致;不满足时不得推进到 S50。
36
39
 
37
40
  ## 校验节点
38
41
 
39
42
  - S50_generate_specs 退出前:所有 yaml 逐一过 schema 校验,失败则阶段状态置 FAILED。
40
43
  - 后续阶段读取 specs 时不做 schema 校验(已由 S50 把关),但仍不得修改已冻结字段。
44
+ - specs 内容不得退化为索引式占位(仅 `id`/`name` 且缺少语义字段);发现此类情况应在 S50 判为失败或在 S85 记 Critical。
41
45
 
42
46
  ## 正反例
43
47
 
@@ -62,6 +62,7 @@ profile 提供:`min_depth_score`、`dimension_weights`、`critical_patterns`
62
62
  **阶段合规**:对照 `.harness-build-state.yaml`,S00–S80 在封存前应 DONE(S10_wiki 若不存在则跳过)。
63
63
 
64
64
  **可追溯**:若存在 `requirements/yaml/*.yaml`,每条需求须在已声明 spec 层有映射、`aggregates` 或合法 `waived`。
65
+ **完整性抽检**:随机抽检至少 5 条(或按规模取样),校验 `description` 是否覆盖 `source_excerpt` 中的条件、数值、枚举、时序。
65
66
 
66
67
  **其它**:references/wiki 一致性;构建期表单确认(`harness-build-rule-user-interaction`);中文与 agent-agnostic 扫尾。
67
68
 
@@ -89,6 +90,7 @@ Part A 结论写入报告「Part A 清单」章节,逐项 PASS/WARN/FAIL/N/A
89
90
  - 索引式 ID 占比(如 `CMDID_<模块>_<编号>` 且无 `enum_values`/`can_msg_id`)
90
91
  - 方向分布:仅 `car2hmi` 无 `hmi2car` → WARN/FAIL
91
92
  - 字段深度:仅 id/name/direction/type/traces_to → 索引式
93
+ - 对 `enum` 类型:缺少 `enum_values` 视为深度不足(至少 WARN,可按 profile 记 Critical)
92
94
 
93
95
  **若 profile 提供 `oem_signal_name_patterns`**:计算 OEM 名占比;`< threshold` → 索引式规格(模式 P1,可 Critical)。
94
96
 
@@ -96,6 +98,7 @@ Part A 结论写入报告「Part A 清单」章节,逐项 PASS/WARN/FAIL/N/A
96
98
 
97
99
  - `req_count / state_count` > profile 阈值 → 严重不足(P4)
98
100
  - `transitions < states * 1.5` → WARN
101
+ - `guard` / `action` 为空或出现「见需求」「TBD」占位语义 → WARN/FAIL
99
102
 
100
103
  ### 维度 4:interfaces 契约(仅当存在 specs/interfaces)
101
104
 
@@ -44,7 +44,13 @@ items:
44
44
  - id: REQ-<MODULE>-001
45
45
  category: functional | non-functional | constraint
46
46
  priority: must | should | may
47
- description: "一句话陈述需求"
47
+ title: "短索引标题(用于表格与路由)"
48
+ source_excerpt: |
49
+ 从 source_anchor 对应原文复制的完整摘录(允许多行)。
50
+ 必须保留条件、数值、枚举、时序等关键信息。
51
+ description: |
52
+ 基于 source_excerpt 的完整需求陈述。
53
+ 允许整理语序,但信息集合不得缩小。
48
54
  acceptance: "如何验证该需求满足"
49
55
  source_anchor: "SyRD-..."
50
56
  source_file: "requirements/raw/<file>"
@@ -58,10 +64,13 @@ items:
58
64
  规则:
59
65
  - 条目 id **稳定**:一旦产出不得改写,新条目追加。
60
66
  - 有可追踪锚点时默认 1:1 条目化(细不能粗)。
61
- - 每条 `description` 必须是一句清晰陈述,不得写成聚合摘要。
67
+ - `title` 仅作索引;不得把 `title` 充当完整需求描述。
68
+ - `source_excerpt` 必须来自源文档原文;禁止写成「见上文」「同前」等替代语。
69
+ - `description` 必须覆盖 `source_excerpt` 的关键约束;不得删减条件、数值、枚举、时序。
62
70
  - `traces_to` 初始可为空,阶段 B 生成 specs 后回填。
63
71
  - `source_doc` 与 `source_file` 必须引用真实源文件。
64
72
  - 聚合仅在用户确认后允许,且必须带 `aggregates + aggregation_reason`。
73
+ - 产出后必须校验 `requirements/yaml/schema.json`(若存在);失败不得进入 S50。
65
74
 
66
75
  ### 阶段 B:requirements → specs(规格生成)
67
76
 
@@ -71,7 +80,11 @@ items:
71
80
  2. 读取 `__HARNESS_ROOT__specs/<layer>/schema.json`。
72
81
  3. 生成 `__HARNESS_ROOT__specs/<layer>/<module>.<layer>.yaml`,要求:
73
82
  - 满足 schema(所有 `required` 字段完备、`enum` 取值合法)
74
- - 在本层的语义内回答相关需求条目
83
+ - 在本层语义内完整回答相关需求条目,禁止仅保留索引占位
84
+ - `signals`:除 `id` 外须补全 `name`、`direction`、`type`;`type=enum` 时必须有 `enum_values`
85
+ - `behavior`:`transitions[].guard/action` 应为可执行陈述,禁止「见需求」占位
86
+ - `ui`:`components[].states[].condition` 必须写出具体触发条件并可追溯到 REQ
87
+ - `interfaces`:`methods[].description` 需包含参数语义与返回约束,不得仅列方法名
75
88
  4. 回填 `traces_to`:在 `requirements` 中把 spec 的路径/锚点记回对应条目。
76
89
  5. 校验 YAML 是否通过 schema(优先使用本地校验器;若无,对照 schema 的 `required` 与 `enum` 手动核对)。
77
90
  6. 生成 S40 覆盖率报告:`__HARNESS_ROOT__requirements/coverage-report.yaml`,至少包含:
@@ -91,6 +104,25 @@ waived:
91
104
  unmapped: []
92
105
  ```
93
106
 
107
+ 7. 生成 S50 规格覆盖率报告:`__HARNESS_ROOT__specs/coverage-report.yaml`,至少包含:
108
+
109
+ ```yaml
110
+ generated_at: "..."
111
+ module: "<module>"
112
+ requirements_total: 120
113
+ requirements_mapped: 116
114
+ requirements_waived: 4
115
+ mapping_ratio: 0.967
116
+ layers:
117
+ - name: signals
118
+ req_mapped: 80
119
+ quality_flags: []
120
+ - name: behavior
121
+ req_mapped: 20
122
+ quality_flags: []
123
+ critical_gaps: []
124
+ ```
125
+
94
126
  ## 状态更新
95
127
 
96
128
  每完成一个模块后:
@@ -101,8 +133,10 @@ unmapped: []
101
133
 
102
134
  在 S40 标 DONE 前必须输出表格并经用户确认:
103
135
 
104
- - 列:`源 ID | YAML REQ ID | 状态(mapped/waived/gap)`
136
+ - 列:`源 ID | YAML REQ ID | excerpt_len | desc_len | 缩略风险 | 状态(mapped/waived/gap)`
137
+ - `缩略风险` 判定建议:`desc_len / excerpt_len < 0.5` 记 WARN;`< 0.25` 记 FAIL。
105
138
  - 若存在 `gap` 且未完成 waiver,**禁止**标记 `S40_extract_requirements: DONE`。
139
+ - 若存在 `source_excerpt` 缺失、`description` 缩略 FAIL、或 schema 校验失败,**禁止**标记 DONE。
106
140
 
107
141
  ## 守则(不得违反)
108
142
 
@@ -23,6 +23,7 @@ requirements: # 正式需求(参与条目化)
23
23
  raw: requirements/raw/ # S20_collect_inputs 用户入口
24
24
  md: requirements/md/ # convert 产物
25
25
  yaml: requirements/yaml/ # S40 条目化产出
26
+ schema: requirements/yaml/schema.json # requirements 条目结构校验
26
27
  # requirements 输入路径(可选):可记录外部需求文档来源(文件或目录)。
27
28
  <% if (requirementsPath) { -%>
28
29
  source: "<%= requirementsPath %>"
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "harness://requirements/yaml/schema.json",
4
+ "title": "需求条目 Schema",
5
+ "description": "描述 requirements/yaml 下的结构化需求条目,要求保留原文摘录与完整需求陈述。",
6
+ "type": "object",
7
+ "required": ["module", "source_doc", "extracted_at", "intake_profile", "items"],
8
+ "properties": {
9
+ "module": { "type": "string", "minLength": 1 },
10
+ "source_doc": { "type": "string", "minLength": 1 },
11
+ "extracted_at": { "type": "string", "format": "date-time" },
12
+ "intake_profile": {
13
+ "type": "object",
14
+ "required": ["extraction_strategy"],
15
+ "properties": {
16
+ "extraction_strategy": {
17
+ "type": "string",
18
+ "enum": ["md_primary", "raw_primary", "hybrid", "manual_assisted"]
19
+ }
20
+ },
21
+ "additionalProperties": true
22
+ },
23
+ "items": {
24
+ "type": "array",
25
+ "minItems": 1,
26
+ "items": {
27
+ "type": "object",
28
+ "required": [
29
+ "id",
30
+ "category",
31
+ "priority",
32
+ "title",
33
+ "source_excerpt",
34
+ "description",
35
+ "acceptance",
36
+ "source_anchor",
37
+ "source_file",
38
+ "source_section",
39
+ "aggregates",
40
+ "waived",
41
+ "traces_to"
42
+ ],
43
+ "properties": {
44
+ "id": { "type": "string", "pattern": "^REQ-[A-Z0-9_]+-\\d{3,}$" },
45
+ "category": { "type": "string", "enum": ["functional", "non-functional", "constraint"] },
46
+ "priority": { "type": "string", "enum": ["must", "should", "may"] },
47
+ "title": { "type": "string", "minLength": 2 },
48
+ "source_excerpt": { "type": "string", "minLength": 1 },
49
+ "description": { "type": "string", "minLength": 1 },
50
+ "acceptance": { "type": "string", "minLength": 1 },
51
+ "source_anchor": { "type": "string", "minLength": 1 },
52
+ "source_file": { "type": "string", "minLength": 1 },
53
+ "source_section": { "type": "string", "minLength": 1 },
54
+ "aggregates": {
55
+ "type": "array",
56
+ "items": { "type": "string", "minLength": 1 }
57
+ },
58
+ "aggregation_reason": { "type": "string" },
59
+ "waived": { "type": "boolean" },
60
+ "waived_reason": { "type": "string" },
61
+ "traces_to": {
62
+ "type": "array",
63
+ "items": { "type": "string", "minLength": 1 }
64
+ }
65
+ },
66
+ "additionalProperties": true
67
+ }
68
+ }
69
+ },
70
+ "additionalProperties": true
71
+ }
@@ -6,7 +6,7 @@
6
6
  "type": "object",
7
7
  "required": ["module", "version", "states", "transitions"],
8
8
  "properties": {
9
- "module": { "type": "string" },
9
+ "module": { "type": "string", "minLength": 1 },
10
10
  "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
11
11
  "initial_state": { "type": "string" },
12
12
  "states": {
@@ -15,8 +15,8 @@
15
15
  "type": "object",
16
16
  "required": ["name"],
17
17
  "properties": {
18
- "name": { "type": "string" },
19
- "description": { "type": "string" }
18
+ "name": { "type": "string", "minLength": 1 },
19
+ "description": { "type": "string", "minLength": 8 }
20
20
  }
21
21
  }
22
22
  },
@@ -26,11 +26,11 @@
26
26
  "type": "object",
27
27
  "required": ["from", "to", "trigger"],
28
28
  "properties": {
29
- "from": { "type": "string" },
30
- "to": { "type": "string" },
31
- "trigger": { "type": "string" },
32
- "guard": { "type": "string" },
33
- "action": { "type": "string" }
29
+ "from": { "type": "string", "minLength": 1 },
30
+ "to": { "type": "string", "minLength": 1 },
31
+ "trigger": { "type": "string", "minLength": 1 },
32
+ "guard": { "type": "string", "minLength": 4 },
33
+ "action": { "type": "string", "minLength": 4 }
34
34
  }
35
35
  }
36
36
  }
@@ -6,7 +6,7 @@
6
6
  "type": "object",
7
7
  "required": ["module", "version", "interfaces"],
8
8
  "properties": {
9
- "module": { "type": "string" },
9
+ "module": { "type": "string", "minLength": 1 },
10
10
  "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
11
11
  "interfaces": {
12
12
  "type": "array",
@@ -14,7 +14,7 @@
14
14
  "type": "object",
15
15
  "required": ["name", "kind"],
16
16
  "properties": {
17
- "name": { "type": "string" },
17
+ "name": { "type": "string", "minLength": 1 },
18
18
  "kind": { "type": "string", "enum": ["aidl", "http", "ipc", "binder", "custom"] },
19
19
  "methods": {
20
20
  "type": "array",
@@ -22,10 +22,10 @@
22
22
  "type": "object",
23
23
  "required": ["name"],
24
24
  "properties": {
25
- "name": { "type": "string" },
25
+ "name": { "type": "string", "minLength": 1 },
26
26
  "params": { "type": "array", "items": { "type": "object" } },
27
27
  "returns": { "type": "string" },
28
- "description": { "type": "string" }
28
+ "description": { "type": "string", "minLength": 8 }
29
29
  }
30
30
  }
31
31
  }
@@ -15,13 +15,15 @@
15
15
  "required": ["id", "name", "direction", "type"],
16
16
  "properties": {
17
17
  "id": { "type": "string", "pattern": "^CMDID_[A-Z0-9_]+$" },
18
- "name": { "type": "string" },
18
+ "name": { "type": "string", "minLength": 1 },
19
+ "description": { "type": "string", "minLength": 8 },
19
20
  "direction": { "type": "string", "enum": ["car2hmi", "hmi2car", "bidirectional"] },
20
21
  "type": { "type": "string", "enum": ["int", "bool", "enum", "byte-array", "float"] },
21
22
  "length_bytes": { "type": "integer", "minimum": 1 },
22
23
  "default": {},
23
24
  "enum_values": {
24
25
  "type": "array",
26
+ "minItems": 1,
25
27
  "items": { "type": "object", "required": ["value", "meaning"] }
26
28
  },
27
29
  "traces_to": {
@@ -29,6 +31,15 @@
29
31
  "items": { "type": "string" }
30
32
  }
31
33
  },
34
+ "allOf": [
35
+ {
36
+ "if": {
37
+ "properties": { "type": { "const": "enum" } },
38
+ "required": ["type"]
39
+ },
40
+ "then": { "required": ["enum_values"] }
41
+ }
42
+ ],
32
43
  "additionalProperties": true
33
44
  }
34
45
  }
@@ -6,7 +6,7 @@
6
6
  "type": "object",
7
7
  "required": ["module", "version", "components"],
8
8
  "properties": {
9
- "module": { "type": "string" },
9
+ "module": { "type": "string", "minLength": 1 },
10
10
  "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
11
11
  "figma_source": { "type": "string", "format": "uri" },
12
12
  "ue_spec_ref": { "type": "string" },
@@ -16,11 +16,12 @@
16
16
  "type": "object",
17
17
  "required": ["id", "type"],
18
18
  "properties": {
19
- "id": { "type": "string" },
19
+ "id": { "type": "string", "minLength": 1 },
20
20
  "type": {
21
21
  "type": "string",
22
22
  "enum": ["Switch", "Slider", "Button", "Tab", "Checkbox", "RadioButton", "Text", "Image", "Custom"]
23
23
  },
24
+ "description": { "type": "string", "minLength": 8 },
24
25
  "figma_node_id": { "type": "string" },
25
26
  "bound_signal": { "type": "string" },
26
27
  "states": {
@@ -29,9 +30,9 @@
29
30
  "type": "object",
30
31
  "required": ["name"],
31
32
  "properties": {
32
- "name": { "type": "string" },
33
+ "name": { "type": "string", "minLength": 1 },
33
34
  "visual": { "type": "string" },
34
- "condition": { "type": "string" }
35
+ "condition": { "type": "string", "minLength": 4 }
35
36
  }
36
37
  }
37
38
  }