sdd-flow-kit 1.3.15 → 1.3.21

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 (60) hide show
  1. package/dist/cli.js +16 -0
  2. package/dist/core/acAssertionBinding.js +180 -0
  3. package/dist/core/apiFieldContract.js +90 -0
  4. package/dist/core/apiFieldCoverage.js +335 -0
  5. package/dist/core/componentSubstance.js +172 -0
  6. package/dist/core/demoImplementationCoverage.js +244 -0
  7. package/dist/core/e2eAssertionDepth.js +237 -0
  8. package/dist/core/e2eDemoSignalClicks.js +90 -0
  9. package/dist/core/prdArtifacts.js +12 -1
  10. package/dist/core/prdCoverage.js +52 -36
  11. package/dist/core/prdSemanticDiff.js +7 -4
  12. package/dist/core/snakeCamel.js +93 -0
  13. package/dist/core/syncSourcePrd.js +181 -0
  14. package/dist/core/techDocQuality.js +91 -0
  15. package/dist/core/testAssertionAnalysis.js +213 -0
  16. package/dist/core/visualRegression.js +243 -0
  17. package/dist/core/writeNext.js +1 -1
  18. package/dist/index.js +6 -1
  19. package/dist/steps/runGate.js +49 -5
  20. package/dist/steps/runSyncSourcePrd.js +38 -0
  21. package/dist/steps/runValidate.js +4 -2
  22. package/dist/steps/setupProject.js +3 -0
  23. package/dist/steps/startFlow.js +9 -2
  24. package/package.json +2 -1
  25. package/scripts/assert-active-openspec-change.sh +114 -0
  26. package/scripts/assess-ac-assertion-binding.mjs +104 -0
  27. package/scripts/assess-api-field-contract.mjs +119 -0
  28. package/scripts/assess-api-field-coverage.mjs +88 -0
  29. package/scripts/assess-component-substance.mjs +87 -0
  30. package/scripts/assess-demo-implementation-coverage.mjs +188 -0
  31. package/scripts/assess-e2e-assertion-depth.mjs +162 -0
  32. package/scripts/assess-playwright-e2e-coverage.mjs +244 -0
  33. package/scripts/assess-tech-doc-quality.mjs +98 -0
  34. package/scripts/assess-visual-regression.mjs +92 -0
  35. package/scripts/install-git-hooks.sh +27 -0
  36. package/scripts/lib/ac-assertion-binding.mjs +119 -0
  37. package/scripts/lib/api-field-coverage.mjs +238 -0
  38. package/scripts/lib/component-substance.mjs +117 -0
  39. package/scripts/lib/demo-reference-parse.mjs +55 -0
  40. package/scripts/lib/e2e-demo-signal-clicks.mjs +43 -0
  41. package/scripts/lib/snake-camel.mjs +74 -0
  42. package/scripts/lib/test-assertion-analysis.mjs +148 -0
  43. package/scripts/lib/visual-regression.mjs +191 -0
  44. package/scripts/resolve-playwright-e2e-scope.mjs +248 -0
  45. package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +6 -2
  46. package/src/templates/artifacts/02-/351/234/200/346/261/202/345/210/206/346/236/220/346/217/220/347/244/272/350/257/215.template.md +5 -0
  47. package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +22 -0
  48. package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +4 -3
  49. package/src/templates/artifacts/06-openspec-/346/217/220/346/241/210/350/215/211/347/250/277.template.md +2 -2
  50. package/src/templates/artifacts/07-opsx-auto-chain.template.md +2 -2
  51. package/src/templates/artifacts/08-PRD/344/270/200/350/207/264/346/200/247/345/244/215/346/237/245/346/212/245/345/221/212.template.md +2 -0
  52. package/src/templates/artifacts/design-demo-reference.template.md +24 -0
  53. package/src/templates/artifacts/tests-fixtures-readme.template.md +19 -0
  54. package/src/templates/artifacts/visual-baseline-readme.template.md +28 -0
  55. package/src/templates/prompts/02-sdd-loop-prompt.md +25 -11
  56. package/src/templates/prompts/04-tech-doc-fill-prompt.md +76 -0
  57. package/src/templates/skills/confluence-doc.py.template +64 -25
  58. package/src/templates/skills/demo-reference-mandatory-workflow.md +79 -0
  59. package/src/templates/skills/doc-skill.SKILL.md.template +13 -6
  60. package/src/templates/skills/layered-testing-strategy.md +15 -6
