svharness 0.14.1 → 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 +19 -6
- package/dist/commands/doctor/check-bootstrap.js +4 -2
- package/dist/commands/doctor/check-empty-dirs.js +15 -7
- package/dist/commands/doctor/check-memory.js +2 -1
- package/dist/commands/doctor/check-requirements-coverage.js +100 -0
- package/dist/commands/doctor/check-requirements-fidelity.js +96 -0
- package/dist/commands/doctor/check-requirements.js +42 -7
- package/dist/commands/doctor/check-review-report.js +113 -0
- package/dist/commands/doctor/check-skills-tasks.js +2 -1
- package/dist/commands/doctor/check-specs-depth.js +165 -0
- package/dist/commands/doctor/check-specs.js +10 -6
- package/dist/commands/doctor/check-state-phases.js +23 -0
- package/dist/commands/doctor/index.js +8 -0
- package/dist/commands/doctor/utils.js +54 -4
- package/dist/commands/init.js +6 -3
- package/dist/core/build-project-entry.js +2 -0
- package/dist/core/render-meta.js +1 -0
- package/dist/core/state.js +22 -4
- package/dist/utils/yaml-safe-path.js +33 -0
- package/package.json +1 -2
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +2 -2
- package/templates/_shared/build-rules/harness-build-rule-convert-check.md +6 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +21 -7
- package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +38 -12
- package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +51 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +18 -0
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +8 -0
- package/templates/_shared/build-skills/harness-build-harness-data-review.md +16 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +16 -6
- package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +196 -31
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +83 -13
- package/templates/_shared/build-skills/harness-build-skills-main.md +3 -2
- package/templates/_shared/meta/harness.yaml.ejs +1 -0
- package/templates/_shared/meta/task_list.md.ejs +44 -0
- package/templates/_shared/skeleton/agent-env/review-profiles/_default.yaml +64 -0
- package/templates/_shared/skeleton/agent-env/review-profiles/android-compose.yaml +35 -0
- package/templates/_shared/skeleton/requirements/yaml/schema.json +71 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +8 -8
- package/templates/_shared/skeleton/specs/interfaces/schema.json +4 -4
- package/templates/_shared/skeleton/specs/signals/schema.json +12 -1
- package/templates/_shared/skeleton/specs/ui/schema.json +5 -4
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/
|
|
@@ -168,7 +168,7 @@ harness-build-{skill|rule}-<semantic-name>
|
|
|
168
168
|
| 资源类型 | 目录 | 命名模式 | 现有成员 |
|
|
169
169
|
|----------|------|----------|----------|
|
|
170
170
|
| **Build skill** | `templates/_shared/build-skills/` | `harness-build-skill-<name>.md` | `skills-main` / `orchestrator` / `spec-builder` / `references-intake` / `agent-env-merge` / `knowledge-builder` / `wiki-writer` |
|
|
171
|
-
| **Build rule** | `templates/_shared/build-rules/` | `harness-build-rule-<name>.md` | `agent-agnostic` / `chinese-only` / `memory-write` / `orchestrator-flow` / `skills-tasks-output` / `specs-schema` / `user-interaction` |
|
|
171
|
+
| **Build rule** | `templates/_shared/build-rules/` | `harness-build-rule-<name>.md` | `agent-agnostic` / `chinese-only` / `memory-write` / `orchestrator-flow` / `requirements-extraction` / `skills-tasks-output` / `specs-schema` / `user-interaction` |
|
|
172
172
|
|
|
173
173
|
约束:
|
|
174
174
|
|
|
@@ -183,10 +183,10 @@ harness-build-{skill|rule}-<semantic-name>
|
|
|
183
183
|
| 阶段 | 动作 | 产物 |
|
|
184
184
|
|------|------|------|
|
|
185
185
|
| **S10_wiki** | 构建 baseline wiki(仅当有 baseline 时出现,项目经验底座) | `baseline/wiki/` |
|
|
186
|
-
| **S20_collect_inputs** | 添加相关文件:需求/参考文档 + 额外运行期资源征询(`--extra-skills` 单入口,可混放 skills/rules) | `requirements/raw/` + `references/raw/` + `agent-env/_incoming/skills/` |
|
|
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` |
|
|
189
|
-
| **S50_generate_specs** | 生成规格 | `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/` |
|
|
@@ -201,6 +201,7 @@ harness-build-{skill|rule}-<semantic-name>
|
|
|
201
201
|
> **S60_process_references 专注 references:必须执行 `svharness convert`,并产出结构化索引(规制约束 / skills 候选 / signals / manuals),经表单确认后再进入后续阶段。**
|
|
202
202
|
> **S61_confirm_baseline_extraction 必须显式确认是否启用 baseline 自动提取 skills/rules;未经确认不得在 S65 自动提取。**
|
|
203
203
|
> **S65_customize_agent_env 专注 extra-skills/extra-rules:来源可来自 `--extra-skills` 导入后的 `_incoming/manifest.yaml`,先冲突识别与重命名建议,再表单确认写入;推荐命名:`harness-apply-skills-<topic>` 与 `harness-apply-rules-<topic>.md/.mdc`。**
|
|
204
|
+
> **S40/S50/S85 的 DONE 都带覆盖率门禁:未闭环 gap(或未备案 waiver)不得推进。**
|
|
204
205
|
|
|
205
206
|
---
|
|
206
207
|
|
|
@@ -416,7 +417,19 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
|
|
|
416
417
|
| `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
|
|
417
418
|
| `--strict` | 将 warning 视为 error |
|
|
418
419
|
|
|
419
|
-
退出码:`0` 通过,`1` 存在 error
|
|
420
|
+
退出码:`0` 通过,`1` 存在 error。
|
|
421
|
+
|
|
422
|
+
**S85 Agent 审查(合并 skill)**:doctor 通过后,由 `harness-build-skill-pre-seal-review`(v2,已吸收 `harness-build-harness-data-review`)产出 **`HARNESS-REVIEW-REPORT.md`**:
|
|
423
|
+
|
|
424
|
+
- **Part A**:从 `harness.yaml` 发现 assets,完整性清单(arch-agnostic)
|
|
425
|
+
- **Part B**:按 `specs.layers` 与 `agent-env/review-profiles/<arch>.yaml` 做深度评分
|
|
426
|
+
- **门禁**:`overall_score >= 5.0`(C 级)、`critical_count == 0`、报告 frontmatter `gate_pass: true`、用户表单确认
|
|
427
|
+
|
|
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`。
|
|
420
433
|
|
|
421
434
|
### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
|
|
422
435
|
|
|
@@ -8,12 +8,14 @@ async function checkBootstrap(ctx) {
|
|
|
8
8
|
if (ctx.mode !== 'pre-seal') {
|
|
9
9
|
return { id, title: 'Bootstrap 模式', findings };
|
|
10
10
|
}
|
|
11
|
-
const harness = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
|
|
11
|
+
const { doc: harness, error } = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
|
|
12
12
|
if (!harness) {
|
|
13
13
|
findings.push({
|
|
14
14
|
checkId: id,
|
|
15
15
|
severity: 'error',
|
|
16
|
-
message:
|
|
16
|
+
message: error
|
|
17
|
+
? `无法解析 harness.yaml:${error}`
|
|
18
|
+
: '缺少 harness.yaml',
|
|
17
19
|
path: 'harness.yaml',
|
|
18
20
|
});
|
|
19
21
|
return { id, title: 'Bootstrap 模式', findings };
|
|
@@ -7,28 +7,36 @@ exports.checkEmptyDirs = checkEmptyDirs;
|
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const utils_1 = require("./utils");
|
|
10
|
-
async function collectEmptyDirs(
|
|
10
|
+
async function collectEmptyDirs(ctx, dir, findings, checkId) {
|
|
11
11
|
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
12
12
|
return;
|
|
13
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
13
14
|
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
14
15
|
for (const ent of entries) {
|
|
15
16
|
if (ent.name.startsWith('.') && ent.name !== '.gitkeep')
|
|
16
17
|
continue;
|
|
17
18
|
const full = node_path_1.default.join(dir, ent.name);
|
|
18
19
|
if (ent.isDirectory()) {
|
|
19
|
-
const relPath = (0, utils_1.rel)(harnessRoot, full);
|
|
20
|
+
const relPath = (0, utils_1.rel)(ctx.harnessRoot, full);
|
|
21
|
+
if ((0, utils_1.isBaselineCodeMirror)(relPath))
|
|
22
|
+
continue;
|
|
23
|
+
if ((0, utils_1.isSkillReferencePlaceholder)(relPath))
|
|
24
|
+
continue;
|
|
20
25
|
if (utils_1.EMPTY_DIR_WHITELIST.has(relPath)) {
|
|
21
|
-
await collectEmptyDirs(
|
|
26
|
+
await collectEmptyDirs(ctx, full, findings, checkId);
|
|
22
27
|
continue;
|
|
23
28
|
}
|
|
24
29
|
if (relPath === 'agent-env/_incoming/skills') {
|
|
25
|
-
const state = await (0, utils_1.readBuildState)(harnessRoot);
|
|
26
30
|
const s65 = state?.phases.S65_customize_agent_env;
|
|
27
31
|
if (s65?.status === 'DONE') {
|
|
28
|
-
await collectEmptyDirs(
|
|
32
|
+
await collectEmptyDirs(ctx, full, findings, checkId);
|
|
29
33
|
continue;
|
|
30
34
|
}
|
|
31
35
|
}
|
|
36
|
+
if ((0, utils_1.isDeferredEmptyDir)(relPath, ctx.mode, state)) {
|
|
37
|
+
await collectEmptyDirs(ctx, full, findings, checkId);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
32
40
|
if (await (0, utils_1.isEmptyOrGitkeepOnly)(full)) {
|
|
33
41
|
findings.push({
|
|
34
42
|
checkId,
|
|
@@ -38,7 +46,7 @@ async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
|
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
48
|
else {
|
|
41
|
-
await collectEmptyDirs(
|
|
49
|
+
await collectEmptyDirs(ctx, full, findings, checkId);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -46,6 +54,6 @@ async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
|
|
|
46
54
|
async function checkEmptyDirs(ctx) {
|
|
47
55
|
const id = 'empty-dirs';
|
|
48
56
|
const findings = [];
|
|
49
|
-
await collectEmptyDirs(ctx
|
|
57
|
+
await collectEmptyDirs(ctx, ctx.harnessRoot, findings, id);
|
|
50
58
|
return { id, title: '空目录检查', findings };
|
|
51
59
|
}
|
|
@@ -10,8 +10,9 @@ async function checkMemory(ctx) {
|
|
|
10
10
|
const id = 'memory';
|
|
11
11
|
const findings = [];
|
|
12
12
|
const categoriesDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', 'memory', 'categories');
|
|
13
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
13
14
|
const mdFiles = await (0, utils_1.listRealFilesRecursive)(categoriesDir, { extensions: ['.md'] });
|
|
14
|
-
if (mdFiles.length === 0) {
|
|
15
|
+
if (mdFiles.length === 0 && !(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S80_seed_memory')) {
|
|
15
16
|
findings.push({
|
|
16
17
|
checkId: id,
|
|
17
18
|
severity: 'error',
|
|
@@ -0,0 +1,100 @@
|
|
|
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.checkRequirementsCoverage = checkRequirementsCoverage;
|
|
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 SOURCE_ANCHOR_RE = /SyRD-[\w-]+/g;
|
|
12
|
+
const WILDCARD_RE = /\*/;
|
|
13
|
+
function collectStringValues(node, out) {
|
|
14
|
+
if (typeof node === 'string') {
|
|
15
|
+
out.push(node);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(node)) {
|
|
19
|
+
for (const item of node)
|
|
20
|
+
collectStringValues(item, out);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (node && typeof node === 'object') {
|
|
24
|
+
for (const value of Object.values(node)) {
|
|
25
|
+
collectStringValues(value, out);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function collectAnchors(node, out) {
|
|
30
|
+
const values = [];
|
|
31
|
+
collectStringValues(node, values);
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
const matches = value.match(SOURCE_ANCHOR_RE);
|
|
34
|
+
if (!matches)
|
|
35
|
+
continue;
|
|
36
|
+
for (const m of matches)
|
|
37
|
+
out.add(m);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function checkRequirementsCoverage(ctx) {
|
|
41
|
+
const id = 'requirements-coverage';
|
|
42
|
+
const findings = [];
|
|
43
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
44
|
+
const coveragePath = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'coverage-report.yaml');
|
|
45
|
+
const hasCoverageReport = await fs_extra_1.default.pathExists(coveragePath);
|
|
46
|
+
const s40Done = state?.phases?.S40_extract_requirements?.status === 'DONE';
|
|
47
|
+
if (s40Done && !hasCoverageReport) {
|
|
48
|
+
findings.push({
|
|
49
|
+
checkId: id,
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
message: 'S40_extract_requirements 已是 DONE,但缺少 requirements/coverage-report.yaml',
|
|
52
|
+
path: 'requirements/coverage-report.yaml',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const mdDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'md');
|
|
56
|
+
const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
|
|
57
|
+
const mdFiles = await (0, utils_1.listRealFilesRecursive)(mdDir, { extensions: ['.md'] });
|
|
58
|
+
const reqYamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
|
|
59
|
+
const mdAnchors = new Set();
|
|
60
|
+
for (const mdFile of mdFiles) {
|
|
61
|
+
const raw = await fs_extra_1.default.readFile(mdFile, 'utf8');
|
|
62
|
+
const matches = raw.match(SOURCE_ANCHOR_RE);
|
|
63
|
+
if (!matches)
|
|
64
|
+
continue;
|
|
65
|
+
for (const m of matches)
|
|
66
|
+
mdAnchors.add(m);
|
|
67
|
+
}
|
|
68
|
+
const yamlAnchors = new Set();
|
|
69
|
+
for (const yamlFile of reqYamlFiles) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(yamlFile, 'utf8'));
|
|
72
|
+
collectAnchors(parsed, yamlAnchors);
|
|
73
|
+
const values = [];
|
|
74
|
+
collectStringValues(parsed, values);
|
|
75
|
+
if (values.some((v) => WILDCARD_RE.test(v) && /SyRD-|REQ-/i.test(v))) {
|
|
76
|
+
findings.push({
|
|
77
|
+
checkId: id,
|
|
78
|
+
severity: 'warning',
|
|
79
|
+
message: '检测到可能的通配符聚合锚点,请确认已备案 aggregates/waiver',
|
|
80
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, yamlFile),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// parse errors are handled by check-requirements
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (mdAnchors.size > 0) {
|
|
89
|
+
const ratio = yamlAnchors.size / mdAnchors.size;
|
|
90
|
+
if (ratio < 0.5) {
|
|
91
|
+
findings.push({
|
|
92
|
+
checkId: id,
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
message: `requirements 锚点覆盖率偏低(${yamlAnchors.size}/${mdAnchors.size}=${ratio.toFixed(3)})`,
|
|
95
|
+
path: 'requirements',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { id, title: '需求覆盖率', findings };
|
|
100
|
+
}
|
|
@@ -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,12 +7,15 @@ 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';
|
|
13
15
|
const findings = [];
|
|
14
16
|
const rawDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'raw');
|
|
15
17
|
const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
|
|
18
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
16
19
|
const rawFiles = await (0, utils_1.listRealFilesRecursive)(rawDir);
|
|
17
20
|
if (rawFiles.length === 0) {
|
|
18
21
|
findings.push({
|
|
@@ -24,14 +27,34 @@ async function checkRequirements(ctx) {
|
|
|
24
27
|
}
|
|
25
28
|
const yamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
|
|
26
29
|
if (yamlFiles.length === 0) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (!(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S40_extract_requirements')) {
|
|
31
|
+
findings.push({
|
|
32
|
+
checkId: id,
|
|
33
|
+
severity: 'error',
|
|
34
|
+
message: 'requirements/yaml/ 无条目化 YAML 产出',
|
|
35
|
+
path: 'requirements/yaml',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
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
|
+
}
|
|
35
58
|
for (const file of yamlFiles) {
|
|
36
59
|
try {
|
|
37
60
|
const raw = await fs_extra_1.default.readFile(file, 'utf8');
|
|
@@ -44,7 +67,19 @@ async function checkRequirements(ctx) {
|
|
|
44
67
|
});
|
|
45
68
|
continue;
|
|
46
69
|
}
|
|
47
|
-
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
|
+
}
|
|
48
83
|
}
|
|
49
84
|
catch (err) {
|
|
50
85
|
findings.push({
|
|
@@ -0,0 +1,113 @@
|
|
|
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.checkReviewReport = checkReviewReport;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
function parseFrontmatter(content) {
|
|
12
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
return js_yaml_1.default.load(match[1]);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function checkReviewReport(ctx) {
|
|
23
|
+
const id = 'review-report';
|
|
24
|
+
const findings = [];
|
|
25
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
26
|
+
const s85 = state?.phases?.S85_pre_seal_validation;
|
|
27
|
+
const reportRel = (s85 && typeof s85 === 'object' && 'review_report_path' in s85 && s85.review_report_path) ||
|
|
28
|
+
'HARNESS-REVIEW-REPORT.md';
|
|
29
|
+
const reportPath = node_path_1.default.join(ctx.harnessRoot, String(reportRel));
|
|
30
|
+
if (ctx.mode !== 'pre-seal' && ctx.mode !== 'health') {
|
|
31
|
+
return { id, title: '深度审查报告', findings };
|
|
32
|
+
}
|
|
33
|
+
if (!s85) {
|
|
34
|
+
if (ctx.mode === 'pre-seal') {
|
|
35
|
+
findings.push({
|
|
36
|
+
checkId: id,
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
message: '状态文件缺少 S85_pre_seal_validation 阶段定义',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return { id, title: '深度审查报告', findings };
|
|
42
|
+
}
|
|
43
|
+
const s85Done = s85.status === 'DONE';
|
|
44
|
+
if (!(await fs_extra_1.default.pathExists(reportPath))) {
|
|
45
|
+
if (s85Done) {
|
|
46
|
+
findings.push({
|
|
47
|
+
checkId: id,
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
message: `S85 已为 DONE,但未找到审查报告 ${reportRel}`,
|
|
50
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { id, title: '深度审查报告', findings };
|
|
54
|
+
}
|
|
55
|
+
const content = await fs_extra_1.default.readFile(reportPath, 'utf8');
|
|
56
|
+
const fm = parseFrontmatter(content);
|
|
57
|
+
if (!fm) {
|
|
58
|
+
findings.push({
|
|
59
|
+
checkId: id,
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
message: `${reportRel} 缺少可解析的 YAML frontmatter(需 gate_pass、overall_score 等)`,
|
|
62
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
|
|
63
|
+
});
|
|
64
|
+
return { id, title: '深度审查报告', findings };
|
|
65
|
+
}
|
|
66
|
+
const minScore = typeof s85 === 'object' && 'min_depth_score' in s85 && s85.min_depth_score != null
|
|
67
|
+
? Number(s85.min_depth_score)
|
|
68
|
+
: 5.0;
|
|
69
|
+
if (fm.gate_pass !== true) {
|
|
70
|
+
findings.push({
|
|
71
|
+
checkId: id,
|
|
72
|
+
severity: s85Done ? 'warning' : 'info',
|
|
73
|
+
message: `审查报告 gate_pass 不为 true(当前:${String(fm.gate_pass)})`,
|
|
74
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (typeof fm.overall_score === 'number' && fm.overall_score < minScore) {
|
|
78
|
+
findings.push({
|
|
79
|
+
checkId: id,
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
message: `overall_score ${fm.overall_score} 低于门禁 ${minScore}`,
|
|
82
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (typeof fm.critical_count === 'number' && fm.critical_count > 0) {
|
|
86
|
+
findings.push({
|
|
87
|
+
checkId: id,
|
|
88
|
+
severity: 'warning',
|
|
89
|
+
message: `critical_count=${fm.critical_count},深度审查存在 Critical 差距`,
|
|
90
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (s85Done) {
|
|
94
|
+
if (typeof s85 === 'object' && s85.review_gate_pass !== true) {
|
|
95
|
+
findings.push({
|
|
96
|
+
checkId: id,
|
|
97
|
+
severity: 'warning',
|
|
98
|
+
message: 'S85 为 DONE 但 review_gate_pass 不为 true,与报告/frontmatter 不一致',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (typeof s85 === 'object' &&
|
|
102
|
+
'critical_gaps' in s85 &&
|
|
103
|
+
typeof s85.critical_gaps === 'number' &&
|
|
104
|
+
s85.critical_gaps > 0) {
|
|
105
|
+
findings.push({
|
|
106
|
+
checkId: id,
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
message: `S85 为 DONE 但 critical_gaps=${s85.critical_gaps}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { id, title: '深度审查报告', findings };
|
|
113
|
+
}
|
|
@@ -90,8 +90,9 @@ async function checkSkillsTasks(ctx) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
|
|
93
94
|
const categoryCount = await countTaskCategories(templatesDir);
|
|
94
|
-
if (categoryCount < 3) {
|
|
95
|
+
if (categoryCount < 3 && !(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S70_runtime_assets')) {
|
|
95
96
|
findings.push({
|
|
96
97
|
checkId: id,
|
|
97
98
|
severity: 'error',
|