svharness 0.13.5 → 0.14.2

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 (49) hide show
  1. package/README.md +43 -7
  2. package/dist/commands/apply.js +7 -77
  3. package/dist/commands/doctor/check-bootstrap.js +34 -0
  4. package/dist/commands/doctor/check-chinese-heuristic.js +64 -0
  5. package/dist/commands/doctor/check-convert-pairing.js +54 -0
  6. package/dist/commands/doctor/check-empty-dirs.js +59 -0
  7. package/dist/commands/doctor/check-incoming.js +47 -0
  8. package/dist/commands/doctor/check-memory.js +24 -0
  9. package/dist/commands/doctor/check-policy-grep.js +44 -0
  10. package/dist/commands/doctor/check-references.js +78 -0
  11. package/dist/commands/doctor/check-requirements-coverage.js +100 -0
  12. package/dist/commands/doctor/check-requirements.js +72 -0
  13. package/dist/commands/doctor/check-review-report.js +113 -0
  14. package/dist/commands/doctor/check-skills-tasks.js +104 -0
  15. package/dist/commands/doctor/check-specs.js +87 -0
  16. package/dist/commands/doctor/check-state-phases.js +116 -0
  17. package/dist/commands/doctor/check-wiki.js +52 -0
  18. package/dist/commands/doctor/index.js +105 -0
  19. package/dist/commands/doctor/report.js +79 -0
  20. package/dist/commands/doctor/types.js +2 -0
  21. package/dist/commands/doctor/utils.js +180 -0
  22. package/dist/commands/doctor.js +51 -0
  23. package/dist/commands/init.js +6 -3
  24. package/dist/config/index.js +2 -1
  25. package/dist/config/merge-options.js +13 -0
  26. package/dist/core/agent-injector.js +1 -0
  27. package/dist/core/build-project-entry.js +2 -0
  28. package/dist/core/render-meta.js +1 -0
  29. package/dist/core/state.js +29 -5
  30. package/dist/index.js +41 -0
  31. package/dist/utils/skill-md-validate.js +85 -0
  32. package/dist/utils/yaml-safe-path.js +33 -0
  33. package/package.json +3 -2
  34. package/templates/_shared/build-rules/harness-build-rule-convert-check.md +6 -0
  35. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +21 -3
  36. package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +59 -0
  37. package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +43 -0
  38. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +14 -0
  39. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +8 -0
  40. package/templates/_shared/build-skills/harness-build-harness-data-review.md +16 -0
  41. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +19 -3
  42. package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +213 -0
  43. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +47 -11
  44. package/templates/_shared/build-skills/harness-build-skills-main.md +3 -1
  45. package/templates/_shared/meta/README.md.ejs +2 -1
  46. package/templates/_shared/meta/task_list.md.ejs +44 -0
  47. package/templates/_shared/skeleton/agent-env/review-profiles/_default.yaml +64 -0
  48. package/templates/_shared/skeleton/agent-env/review-profiles/android-compose.yaml +35 -0
  49. package/templates/svharness.config.example.yaml +8 -0