@@ -134,6 +134,7 @@ async function installAgentSkill(projectRoot, projectKind) {
134
134
  return;
135
135
  const template = await (0, templates_1.loadTemplateText)("artifacts/06-agent-skill.template.md");
136
136
  const layered = await (0, templates_1.loadTemplateText)("skills/layered-testing-strategy.md");
137
+ const demoRef = await (0, templates_1.loadTemplateText)("skills/demo-reference-mandatory-workflow.md");
137
138
  for (const target of targets) {
138
139
  const agentName = target.includes("/.cursor/")
139
140
  ? "cursor"
@@ -150,6 +151,8 @@ async function installAgentSkill(projectRoot, projectKind) {
150
151
  await (0, fs_1.writeTextFileEnsuredDir)(target, content);
151
152
  const layeredPath = path_1.default.join(path_1.default.dirname(target), "LAYERED-TESTING.md");
152
153
  await (0, fs_1.writeTextFileEnsuredDir)(layeredPath, layered);
154
+ const demoRefPath = path_1.default.join(path_1.default.dirname(target), "DEMO-REFERENCE-WORKFLOW.md");
155
+ await (0, fs_1.writeTextFileEnsuredDir)(demoRefPath, demoRef);
153
156
  }
154
157
  }
155
158
  async function installSddVersionTriggerRule(projectRoot, agent) {
@@ -10,6 +10,7 @@ const utils_1 = require("../core/utils");
10
10
  const fs_1 = require("../core/fs");
11
11
  const templates_1 = require("../core/templates");
12
12
  const inferProject_1 = require("../core/inferProject");
13
+ const syncSourcePrd_1 = require("../core/syncSourcePrd");
13
14
  const sessionState_1 = require("../core/sessionState");
14
15
  const projectRoots_1 = require("../core/projectRoots");
15
16
  const writeNext_1 = require("../core/writeNext");
@@ -30,12 +31,14 @@ async function startFlowScaffold(options) {
30
31
  // 1) Step1: doc-skill 指引(按产品解析脚本路径,支持 monorepo frontend)
31
32
  const kind = (0, inferProject_1.productToKind)(options.product);
32
33
  const docSkill = await (0, inferProject_1.resolveDocSkillDir)(options.projectRoot, kind);
34
+ const syncSourceDirRel = await (0, syncSourcePrd_1.resolveSyncSourceDirRel)(options.projectRoot, sourceDir, options.product);
33
35
  const step1Template = await (0, templates_1.loadTemplateText)("artifacts/01-adi-doc-skill-指引.template.md");
34
36
  const step1 = renderTemplate(step1Template, {
35
37
  PRODUCT: options.product,
36
38
  PRODUCT_PREFIX: inferProject_1.PRODUCT_PREFIX_BY_KIND[kind],
37
39
  DOC_SKILL_REL: docSkill.relFromRoot.replace(/\\/g, "/"),
38
40
  VERSION_EXAMPLE: options.version,
41
+ SYNC_SOURCE_DIR_REL: syncSourceDirRel,
39
42
  });
40
43
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "01-获取需求文档指引.md"), step1);
41
44
  // source 占位:让第 2 步提示词在任何工具里都能直接读
@@ -74,9 +77,13 @@ async function startFlowScaffold(options) {
74
77
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "02-改动点清单.md"), file02Template);
75
78
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "03-待确认问题清单.md"), file03Template);
76
79
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "05-验收清单.md"), file05Template);
77
- // 4) Step4 技术文档入口(草稿)
80
+ // 4) Step4 技术文档入口(完整模板 + 填写指引)
78
81
  const techDocTemplate = await (0, templates_1.loadTemplateText)("artifacts/04-技术文档.template.md");