@@ -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,72 @@
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.checkRequirements = checkRequirements;
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
+ async function checkRequirements(ctx) {
12
+ const id = 'requirements';
13
+ const findings = [];
14
+ const rawDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'raw');
15
+ const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
16
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
17
+ const rawFiles = await (0, utils_1.listRealFilesRecursive)(rawDir);
18
+ if (rawFiles.length === 0) {
19
+ findings.push({
20
+ checkId: id,
21
+ severity: 'error',
22
+ message: 'requirements/raw/ 无真实文档(不得仅含 .gitkeep)',
23
+ path: 'requirements/raw',
24
+ });
25
+ }
26
+ const yamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
27
+ if (yamlFiles.length === 0) {
28
+ if (!(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S40_extract_requirements')) {
29
+ findings.push({
30
+ checkId: id,
31
+ severity: 'error',
32
+ message: 'requirements/yaml/ 无条目化 YAML 产出',
33
+ path: 'requirements/yaml',
34
+ });
35
+ }
36
+ }
37
+ else {
38
+ for (const file of yamlFiles) {
39
+ try {
40
+ const raw = await fs_extra_1.default.readFile(file, 'utf8');
41
+ if (!raw.trim()) {
42
+ findings.push({
43
+ checkId: id,
44
+ severity: 'error',
45
+ message: 'requirements YAML 文件为空',
46
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
47
+ });
48
+ continue;
49
+ }
50
+ js_yaml_1.default.load(raw);
51
+ }
52
+ catch (err) {
53
+ findings.push({
54
+ checkId: id,
55
+ severity: 'error',
56
+ message: `requirements YAML 解析失败:${err.message}`,
57
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
58
+ });
59
+ }
60
+ }
61
+ }
62
+ const mdDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'md');
63
+ if (rawFiles.length > 0 && (await (0, utils_1.listDirEntries)(mdDir)).length === 0) {
64
+ findings.push({
65
+ checkId: id,
66
+ severity: 'warning',
67
+ message: 'requirements/raw 有文件但 requirements/md/ 为空,请确认 S30 已完成',
68
+ path: 'requirements/md',
69
+ });
70
+ }
71
+ return { id, title: '需求文档', findings };
72
+ }
@@ -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
+ }
@@ -0,0 +1,104 @@
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.checkSkillsTasks = checkSkillsTasks;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ const skill_md_validate_1 = require("../../utils/skill-md-validate");
11
+ async function countTaskCategories(templatesDir) {
12
+ if (!(await fs_extra_1.default.pathExists(templatesDir)))
13
+ return 0;
14
+ const entries = await fs_extra_1.default.readdir(templatesDir, { withFileTypes: true });
15
+ let matched = 0;
16
+ const seen = new Set();
17
+ for (const ent of entries) {
18
+ if (!ent.isDirectory())
19
+ continue;
20
+ const dirPath = node_path_1.default.join(templatesDir, ent.name);
21
+ const mdFiles = (await fs_extra_1.default.readdir(dirPath)).filter((n) => n.endsWith('.md'));
22
+ if (mdFiles.length === 0)
23
+ continue;
24
+ const haystack = `${ent.name} ${mdFiles.join(' ')}`.toLowerCase();
25
+ for (let i = 0; i < utils_1.TASK_CATEGORY_HINTS.length; i++) {
26
+ if (seen.has(i))
27
+ continue;
28
+ if (utils_1.TASK_CATEGORY_HINTS[i].some((kw) => haystack.includes(kw.toLowerCase()))) {
29
+ seen.add(i);
30
+ matched++;
31
+ }
32
+ }
33
+ }
34
+ if (matched < 3) {
35
+ const flatMd = await fs_extra_1.default
36
+ .readdir(templatesDir)
37
+ .then((names) => names.filter((n) => n.endsWith('.md')));
38
+ if (flatMd.length >= 3)
39
+ matched = 3;
40
+ }
41
+ return matched;
42
+ }
43
+ async function checkSkillsTasks(ctx) {
44
+ const id = 'skills-tasks';
45
+ const findings = [];
46
+ const skillsDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', 'skills');
47
+ const templatesDir = node_path_1.default.join(ctx.harnessRoot, 'tasks', 'templates');
48
+ if (!(await fs_extra_1.default.pathExists(skillsDir))) {
49
+ findings.push({
50
+ checkId: id,
51
+ severity: 'error',
52
+ message: '缺少 agent-env/skills/',
53
+ path: 'agent-env/skills',
54
+ });
55
+ }
56
+ else {
57
+ const skillDirs = (await fs_extra_1.default.readdir(skillsDir, { withFileTypes: true })).filter((d) => d.isDirectory() && !d.name.startsWith('.'));
58
+ const runtimeSkills = skillDirs.filter((d) => !d.name.startsWith('harness-build-'));
59
+ if (runtimeSkills.length === 0) {
60
+ findings.push({
61
+ checkId: id,
62
+ severity: 'error',
63
+ message: 'agent-env/skills/ 无运行期 skill 子目录',
64
+ path: 'agent-env/skills',
65
+ });
66
+ }
67
+ for (const dir of runtimeSkills) {
68
+ const skillMd = node_path_1.default.join(skillsDir, dir.name, 'SKILL.md');
69
+ if (!(await fs_extra_1.default.pathExists(skillMd))) {
70
+ findings.push({
71
+ checkId: id,
72
+ severity: 'error',
73
+ message: `skill 目录缺少 SKILL.md:${dir.name}`,
74
+ path: (0, utils_1.rel)(ctx.harnessRoot, node_path_1.default.join(skillsDir, dir.name)),
75
+ });
76
+ continue;
77
+ }
78
+ for (const msg of await (0, skill_md_validate_1.validateFrontmatterFields)({
79
+ targetRoot: ctx.harnessRoot,
80
+ file: skillMd,
81
+ requiredKeys: ['name', 'description'],
82
+ })) {
83
+ findings.push({ checkId: id, severity: 'error', message: msg, path: (0, utils_1.rel)(ctx.harnessRoot, skillMd) });
84
+ }
85
+ for (const msg of await (0, skill_md_validate_1.validateSkillNameMatchesDir)({
86
+ targetRoot: ctx.harnessRoot,
87
+ file: skillMd,
88
+ })) {
89
+ findings.push({ checkId: id, severity: 'warning', message: msg, path: (0, utils_1.rel)(ctx.harnessRoot, skillMd) });
90
+ }
91
+ }
92
+ }
93
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
94
+ const categoryCount = await countTaskCategories(templatesDir);
95
+ if (categoryCount < 3 && !(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S70_runtime_assets')) {
96
+ findings.push({
97
+ checkId: id,
98
+ severity: 'error',
99
+ message: `tasks/templates/ 须覆盖三类典型场景(当前识别 ${categoryCount}/3)`,
100
+ path: 'tasks/templates',
101
+ });
102
+ }
103
+ return { id, title: 'Skills 与 Tasks', findings };
104
+ }
@@ -0,0 +1,87 @@
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.checkSpecs = checkSpecs;
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 ajv_1 = __importDefault(require("ajv"));
11
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
12
+ const utils_1 = require("./utils");
13
+ const SPEC_DOMAINS = ['signals', 'ui', 'behavior', 'interfaces'];
14
+ async function checkSpecs(ctx) {
15
+ const id = 'specs';
16
+ const findings = [];
17
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
18
+ (0, ajv_formats_1.default)(ajv);
19
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
20
+ const deferSpecs = (0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S50_generate_specs');
21
+ for (const domain of SPEC_DOMAINS) {
22
+ const domainDir = node_path_1.default.join(ctx.harnessRoot, 'specs', domain);
23
+ const schemaPath = node_path_1.default.join(domainDir, 'schema.json');
24
+ if (!(await fs_extra_1.default.pathExists(schemaPath))) {
25
+ findings.push({
26
+ checkId: id,
27
+ severity: 'error',
28
+ message: `缺少 schema:specs/${domain}/schema.json`,
29
+ path: `specs/${domain}/schema.json`,
30
+ });
31
+ continue;
32
+ }
33
+ const specFiles = (await (0, utils_1.listRealFilesRecursive)(domainDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json') && !f.endsWith('schema.yaml'));
34
+ const moduleYaml = specFiles.filter((f) => node_path_1.default.basename(f) !== 'schema.json');
35
+ if (moduleYaml.length === 0) {
36
+ if (!deferSpecs) {
37
+ findings.push({
38
+ checkId: id,
39
+ severity: 'error',
40
+ message: `specs/${domain}/ 无模块 YAML(除 schema.json 外)`,
41
+ path: `specs/${domain}`,
42
+ });
43
+ }
44
+ continue;
45
+ }
46
+ let validate;
47
+ try {
48
+ const schema = JSON.parse(await fs_extra_1.default.readFile(schemaPath, 'utf8'));
49
+ validate = ajv.compile(schema);
50
+ }
51
+ catch (err) {
52
+ findings.push({
53
+ checkId: id,
54
+ severity: 'error',
55
+ message: `specs/${domain}/schema.json 无效:${err.message}`,
56
+ path: `specs/${domain}/schema.json`,
57
+ });
58
+ continue;
59
+ }
60
+ for (const file of moduleYaml) {
61
+ try {
62
+ const doc = js_yaml_1.default.load(await fs_extra_1.default.readFile(file, 'utf8'));
63
+ if (!validate(doc)) {
64
+ const detail = (validate.errors ?? [])
65
+ .slice(0, 3)
66
+ .map((e) => `${e.instancePath || '/'} ${e.message}`)
67
+ .join('; ');
68
+ findings.push({
69
+ checkId: id,
70
+ severity: 'error',
71
+ message: `schema 校验失败:${detail}`,
72
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
73
+ });
74
+ }
75
+ }
76
+ catch (err) {
77
+ findings.push({
78
+ checkId: id,
79
+ severity: 'error',
80
+ message: `YAML 解析失败:${err.message}`,
81
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
82
+ });
83
+ }
84
+ }
85
+ }
86
+ return { id, title: 'Specs 四域与 schema', findings };
87
+ }
@@ -0,0 +1,116 @@
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.checkStatePhases = checkStatePhases;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const utils_1 = require("./utils");
10
+ const PRE_SEAL_PHASES = [
11
+ 'S00_bootstrap',
12
+ 'S10_wiki',
13
+ 'S20_collect_inputs',
14
+ 'S30_convert_docs',
15
+ 'S40_extract_requirements',
16
+ 'S50_generate_specs',
17
+ 'S60_process_references',
18
+ 'S61_confirm_baseline_extraction',
19
+ 'S65_customize_agent_env',
20
+ 'S70_runtime_assets',
21
+ 'S80_seed_memory',
22
+ ];
23
+ async function checkStatePhases(ctx) {
24
+ const id = 'state-phases';
25
+ const findings = [];
26
+ const statePath = node_path_1.default.join(ctx.harnessRoot, '.harness-build-state.yaml');
27
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
28
+ if (!state) {
29
+ findings.push({
30
+ checkId: id,
31
+ severity: 'error',
32
+ message: '缺少 .harness-build-state.yaml,请先运行 svharness build',
33
+ path: (0, utils_1.rel)(ctx.harnessRoot, statePath),
34
+ });
35
+ return { id, title: '构建阶段状态', findings };
36
+ }
37
+ if (ctx.mode === 'pre-seal') {
38
+ for (const phaseId of PRE_SEAL_PHASES) {
39
+ const phase = state.phases[phaseId];
40
+ if (!phase) {
41
+ if (phaseId === 'S10_wiki')
42
+ continue;
43
+ findings.push({
44
+ checkId: id,
45
+ severity: 'error',
46
+ message: `阶段 ${phaseId} 未在状态文件中定义`,
47
+ });
48
+ continue;
49
+ }
50
+ if (phase.status !== 'DONE') {
51
+ findings.push({
52
+ checkId: id,
53
+ severity: 'error',
54
+ message: `阶段 ${phaseId} 状态为 ${phase.status},封存前须为 DONE`,
55
+ });
56
+ }
57
+ }
58
+ const s61 = state.phases.S61_confirm_baseline_extraction;
59
+ if (s61 && s61.baseline_auto_extract === 'UNSET') {
60
+ findings.push({
61
+ checkId: id,
62
+ severity: 'error',
63
+ message: 'S61_confirm_baseline_extraction.baseline_auto_extract 仍为 UNSET,须先确认',
64
+ });
65
+ }
66
+ }
67
+ for (const [phaseId, phase] of Object.entries(state.phases)) {
68
+ if (phase.status === 'FAILED' || phase.status === 'BLOCKED') {
69
+ findings.push({
70
+ checkId: id,
71
+ severity: 'error',
72
+ message: `阶段 ${phaseId} 处于 ${phase.status},须先解除`,
73
+ });
74
+ }
75
+ if (phaseId === 'S85_pre_seal_validation' && ctx.mode === 'pre-seal') {
76
+ // S85 is validated separately; not required DONE before doctor in pre-seal mode
77
+ }
78
+ if (phaseId === 'S90_finalize' && ctx.mode === 'pre-seal' && phase.status === 'DONE') {
79
+ findings.push({
80
+ checkId: id,
81
+ severity: 'warning',
82
+ message: 'S90_finalize 已为 DONE,doctor pre-seal 模式通常应在封板前执行',
83
+ });
84
+ }
85
+ }
86
+ if (!state.phases.S85_pre_seal_validation && ctx.mode === 'pre-seal') {
87
+ findings.push({
88
+ checkId: id,
89
+ severity: 'warning',
90
+ message: '状态文件缺少 S85_pre_seal_validation,请由 orchestrator 补丁插入后再封存',
91
+ });
92
+ }
93
+ if (state.phases.S40_extract_requirements?.status === 'DONE') {
94
+ const coveragePath = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'coverage-report.yaml');
95
+ if (!(await fs_extra_1.default.pathExists(coveragePath))) {
96
+ findings.push({
97
+ checkId: id,
98
+ severity: 'warning',
99
+ message: 'S40_extract_requirements 已 DONE,但未找到 requirements/coverage-report.yaml',
100
+ path: (0, utils_1.rel)(ctx.harnessRoot, coveragePath),
101
+ });
102
+ }
103
+ }
104
+ if (state.phases.S50_generate_specs?.status === 'DONE') {
105
+ const specCoveragePath = node_path_1.default.join(ctx.harnessRoot, 'specs', 'coverage-report.yaml');
106
+ if (!(await fs_extra_1.default.pathExists(specCoveragePath))) {
107
+ findings.push({
108
+ checkId: id,
109
+ severity: 'warning',
110
+ message: 'S50_generate_specs 已 DONE,但未找到 specs/coverage-report.yaml',
111
+ path: (0, utils_1.rel)(ctx.harnessRoot, specCoveragePath),
112
+ });
113
+ }
114
+ }
115
+ return { id, title: '构建阶段状态', findings };
116
+ }
@@ -0,0 +1,52 @@
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.checkWiki = checkWiki;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ async function checkWiki(ctx) {
11
+ const id = 'wiki';
12
+ const findings = [];
13
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
14
+ const wikiPhase = state?.phases.S10_wiki;
15
+ if (!wikiPhase) {
16
+ return { id, title: 'Baseline Wiki', findings };
17
+ }
18
+ if (wikiPhase.status !== 'DONE' && ctx.mode === 'pre-seal') {
19
+ findings.push({
20
+ checkId: id,
21
+ severity: 'error',
22
+ message: `S10_wiki 状态为 ${wikiPhase.status},封存前须为 DONE`,
23
+ });
24
+ return { id, title: 'Baseline Wiki', findings };
25
+ }
26
+ const wikiDir = node_path_1.default.join(ctx.harnessRoot, 'baseline', 'wiki');
27
+ const wikiPages = (await (0, utils_1.listRealFilesRecursive)(wikiDir, { extensions: ['.md'] })).filter((f) => !f.endsWith('TASKS.md') && !f.endsWith('README.md'));
28
+ if (wikiPhase.mode === 'outline-only' || wikiPhase.status === 'PENDING') {
29
+ const tasksFile = node_path_1.default.join(ctx.harnessRoot, wikiPhase.tasks_file ?? 'baseline/wiki/TASKS.md');
30
+ if (await fs_extra_1.default.pathExists(tasksFile)) {
31
+ const content = await fs_extra_1.default.readFile(tasksFile, 'utf8');
32
+ const openTasks = content.match(/- \[ \]/g);
33
+ if (openTasks && openTasks.length > 0) {
34
+ findings.push({
35
+ checkId: id,
36
+ severity: 'error',
37
+ message: `TASKS.md 仍有 ${openTasks.length} 项未完成 [ ]`,
38
+ path: (0, utils_1.rel)(ctx.harnessRoot, tasksFile),
39
+ });
40
+ }
41
+ }
42
+ }
43
+ if (wikiPhase.status === 'DONE' && wikiPages.length === 0) {
44
+ findings.push({
45
+ checkId: id,
46
+ severity: 'warning',
47
+ message: 'baseline/wiki/ 无实质 wiki 页面(除 TASKS/README)',
48
+ path: 'baseline/wiki',
49
+ });
50
+ }
51
+ return { id, title: 'Baseline Wiki', findings };
52
+ }