79
82
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "04-技术文档草稿.md"), techDocTemplate);
83
+ const techDocFillGuide = await (0, templates_1.loadTemplateText)("prompts/04-tech-doc-fill-prompt.md");
84
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "04-技术文档生成指引.md"), techDocFillGuide);
85
+ const visualBaselineReadme = await (0, templates_1.loadTemplateText)("artifacts/visual-baseline-readme.template.md");
86
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "visual-baseline/README.md"), visualBaselineReadme);
80
87
  // 5) Step5 openspec 提案入口(草稿)
81
88
  const openspecEntryTemplate = await (0, templates_1.loadTemplateText)("artifacts/06-openspec-提案草稿.template.md");
82
89
  await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "06-openspec-提案草稿.md"), openspecEntryTemplate);
@@ -118,7 +125,7 @@ async function startFlowScaffold(options) {
118
125
  "用户确认后产出(顺序重要):",
119
126
  "- `03-待确认问题清单.md`(已闭合)",
120
127
  "- `05-验收清单.md`(从 `source/PRD.md` 逐条抽取 AC,标注「定义状态:已定义」)",
121
- "- `04-技术文档草稿.md`(引用 AC-ID)",
128
+ "- `04-技术文档草稿.md`(**在脚手架模板原文件上逐节填写**,见 `04-技术文档生成指引.md`;禁止摘要替换)",
122
129
  "- `01-需求分析报告.md`(业务概述,非验收 SSOT)",
123
130
  "- `02-改动点清单.md`",
124
131
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.3.15",
3
+ "version": "1.3.21",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "dist",
14
14
  "src/templates",
15
+ "scripts",
15
16
  "README.md"
16
17
  ],
17
18
  "bin": {
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # 校验:改动 src/ 或 e2e/ 时必须绑定活跃 OpenSpec change(见 AGENTS.md)
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ cd "$ROOT_DIR"
7
+
8
+ WATCH_PATHS=('src/' 'e2e/')
9
+ CHANGE_DIR_PREFIX="openspec/changes"
10
+ ACTIVE_CHANGE_FILES=('.opsx-active-change' '.poquan-active-change')
11
+
12
+ log() { printf '[opsx:gate] %s\n' "$*"; }
13
+ die() { log "ERROR: $*"; exit 1; }
14
+
15
+ path_is_watched() {
16
+ local path="$1"
17
+ local prefix
18
+ for prefix in "${WATCH_PATHS[@]}"; do
19
+ if [[ "$path" == "$prefix"* ]] || [[ "$path" == */"$prefix"* ]]; then
20
+ return 0
21
+ fi
22
+ done
23
+ return 1
24
+ }
25
+
26
+ collect_candidate_files() {
27
+ CANDIDATE_FILES=()
28
+ if [[ "${1:-}" == "--ci" ]]; then
29
+ local base="${GITHUB_BASE_REF:-main}"
30
+ base="${base#origin/}"
31
+ git fetch origin "$base" --depth=1 2>/dev/null || true
32
+ local merge_base
33
+ merge_base="$(git merge-base "origin/${base}" HEAD 2>/dev/null || git merge-base "${base}" HEAD 2>/dev/null || echo "")"
34
+ [[ -n "$merge_base" ]] || merge_base="HEAD~1"
35
+ while IFS= read -r line; do
36
+ [[ -n "$line" ]] && CANDIDATE_FILES+=("$line")
37
+ done < <(git diff --name-only "$merge_base"...HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD)
38
+ else
39
+ while IFS= read -r line; do
40
+ [[ -n "$line" ]] && CANDIDATE_FILES+=("$line")
41
+ done < <(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
42
+ if [[ ${#CANDIDATE_FILES[@]} -eq 0 ]]; then
43
+ while IFS= read -r line; do
44
+ [[ -n "$line" ]] && CANDIDATE_FILES+=("$line")
45
+ done < <(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null || true)
46
+ fi
47
+ fi
48
+ }
49
+
50
+ has_watched_changes() {
51
+ local file
52
+ for file in "${CANDIDATE_FILES[@]}"; do
53
+ if path_is_watched "$file"; then
54
+ return 0
55
+ fi
56
+ done
57
+ return 1
58
+ }
59
+
60
+ resolve_change_name() {
61
+ if [[ -n "${OPSX_ACTIVE_CHANGE:-}" ]]; then
62
+ echo "$OPSX_ACTIVE_CHANGE"
63
+ return 0
64
+ fi
65
+ if [[ -n "${POQUAN_ACTIVE_CHANGE:-}" ]]; then
66
+ echo "$POQUAN_ACTIVE_CHANGE"
67
+ return 0
68
+ fi
69
+ local file name
70
+ for file in "${ACTIVE_CHANGE_FILES[@]}"; do
71
+ if [[ -f "$file" ]]; then
72
+ name="$(tr -d '[:space:]' <"$file")"
73
+ [[ -n "$name" ]] && { echo "$name"; return 0; }
74
+ fi
75
+ done
76
+ local branch
77
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
78
+ if [[ "$branch" =~ ^change/([^/]+) ]]; then
79
+ echo "${BASH_REMATCH[1]}"
80
+ return 0
81
+ fi
82
+ return 1
83
+ }
84
+
85
+ validate_change_dir() {
86
+ local name="$1"
87
+ local dir="${CHANGE_DIR_PREFIX}/${name}"
88
+ [[ -d "$dir" ]] || die "change 目录不存在: ${dir}(请先 Propose 或设置 OPSX_ACTIVE_CHANGE)"
89
+ [[ "$dir" != *"/archive/"* ]] || die "change 已归档,请新建 change: ${name}"
90
+ if [[ -f "${dir}/tasks.md" ]] && grep -q '^- \[ \]' "${dir}/tasks.md" 2>/dev/null; then
91
+ log "WARN: tasks.md 仍有未完成项;合码前建议全部勾选并跑 delivery-pipeline"
92
+ fi
93
+ }
94
+
95
+ main() {
96
+ if [[ "${OPSX_SKIP_OPSX:-}" == "1" ]] || [[ "${POQUAN_SKIP_OPSX:-}" == "1" ]]; then
97
+ log "已跳过(OPSX_SKIP_OPSX 或 POQUAN_SKIP_OPSX=1)"
98
+ exit 0
99
+ fi
100
+
101
+ collect_candidate_files "${1:-}"
102
+ if ! has_watched_changes; then
103
+ log "无 src/ 或 e2e/ 改动,跳过"
104
+ exit 0
105
+ fi
106
+
107
+ local change_name
108
+ change_name="$(resolve_change_name)" || die "改动 src/e2e 但未绑定 OpenSpec change。请先 Propose,或设置 OPSX_ACTIVE_CHANGE / .opsx-active-change / 分支 change/<name>。详见 AGENTS.md"
109
+
110
+ validate_change_dir "$change_name"
111
+ log "OK — 活跃 change: ${change_name}"
112
+ }
113
+
114
+ main "$@"
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AC 断言绑定门禁:P0/e2e AC 的验收标准字面量须出现在 test 块 expect 中。
4
+ *
5
+ * 用法:
6
+ * node scripts/assess-ac-assertion-binding.mjs --run-dir openspec/PRD/<runId>
7
+ * node scripts/assess-ac-assertion-binding.mjs --run-dir <path> --change <name>(可选)
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import {
13
+ checkAcAssertionBinding,
14
+ parseAcMatrixFrom05,
15
+ } from "./lib/ac-assertion-binding.mjs";
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const TEST_FILE_RE = /\.(spec|test)\.(t|j)sx?$/i;
19
+
20
+ function parseArgs(argv) {
21
+ let runDir = "";
22
+ for (let i = 2; i < argv.length; i += 1) {
23
+ const a = argv[i];
24
+ if (a === "--run-dir" && argv[i + 1]) runDir = argv[++i];
25
+ else if (!a.startsWith("-") && !runDir) runDir = a;
26
+ }
27
+ return { runDir };
28
+ }
29
+
30
+ function fail(messages) {
31
+ console.error("[assess-ac-assertion-binding] FAIL");
32
+ for (const m of messages) console.error(` - ${m}`);
33
+ console.error(
34
+ " 修复: Red 阶段从 05「验收标准」复制完整文案到 expect;禁止 toContain('成功') 等模糊断言",
35
+ );
36
+ process.exit(1);
37
+ }
38
+
39
+ function walkTests(dir, out, relPrefix = "") {
40
+ if (!fs.existsSync(dir)) return;
41
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ if (ent.name === "node_modules" || ent.name === ".git" || ent.name === "dist") continue;
43
+ const full = path.join(dir, ent.name);
44
+ const rel = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
45
+ if (ent.isDirectory()) walkTests(full, out, rel);
46
+ else if (TEST_FILE_RE.test(ent.name)) {
47
+ out.push({ rel, content: fs.readFileSync(full, "utf8") });
48
+ }
49
+ }
50
+ }
51
+
52
+ function collectTestFiles() {
53
+ const files = [];
54
+ for (const r of ["e2e", "tests", "test", "__tests__", "playwright", "src"]) {
55
+ walkTests(path.join(ROOT, r), files, r);
56
+ }
57
+ return files;
58
+ }
59
+
60
+ function resolveRunDir(arg) {
61
+ if (!arg) return "";
62
+ if (path.isAbsolute(arg)) return arg;
63
+ const candidates = [
64
+ path.join(ROOT, arg),
65
+ path.join(ROOT, "openspec/PRD", arg),
66
+ path.join(ROOT, "frontend/openspec/PRD", arg),
67
+ ];
68
+ for (const c of candidates) {
69
+ if (fs.existsSync(path.join(c, "05-验收清单.md"))) return c;
70
+ }
71
+ return path.join(ROOT, arg);
72
+ }
73
+
74
+ function main() {
75
+ const skip = process.env.OPSX_SKIP_AC_ASSERTION_BINDING;
76
+ if (skip === "1") {
77
+ console.warn("[assess-ac-assertion-binding] SKIP");
78
+ process.exit(0);
79
+ }
80
+
81
+ const { runDir: runDirArg } = parseArgs(process.argv);
82
+ const runDir = resolveRunDir(runDirArg);
83
+ const ac05Path = path.join(runDir, "05-验收清单.md");
84
+ if (!fs.existsSync(ac05Path)) {
85
+ console.error("[assess-ac-assertion-binding] 缺少 --run-dir(须含 05-验收清单.md)");
86
+ process.exit(2);
87
+ }
88
+
89
+ const items = parseAcMatrixFrom05(fs.readFileSync(ac05Path, "utf8"));
90
+ if (items.length === 0) {
91
+ console.error("[assess-ac-assertion-binding] 05 未解析到 AC 行");
92
+ process.exit(2);
93
+ }
94
+
95
+ const testFiles = collectTestFiles();
96
+ const errors = checkAcAssertionBinding(items, testFiles);
97
+ if (errors.length > 0) fail(errors);
98
+
99
+ console.log(
100
+ `[assess-ac-assertion-binding] PASS run=${path.basename(runDir)} ac=${items.length} tests=${testFiles.length}`,
101
+ );
102
+ }
103
+
104
+ main();
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * API 字段契约门禁:change 引用的 config/api/modules 须含 snake/camel normalize(防 SNAKE_CASE 漏映射)。
4
+ *
5
+ * 用法: node scripts/assess-api-field-contract.mjs --change <change-name>
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const NORMALIZE_MARKERS =
13
+ /snakeToCamel|camelToSnake|normalizeFinanceApiResult|normalize\w*Api|caseTransform|toCamelCase|toSnakeCase/i;
14
+ const API_CLIENT_CALL_RE = /\b(?:apiClient|request|post|get)\s*[.(]/i;
15
+
16
+ function parseArgs(argv) {
17
+ let change = "";
18
+ for (let i = 2; i < argv.length; i += 1) {
19
+ const a = argv[i];
20
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
21
+ else if (!a.startsWith("-") && !change) change = a;
22
+ }
23
+ return { change };
24
+ }
25
+
26
+ function readChangeBlob(changeDir) {
27
+ let blob = "";
28
+ for (const f of ["tasks.md", "proposal.md", "design.md"]) {
29
+ const fp = path.join(changeDir, f);
30
+ if (!fs.existsSync(fp)) continue;
31
+ blob += fs.readFileSync(fp, "utf8");
32
+ }
33
+ return blob;
34
+ }
35
+
36
+ function resolveModuleFile(mod) {
37
+ const candidates = [
38
+ path.join(ROOT, "src/config/api/modules", mod, "index.ts"),
39
+ path.join(ROOT, "frontend/src/config/api/modules", mod, "index.ts"),
40
+ path.join(ROOT, "src/config/api/modules", `${mod}.ts`),
41
+ path.join(ROOT, "frontend/src/config/api/modules", `${mod}.ts`),
42
+ ];
43
+ for (const fp of candidates) {
44
+ if (fs.existsSync(fp)) return fp;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function fail(messages) {
50
+ console.error("[assess-api-field-contract] FAIL");
51
+ for (const msg of messages) console.error(` - ${msg}`);
52
+ console.error(
53
+ " 修复: 在 config/api/modules/<domain> 对请求体 camelToSnake、响应 data snakeToCamel;参考 finance/caseTransform.ts",
54
+ );
55
+ process.exit(1);
56
+ }
57
+
58
+ function main() {
59
+ const skip =
60
+ process.env.POQUAN_SKIP_API_FIELD_CONTRACT ||
61
+ process.env.OPSX_SKIP_API_FIELD_CONTRACT;
62
+ if (skip === "1") {
63
+ console.warn("[assess-api-field-contract] SKIP");
64
+ process.exit(0);
65
+ }
66
+
67
+ const { change } = parseArgs(process.argv);
68
+ if (!change) {
69
+ console.error("[assess-api-field-contract] 缺少 --change <name>");
70
+ process.exit(2);
71
+ }
72
+
73
+ const changeDir = path.join(ROOT, "openspec/changes", change);
74
+ if (!fs.existsSync(changeDir)) {
75
+ console.error(`[assess-api-field-contract] change 不存在: openspec/changes/${change}`);
76
+ process.exit(2);
77
+ }
78
+
79
+ const blob = readChangeBlob(changeDir);
80
+ const moduleRefs = new Set();
81
+ const modRe = /config\/api\/modules\/([\w/-]+)/g;
82
+ let m;
83
+ while ((m = modRe.exec(blob)) !== null) {
84
+ moduleRefs.add(m[1].replace(/\/index$/, "").replace(/\/$/, ""));
85
+ }
86
+
87
+ if (moduleRefs.size === 0) {
88
+ console.log(`[assess-api-field-contract] PASS(无 API 模块引用) change=${change}`);
89
+ process.exit(0);
90
+ }
91
+
92
+ const errors = [];
93
+ const passed = [];
94
+ for (const mod of moduleRefs) {
95
+ const fp = resolveModuleFile(mod);
96
+ if (!fp) {
97
+ errors.push(`config/api/modules/${mod} 引用但文件不存在`);
98
+ continue;
99
+ }
100
+ const rel = path.relative(ROOT, fp).replace(/\\/g, "/");
101
+ const content = fs.readFileSync(fp, "utf8");
102
+ if (!API_CLIENT_CALL_RE.test(content)) {
103
+ passed.push(`${rel}(无 HTTP 调用)`);
104
+ continue;
105
+ }
106
+ if (!NORMALIZE_MARKERS.test(content)) {
107
+ errors.push(`${rel} 含 API 调用但未发现字段 normalize`);
108
+ } else {
109
+ passed.push(rel);
110
+ }
111
+ }
112
+
113
+ if (errors.length > 0) fail(errors);
114
+ console.log(
115
+ `[assess-api-field-contract] PASS change=${change} modules=${passed.join(", ")}`,
116
+ );
117
+ }
118
+
119
+ main();
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * API 字段级覆盖门禁(v1.3.20):fixture 样例 + 04 字段映射表 + 源码消费点。
4
+ *
5
+ * 用法:
6
+ * node scripts/assess-api-field-coverage.mjs --change <change-name> [--run-dir openspec/PRD/runId]
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { assessApiFieldCoverage } from "./lib/api-field-coverage.mjs";
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ function parseArgs(argv) {
16
+ let change = "";
17
+ let runDir = "";
18
+ for (let i = 2; i < argv.length; i += 1) {
19
+ const a = argv[i];
20
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
21
+ else if (a === "--run-dir" && argv[i + 1]) runDir = argv[++i];
22
+ else if (!a.startsWith("-") && !change) change = a;
23
+ }
24
+ return { change, runDir };
25
+ }
26
+
27
+ function resolveRunDir(arg) {
28
+ if (!arg) return "";
29
+ const candidates = [
30
+ path.isAbsolute(arg) ? arg : path.join(ROOT, arg),
31
+ path.join(ROOT, "openspec/PRD", arg),
32
+ path.join(ROOT, "frontend/openspec/PRD", arg),
33
+ ];
34
+ for (const c of candidates) {
35
+ if (fs.existsSync(path.join(c, "04-技术文档草稿.md"))) return c;
36
+ }
37
+ return candidates[0];
38
+ }
39
+
40
+ function fail(messages) {
41
+ console.error("[assess-api-field-coverage] FAIL");
42
+ for (const m of messages) console.error(` - ${m}`);
43
+ console.error(
44
+ " 修复: 1) 04 §4.3.1 补字段映射表 2) tests/fixtures/<module>/*.json 联调样例 3) caseTransform 单测加载 fixture",
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ function main() {
50
+ const skip =
51
+ process.env.POQUAN_SKIP_API_FIELD_COVERAGE ||
52
+ process.env.OPSX_SKIP_API_FIELD_COVERAGE;
53
+ if (skip === "1") {
54
+ console.warn("[assess-api-field-coverage] SKIP");
55
+ process.exit(0);
56
+ }
57
+
58
+ const { change, runDir: runDirArg } = parseArgs(process.argv);
59
+ if (!change) {
60
+ console.error("[assess-api-field-coverage] 缺少 --change <name>");
61
+ process.exit(2);
62
+ }
63
+
64
+ const changeDir = path.join(ROOT, "openspec/changes", change);
65
+ if (!fs.existsSync(changeDir)) {
66
+ console.error(`[assess-api-field-coverage] change 不存在: openspec/changes/${change}`);
67
+ process.exit(2);
68
+ }
69
+
70
+ let fieldDoc = "";
71
+ const runDir = resolveRunDir(runDirArg);
72
+ if (runDir && fs.existsSync(path.join(runDir, "04-技术文档草稿.md"))) {
73
+ fieldDoc += fs.readFileSync(path.join(runDir, "04-技术文档草稿.md"), "utf8");
74
+ }
75
+ const designPath = path.join(changeDir, "design.md");
76
+ if (fs.existsSync(designPath)) {
77
+ fieldDoc += fs.readFileSync(designPath, "utf8");
78
+ }
79
+
80
+ const { errors, passed } = assessApiFieldCoverage(ROOT, change, fieldDoc);
81
+ if (errors.length > 0) fail(errors);
82
+
83
+ console.log(
84
+ `[assess-api-field-coverage] PASS change=${change} ${passed.join("; ")}`,
85
+ );
86
+ }
87
+
88
+ main();
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 组件实质门禁:演示映射的生产组件须非空壳,子组件须被父组件引用。
4
+ *
5
+ * 用法: node scripts/assess-component-substance.mjs --change <change-name>
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import {
11
+ analyzeComponentSubstance,
12
+ checkChildComponentsImported,
13
+ } from "./lib/component-substance.mjs";
14
+ import { parseDemoReferenceSection } from "./lib/demo-reference-parse.mjs";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+
18
+ function parseArgs(argv) {
19
+ let change = "";
20
+ for (let i = 2; i < argv.length; i += 1) {
21
+ const a = argv[i];
22
+ if (a === "--change" && argv[i + 1]) change = argv[++i];
23
+ else if (!a.startsWith("-") && !change) change = a;
24
+ }
25
+ return { change };
26
+ }
27
+
28
+ function fail(messages) {
29
+ console.error("[assess-component-substance] FAIL");
30
+ for (const m of messages) console.error(` - ${m}`);
31
+ console.error(
32
+ " 修复: 按 design 映射实现真实子组件(template+API/emit),并在父 Drawer 中 import;禁止注释塞关键词",
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ async function main() {
38
+ const skip = process.env.OPSX_SKIP_COMPONENT_SUBSTANCE;
39
+ if (skip === "1") {
40
+ console.warn("[assess-component-substance] SKIP");
41
+ process.exit(0);
42
+ }
43
+
44
+ const { change } = parseArgs(process.argv);
45
+ if (!change) {
46
+ console.error("[assess-component-substance] 缺少 --change <name>");
47
+ process.exit(2);
48
+ }
49
+
50
+ const designPath = path.join(ROOT, "openspec/changes", change, "design.md");
51
+ if (!fs.existsSync(designPath)) {
52
+ console.error(`[assess-component-substance] design.md 不存在: ${change}`);
53
+ process.exit(2);
54
+ }
55
+
56
+ const design = fs.readFileSync(designPath, "utf8");
57
+ const parsed = parseDemoReferenceSection(design);
58
+ if (parsed.exempt || parsed.mappings.length === 0) {
59
+ console.log("[assess-component-substance] PASS(无演示映射)");
60
+ process.exit(0);
61
+ }
62
+
63
+ const errors = [];
64
+ for (const m of parsed.mappings) {
65
+ const rel = m.productionPath.replace(/^\.\//, "");
66
+ const abs = path.join(ROOT, rel);
67
+ if (!fs.existsSync(abs)) {
68
+ errors.push(`${m.moduleName}: 文件不存在 ${rel}`);
69
+ continue;
70
+ }
71
+ const content = fs.readFileSync(abs, "utf8");
72
+ const result = analyzeComponentSubstance(rel, content, m.interactionSignals);
73
+ if (!result.ok) {
74
+ errors.push(`${m.moduleName}(${rel}): ${result.issues.join("; ")}`);
75
+ }
76
+ }
77
+
78
+ const importFailures = await checkChildComponentsImported(ROOT, parsed.mappings);
79
+ errors.push(...importFailures);
80
+
81
+ if (errors.length > 0) fail(errors);
82
+ console.log(
83
+ `[assess-component-substance] PASS change=${change} modules=${parsed.mappings.length}`,
84
+ );
85
+ }
86
+
87
+ main();