@chenmk/superflow 0.5.16 → 0.5.18

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 (48) hide show
  1. package/README.md +18 -0
  2. package/README.zh-CN.md +18 -0
  3. package/assets/manifest.json +29 -0
  4. package/assets/scripts/install-sql-pre-commit.sh +54 -39
  5. package/assets/scripts/superflow-delivery-check.sh +62 -11
  6. package/assets/scripts/superflow-integration-evidence-hook.sh +1 -1
  7. package/assets/scripts/superflow-managed-executor-preflight.mjs +30 -0
  8. package/assets/scripts/superflow-managed-work-check.mjs +35 -0
  9. package/assets/scripts/superflow-review-coverage.mjs +166 -0
  10. package/assets/scripts/superflow-test-report-lint.py +290 -13
  11. package/assets/scripts/superflow-verify-integration.sh +4 -9
  12. package/assets/skills/superflow-design/SKILL.md +6 -0
  13. package/assets/skills/superflow-docs/SKILL.md +9 -0
  14. package/assets/skills/superflow-implement/SKILL.md +9 -0
  15. package/assets/skills/superflow-pipeline/references/document-review-coverage.md +60 -3
  16. package/assets/skills/superflow-pipeline/references/test-execution-template.md +3 -1
  17. package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +9 -5
  18. package/assets/skills/superflow-table-impact-analysis/SKILL.md +6 -0
  19. package/assets/skills/superflow-verify/SKILL.md +9 -0
  20. package/assets/skills-en/superflow-design/SKILL.md +6 -0
  21. package/assets/skills-en/superflow-docs/SKILL.md +9 -0
  22. package/assets/skills-en/superflow-implement/SKILL.md +9 -0
  23. package/assets/skills-en/superflow-pipeline/references/document-review-coverage.md +72 -3
  24. package/assets/skills-en/superflow-pipeline/references/test-execution-template.md +3 -1
  25. package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +9 -5
  26. package/assets/skills-en/superflow-table-impact-analysis/SKILL.md +6 -0
  27. package/assets/skills-en/superflow-verify/SKILL.md +9 -0
  28. package/dist/app/cli.js +11 -11
  29. package/dist/app/cli.js.map +1 -1
  30. package/dist/app/commands/change-guard.js +4 -0
  31. package/dist/app/commands/change-guard.js.map +1 -1
  32. package/dist/app/commands/doctor.js +36 -2
  33. package/dist/app/commands/doctor.js.map +1 -1
  34. package/dist/app/commands/hook-migrate.js +48 -8
  35. package/dist/app/commands/hook-migrate.js.map +1 -1
  36. package/dist/app/commands/update.js +8 -6
  37. package/dist/app/commands/update.js.map +1 -1
  38. package/dist/domains/config/cli-help.js +12 -2
  39. package/dist/domains/config/cli-help.js.map +1 -1
  40. package/dist/domains/config/manifest.js +1 -1
  41. package/dist/domains/config/manifest.js.map +1 -1
  42. package/dist/domains/hook-migration.js +70 -5
  43. package/dist/domains/hook-migration.js.map +1 -1
  44. package/dist/domains/hook.js +67 -23
  45. package/dist/domains/hook.js.map +1 -1
  46. package/dist/domains/skill/scripts.js +140 -11
  47. package/dist/domains/skill/scripts.js.map +1 -1
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,24 @@
31
31
  SuperBridge Flow is a workflow CLI for bringing OpenSpec/SDD and Superpowers
32
32
  together in real software delivery.
33
33
 
34
+ ## 0.5.18 database layers and Git gate closure
35
+
36
+ - Preserve Mock logic tests while freezing separate SQL-binding, real database and HTTP-entry obligations. Required MySQL proof cannot be replaced with Mock/H2/SQLite.
37
+ - Bind receipts to engine/version, schema, production statement and actual row observations. Retain historical evidence for its verified revision.
38
+ - Delegate exact known legacy sdd entries to current canonical scripts after backup. Preserve unknown/custom entries and symlinks.
39
+ - Audit effective Git Hook routes, executable state and current package content. Reject root report false PASS and tracked evidence/index mismatches, while keeping private untracked logs local.
40
+
41
+ Run `superflow update --scope global --agent both`, then `superflow doctor <project>`. Unknown custom routes remain visible for owner review; filename/date is not proof of integration.
42
+
43
+ ## 0.5.17 acceptance evidence fixes
44
+
45
+ - Per-case PASS binds retained receipts and raw output to the service entry and current source/build. Empty files, remote links, zero execution and substituted entries no longer pass.
46
+ - Persisted fields freeze before/after values for the same business ID and expected assertions. Independent review still checks the actual Service-to-Mapper statement.
47
+ - Keep partial results; only a complete final summary authorizes delivery. Lightweight documentation acquires no DB requirement.
48
+ - CLI and Codex/Claude scripts use the same installation source. Repeated updates refresh version records and preserve custom state.
49
+
50
+ Complete affected evidence for legacy reports. Hashes prove identity, not business correctness or authenticity.
51
+
34
52
  ## Why SuperBridge Flow
35
53
 
36
54
  Modern agentic coding often fails in the gap between a well-written spec and
package/README.zh-CN.md CHANGED
@@ -47,6 +47,24 @@ CLI 会把 SuperBridge Flow 技能、配套 hook/command 脚本整合为单一 n
47
47
  - Claude Code:`~/.claude/skills/`、`~/.claude/scripts/`,并注册 `~/.claude/settings.json` hook
48
48
  - Codex:`~/.codex/skills/`、`~/.codex/hooks/`
49
49
 
50
+ ## 0.5.18 数据库分层与 Git 门禁接入
51
+
52
+ - 保留 Mock 逻辑单测,分别冻结 SQL 生成/绑定、真实数据库、HTTP 入口义务;必测 MySQL 不能由 Mock/H2/SQLite 替代。
53
+ - 凭证绑定引擎版本、schema、生产 statement 和实际行观测;历史证据按已验证源码版本保留。
54
+ - 已确认旧 sdd 入口备份后委托当前规范脚本;未知/自定义入口和符号链接保留。
55
+ - 核验实际 Git Hook 路由、执行权限及当前包内容;拒绝根级报告假 PASS 和 tracked 证据/index 错配,私有未跟踪日志仍可留本地。
56
+
57
+ 运行 `superflow update --scope global --agent both` 后,用 `superflow doctor <项目>` 核对实际接入。未知自定义路由明确交给 owner 核验,不能只看文件名或日期。
58
+
59
+ ## 0.5.17 验收证据修复
60
+
61
+ - 逐用例 PASS 绑定本地执行凭证、原始输出、具体服务入口及当前源码/构建;空文件、远端链接、零执行和入口替代不再通过。
62
+ - 持久化字段需冻结同业务 ID 前后值与预期断言;独立评审继续核对实际 Service→Mapper statement。
63
+ - 局部结果保留 PARTIAL,最终完整汇总才可交付;轻量文档不追加数据库要求。
64
+ - CLI、Codex/Claude 脚本使用同一安装来源,重复更新刷新版本记录并保留自定义状态。
65
+
66
+ 旧报告缺执行凭证时补齐受影响证据;哈希只证明身份,不证明业务正确性或防伪。
67
+
50
68
  ## 亮点
51
69
 
52
70
  0.5.10 加强文档交付:逐需求×真实入口核对(含排除项)、真实入口验收合同、
@@ -86,5 +86,34 @@
86
86
  "codex": [
87
87
  "codex-auto-backup-hook.sh"
88
88
  ]
89
+ },
90
+ "legacyScriptAliases": {
91
+ "sdd-delivery-check.sh": {
92
+ "target": "superflow-delivery-check.sh",
93
+ "knownHashes": [
94
+ "e536d7f745dc864efe28549831d2367a6e966f68cad299a7eaf1090794531a2f",
95
+ "8c44bb6f9f7f00590661663b94f75b879f5de88cdb85e22419b68dce9e308f77"
96
+ ]
97
+ },
98
+ "sdd-test-report-lint.py": {
99
+ "target": "superflow-test-report-lint.py",
100
+ "knownHashes": [
101
+ "34f1fa898042277c7ff78af71806346f8ffc208202b12a97ce5ca363ca0aeeaf"
102
+ ]
103
+ },
104
+ "sdd-verify-integration.sh": {
105
+ "target": "superflow-verify-integration.sh",
106
+ "knownHashes": [
107
+ "5ef4f28456aa7f722c7de4a59f8d82e9908433bbc5e90651eb65d1241bd1f1b5",
108
+ "b6520e2fe3e6980fdef8cbb0af5fb301a69a390593545137c38a8c5ea2a8c888"
109
+ ]
110
+ },
111
+ "sdd-sql-sync-hook.py": {
112
+ "target": "superflow-sql-sync-hook.py",
113
+ "knownHashes": [
114
+ "f12c036291a8048649d03aefd9be17fe9205d17d149d456ce9c8128b62dc96fb",
115
+ "d128efa960c86dc642d06cd454f5704eb8906c5628c4e2a92ba41278f92a8b5b"
116
+ ]
117
+ }
89
118
  }
90
119
  }
@@ -1,44 +1,59 @@
1
1
  #!/bin/sh
2
-
3
- set -e
4
-
5
- REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
6
- if [ -z "$REPO_ROOT" ]; then
7
- echo "当前目录不是 Git 仓库,无法安装 SQL pre-commit"
8
- exit 1
2
+ # Install the existing SQL and evidence gates; retain unknown/custom Git hooks.
3
+ set -eu
4
+
5
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
6
+ case "$SCRIPT_DIR" in */.claude/scripts) AGENT=claude ;; *) AGENT=codex ;; esac
7
+ if [ "$#" -gt 0 ]; then
8
+ [ "$#" -eq 2 ] && [ "$1" = "--agent" ] || {
9
+ echo "Usage: install-sql-pre-commit.sh [--agent codex|claude]" >&2
10
+ exit 2
11
+ }
12
+ AGENT="$2"
9
13
  fi
10
-
11
- HOOK_DIR="$REPO_ROOT/.git/hooks"
12
- HOOK_FILE="$HOOK_DIR/pre-commit"
13
- SQL_HOOK="$HOME/.codex/hooks/superflow-sql-sync-hook.py"
14
-
15
- mkdir -p "$HOOK_DIR"
16
-
17
- if [ -f "$HOOK_FILE" ] && ! grep -q "superflow-sql-sync-hook.py" "$HOOK_FILE"; then
18
- BACKUP_FILE="$HOOK_FILE.backup.$(date +%Y%m%d%H%M%S)"
19
- cp "$HOOK_FILE" "$BACKUP_FILE"
20
- {
21
- printf "\n"
22
- printf "# SDD SQL style check\n"
23
- printf "SQL_HOOK=\"\\$HOME/.codex/hooks/superflow-sql-sync-hook.py\"\n"
24
- printf "if [ -x \"\\$SQL_HOOK\" ]; then\n"
25
- printf " \"\\$SQL_HOOK\" --check-staged\n"
26
- printf "fi\n"
27
- } >> "$HOOK_FILE"
28
- chmod +x "$HOOK_FILE"
29
- echo "已追加 SQL pre-commit,并备份原 hook: $BACKUP_FILE"
30
- exit 0
14
+ case "$AGENT" in codex) SUBDIR=hooks ;; claude) SUBDIR=scripts ;; *) exit 2 ;; esac
15
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || {
16
+ echo "Not a Git repository / 当前目录不是 Git 仓库" >&2
17
+ exit 2
18
+ }
19
+ HOOK_PATH=$(git -C "$REPO_ROOT" rev-parse --git-path hooks/pre-commit)
20
+ case "$HOOK_PATH" in /*) HOOK_FILE="$HOOK_PATH" ;; *) HOOK_FILE="$REPO_ROOT/$HOOK_PATH" ;; esac
21
+ if [ "$SCRIPT_DIR" = "$REPO_ROOT/.$AGENT/$SUBDIR" ]; then
22
+ SCRIPTS="$SCRIPT_DIR"
23
+ EXPRESSION='$(git rev-parse --show-toplevel)'"/.$AGENT/$SUBDIR"
24
+ else
25
+ SCRIPTS="$HOME/.$AGENT/$SUBDIR"
26
+ EXPRESSION='$HOME'"/.$AGENT/$SUBDIR"
31
27
  fi
32
-
33
- cat > "$HOOK_FILE" <<'EOF'
28
+ for name in superflow-sql-sync-hook.py superflow-delivery-check.sh superflow-test-report-lint.py; do
29
+ [ -x "$SCRIPTS/$name" ] || {
30
+ echo "Missing current gate: $name; run superflow update / 请先更新规范门禁" >&2
31
+ exit 2
32
+ }
33
+ done
34
+ [ ! -L "$HOOK_FILE" ] || { echo "Retain custom symlink / 保留自定义符号链接" >&2; exit 2; }
35
+ mkdir -p "$(dirname "$HOOK_FILE")"
36
+ TEMPORARY=$(mktemp "${HOOK_FILE}.superflow-XXXXXX")
37
+ trap 'rm -f "$TEMPORARY"' EXIT HUP INT TERM
38
+ cat > "$TEMPORARY" <<HOOK
34
39
  #!/bin/sh
35
-
36
- SQL_HOOK="$HOME/.codex/hooks/superflow-sql-sync-hook.py"
37
-
38
- if [ -x "$SQL_HOOK" ]; then
39
- "$SQL_HOOK" --check-staged
40
+ # Superflow managed Git evidence gate v1
41
+ SCRIPTS="$EXPRESSION"
42
+ for name in superflow-sql-sync-hook.py superflow-delivery-check.sh; do
43
+ [ -x "\$SCRIPTS/\$name" ] || { echo "Missing Superflow gate; run superflow update" >&2; exit 2; }
44
+ "\$SCRIPTS/\$name" --check-staged "\$(pwd)" || exit \$?
45
+ done
46
+ HOOK
47
+ if [ -f "$HOOK_FILE" ]; then
48
+ if cmp -s "$HOOK_FILE" "$TEMPORARY"; then
49
+ chmod +x "$HOOK_FILE"
50
+ echo "Git gate already current / Git 门禁已接入"
51
+ exit 0
52
+ fi
53
+ echo "Existing custom/legacy pre-commit retained / 已保留现有自定义或旧 pre-commit" >&2
54
+ echo "Run superflow hook-audit for source diagnostics / 请先核对现有入口来源" >&2
55
+ exit 2
40
56
  fi
41
- EOF
42
-
43
- chmod +x "$HOOK_FILE"
44
- echo "已安装 SQL pre-commit: $HOOK_FILE"
57
+ chmod +x "$TEMPORARY"
58
+ mv "$TEMPORARY" "$HOOK_FILE"
59
+ echo "Installed Git gate / 已安装 Git 门禁: $HOOK_FILE"
@@ -9,6 +9,10 @@
9
9
 
10
10
  set -u
11
11
 
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ LINT="$SCRIPT_DIR/superflow-test-report-lint.py"
14
+ INTEGRATION="$SCRIPT_DIR/superflow-verify-integration.sh"
15
+
12
16
  MODE="${1:-}"
13
17
 
14
18
  read_json_field() {
@@ -108,6 +112,18 @@ if [ "$SDD_ACTIVE" -eq 0 ] && [ -z "$SDD_DOCS_CHANGED" ]; then
108
112
  fi
109
113
 
110
114
  FAILED=0
115
+ UNSTAGED="$(git -C "$REPO_ROOT" diff --name-only)"
116
+ INDEX_MISMATCH=0
117
+ while IFS= read -r staged_path; do
118
+ [ -n "$staged_path" ] || continue
119
+ if printf '%s\n' "$UNSTAGED" | grep -Fxq "$staged_path"; then
120
+ echo "FAIL [$staged_path] 工作树与 Git index 内容不同,证据不能证明待提交快照"
121
+ INDEX_MISMATCH=1
122
+ fi
123
+ done <<STAGED_PATHS
124
+ $ACTIVE_SDD_CHANGED
125
+ STAGED_PATHS
126
+ [ "$INDEX_MISMATCH" -eq 0 ] || exit 2
111
127
 
112
128
  fail() {
113
129
  echo "FAIL $1"
@@ -200,6 +216,24 @@ is_blocked_doc_freeze_report() {
200
216
  return 1
201
217
  fi
202
218
 
219
+ local final_result
220
+ final_result="$(python3 - "$LINT" "$file" <<'PYFINAL'
221
+ import runpy, sys
222
+ from pathlib import Path
223
+ module = runpy.run_path(sys.argv[1])
224
+ text = Path(sys.argv[2]).read_text()
225
+ lines = text.splitlines()
226
+ header = next((i for i, line in enumerate(lines) if module["CASE_EVIDENCE_HEADER"].fullmatch(line.strip())), None)
227
+ active_pass = False
228
+ if header is not None:
229
+ for line in lines[header + 2:]:
230
+ if not line.startswith("|"): break
231
+ cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
232
+ if len(cells) == 5 and cells[3].upper() == "PASS": active_pass = True
233
+ print("CASE_PASS" if active_pass else module["final_report_result"](text))
234
+ PYFINAL
235
+ )" || return 1
236
+ case "$final_result" in PASS|CONFLICT|CASE_PASS) return 1 ;; esac
203
237
  grep -Eiq 'Blocked|阻塞' "$file" \
204
238
  && grep -Eiq 'SDD 文档|文档阶段|范围收口|范围冻结|任务冻结|不包含代码实现|没有 Java 实现变更|未进入实现|当前报告记录可交付边界' "$file"
205
239
  }
@@ -249,12 +283,12 @@ if [ -n "$P_DIRS" ]; then
249
283
  ok "[$rel_report] 已 staged"
250
284
  check_no_open_placeholders "$REPO_ROOT/$rel_report"
251
285
  check_has_closeout_status "$REPO_ROOT/$rel_report"
252
- if [ -x "$HOME/.codex/hooks/superflow-test-report-lint.py" ]; then
253
- lint_args=(--repo-root "$REPO_ROOT")
286
+ if [ -x "$LINT" ]; then
287
+ lint_args=(--repo-root "$REPO_ROOT" --git-index-root "$REPO_ROOT")
254
288
  if [ -f "$REPO_ROOT/$p_dir/tests.md" ]; then
255
289
  lint_args+=(--tests "$REPO_ROOT/$p_dir/tests.md")
256
290
  fi
257
- "$HOME/.codex/hooks/superflow-test-report-lint.py" \
291
+ "$LINT" \
258
292
  "${lint_args[@]}" "$REPO_ROOT/$rel_report"
259
293
  if [ $? -ne 0 ]; then
260
294
  FAILED=1
@@ -283,18 +317,18 @@ fi
283
317
  VERIFY_REPORTS=$(printf '%s\n' "$REPORTS" | grep -E \
284
318
  '(^|/)embedded-changes/p[0-9][^/]*/test-report\.md$' || true)
285
319
 
286
- if [ -n "$VERIFY_REPORTS" ] && [ -x "$HOME/.codex/hooks/superflow-verify-integration.sh" ]; then
287
- VERIFY_ARGS=""
320
+ if [ -n "$VERIFY_REPORTS" ] && [ -x "$INTEGRATION" ]; then
321
+ VERIFY_ARGS=()
288
322
  for report in $VERIFY_REPORTS; do
289
323
  abs_report="$REPO_ROOT/$report"
290
324
  if is_blocked_doc_freeze_report "$abs_report" && [ -z "$CODE_RUNTIME_CHANGED" ]; then
291
325
  ok "[$report] 为 SDD 文档冻结 Blocked 报告,跳过真实集成验收脚本"
292
326
  continue
293
327
  fi
294
- VERIFY_ARGS="$VERIFY_ARGS $abs_report"
328
+ VERIFY_ARGS+=("$abs_report")
295
329
  done
296
- if [ -n "$VERIFY_ARGS" ]; then
297
- "$HOME/.codex/hooks/superflow-verify-integration.sh" $VERIFY_ARGS
330
+ if [ "${#VERIFY_ARGS[@]}" -gt 0 ]; then
331
+ "$INTEGRATION" "${VERIFY_ARGS[@]}"
298
332
  if [ $? -ne 0 ]; then
299
333
  FAILED=1
300
334
  fi
@@ -304,14 +338,31 @@ fi
304
338
  ROOT_REPORTS=$(printf '%s\n' "$REPORTS" | grep -E \
305
339
  '(^|/)openspec/changes/[^/]+/test-report\.md$|(^|/)doc/openspec/changes/[^/]+/test-report\.md$' || true)
306
340
 
307
- if [ -n "$ROOT_REPORTS" ] && [ -x "$HOME/.codex/hooks/superflow-test-report-lint.py" ]; then
341
+ # Evidence/contract-only commits still refer to their existing root report.
342
+ AFFECTED_ROOT_DIRS=$(printf '%s\n' "$ACTIVE_SDD_CHANGED" | sed -nE 's#^((doc/)?openspec/changes/[^/]+)/.*#\1#p' | sort -u)
343
+ for change_root in $AFFECTED_ROOT_DIRS; do
344
+ case "$change_root" in */archive) continue ;; esac
345
+ if [ -f "$REPO_ROOT/$change_root/test-report.md" ]; then
346
+ ROOT_REPORTS="$ROOT_REPORTS
347
+ $change_root/test-report.md"
348
+ fi
349
+ done
350
+ ROOT_REPORTS=$(printf '%s\n' "$ROOT_REPORTS" | sed '/^$/d' | sort -u)
351
+
352
+ if [ -n "$ROOT_REPORTS" ] && [ ! -x "$LINT" ]; then
353
+ fail "同安装目录缺少当前 report lint,请执行 superflow update"
354
+ fi
355
+ if [ -n "$ROOT_REPORTS" ] && [ -x "$LINT" ]; then
308
356
  for report in $ROOT_REPORTS; do
309
- root_lint_args=(--warn-only --repo-root "$REPO_ROOT")
357
+ root_lint_args=(--repo-root "$REPO_ROOT" --git-index-root "$REPO_ROOT")
358
+ if [ -z "$CODE_RUNTIME_CHANGED" ] && [ ! -f "$REPO_ROOT/${report%/test-report.md}/.sdd/reviews/document-review.json" ] && is_blocked_doc_freeze_report "$REPO_ROOT/$report"; then
359
+ root_lint_args+=(--warn-only)
360
+ fi
310
361
  root_tests="${report%/test-report.md}/tests.md"
311
362
  if [ -f "$REPO_ROOT/$root_tests" ]; then
312
363
  root_lint_args+=(--tests "$REPO_ROOT/$root_tests")
313
364
  fi
314
- "$HOME/.codex/hooks/superflow-test-report-lint.py" \
365
+ "$LINT" \
315
366
  "${root_lint_args[@]}" "$REPO_ROOT/$report"
316
367
  if [ $? -ne 0 ]; then
317
368
  FAILED=1
@@ -80,5 +80,5 @@ for report in $REPORTS; do
80
80
  REPORT_ARGS="$REPORT_ARGS $REPO_ROOT/$report"
81
81
  done
82
82
 
83
- "$HOME/.codex/hooks/superflow-verify-integration.sh" $REPORT_ARGS
83
+ "$(dirname "${BASH_SOURCE[0]}")/superflow-verify-integration.sh" $REPORT_ARGS
84
84
  exit $?
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
 
7
8
  const [projectArg = ".", taskId] = process.argv.slice(2);
8
9
  if (!taskId) {
@@ -102,6 +103,35 @@ if (changeDir) {
102
103
  }
103
104
  if (!fs.existsSync(reportFile))
104
105
  errors.push(`缺少 test-report.md: ${reportFile}`);
106
+ const stateFile = path.join(changeDir, ".sdd/state.yaml");
107
+ const reviewFile = path.join(changeDir, ".sdd/reviews/document-review.json");
108
+ const full =
109
+ fs.existsSync(stateFile) &&
110
+ /^workflow:\s*full\s*$/m.test(fs.readFileSync(stateFile, "utf8"));
111
+ if (full && !fs.existsSync(reviewFile))
112
+ errors.push("完整 SDD 缺少冻结入口评审凭证");
113
+ if (fs.existsSync(reviewFile)) {
114
+ try {
115
+ execFileSync(
116
+ "python3",
117
+ [
118
+ path.join(
119
+ path.dirname(fileURLToPath(import.meta.url)),
120
+ "superflow-test-report-lint.py",
121
+ ),
122
+ "--tests",
123
+ path.join(changeDir, "tests.md"),
124
+ "--review",
125
+ reviewFile,
126
+ "--require-complete",
127
+ reportFile,
128
+ ],
129
+ { encoding: "utf8", stdio: "pipe" },
130
+ );
131
+ } catch (error) {
132
+ errors.push(`逐用例执行凭证门禁失败: ${error.stdout ?? error.message}`);
133
+ }
134
+ }
105
135
  }
106
136
 
107
137
  if (errors.length > 0) {
@@ -2,6 +2,8 @@
2
2
  import crypto from "crypto";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
5
7
 
6
8
  const [projectArg = ".", taskId, mode] = process.argv.slice(2);
7
9
  const beforeReadyEvent = mode === "--before-ready-event";
@@ -328,6 +330,39 @@ function verifyOpenSpecTasksComplete(task, executor) {
328
330
  while (current.startsWith(projectRoot)) {
329
331
  const tasksFile = path.join(current, "tasks.md");
330
332
  if (fs.existsSync(tasksFile)) {
333
+ const reviewFile = path.join(
334
+ current,
335
+ ".sdd/reviews/document-review.json",
336
+ );
337
+ const stateFile = path.join(current, ".sdd/state.yaml");
338
+ const full =
339
+ fs.existsSync(stateFile) &&
340
+ /^workflow:\s*full\s*$/m.test(fs.readFileSync(stateFile, "utf8"));
341
+ if (full && !fs.existsSync(reviewFile))
342
+ fail("完整 SDD 缺少冻结入口评审凭证");
343
+ if (fs.existsSync(reviewFile)) {
344
+ try {
345
+ execFileSync(
346
+ "python3",
347
+ [
348
+ path.join(
349
+ path.dirname(fileURLToPath(import.meta.url)),
350
+ "superflow-test-report-lint.py",
351
+ ),
352
+ "--tests",
353
+ path.join(current, "tests.md"),
354
+ "--review",
355
+ reviewFile,
356
+ "--require-complete",
357
+ path.join(current, "test-report.md"),
358
+ ],
359
+ { encoding: "utf8", stdio: "pipe" },
360
+ );
361
+ } catch (error) {
362
+ fail(`逐用例执行凭证门禁失败: ${error.stdout ?? error.message}`);
363
+ }
364
+ }
365
+
331
366
  const tasks = fs
332
367
  .readFileSync(tasksFile, "utf8")
333
368
  .split(/\r?\n/)
@@ -127,6 +127,32 @@ export function validateCoverage(root, review, issues, checkCode = true) {
127
127
  issues.push(`入口 ${entry.id} 缺少角色、实际路由或入口类型`);
128
128
  }
129
129
  refs(entry.sourceRefs, "code", `入口 ${entry.id}`);
130
+ const database = entry.database;
131
+ if (list(entry.persistence).length && !database)
132
+ issues.push(
133
+ `入口 ${entry.id} 持久化影响缺少数据库合同;旧证据保留 PARTIAL`,
134
+ );
135
+ if (database !== undefined) {
136
+ if (
137
+ !nonempty(database?.engine) ||
138
+ !Number.isInteger(database?.major) ||
139
+ database.major < 1
140
+ )
141
+ issues.push(`入口 ${entry.id} 缺少数据库 engine/major`);
142
+ refs(database?.schemaSourceRefs, null, `入口 ${entry.id} 数据库 schema`);
143
+ const statements = list(database?.statements);
144
+ if (
145
+ !statements.length ||
146
+ statements.some(
147
+ (item) =>
148
+ !nonempty(item?.id) || sources.get(item.sourceRef)?.role !== "code",
149
+ )
150
+ )
151
+ issues.push(`入口 ${entry.id} 缺少生产 SQL statement 与源码引用`);
152
+ const identities = statements.map((item) => JSON.stringify(item));
153
+ if (new Set(identities).size !== identities.length)
154
+ issues.push(`入口 ${entry.id} 生产 SQL statement 重复`);
155
+ }
130
156
  }
131
157
  for (const test of cases.values()) {
132
158
  const entry = entries.get(test.entryId);
@@ -138,6 +164,113 @@ export function validateCoverage(root, review, issues, checkCode = true) {
138
164
  issues.push(`用例 ${test.id} 缺少 ${key} 断言`);
139
165
  }
140
166
  refs(test.sourceRefs, "contract", `用例 ${test.id}`);
167
+ if (
168
+ test.evidenceKind !== undefined &&
169
+ !["real", "controlled-simulation", "unit"].includes(test.evidenceKind)
170
+ )
171
+ issues.push(`用例 ${test.id} 的 evidenceKind 无效`);
172
+
173
+ const layer = test.testLayer;
174
+ const realDatabase = ["database", "http-entry"].includes(layer);
175
+ if (
176
+ layer !== undefined &&
177
+ !["logic", "sql-binding", "database", "http-entry"].includes(layer)
178
+ )
179
+ issues.push(`用例 ${test.id} 的 testLayer 无效`);
180
+ if (realDatabase && entry?.database) {
181
+ if (test.evidenceKind !== "real")
182
+ issues.push(`用例 ${test.id} 数据库验证必须使用 real 证据`);
183
+ const boundary = test.mockBoundary;
184
+ if (
185
+ !boundary ||
186
+ !Array.isArray(boundary.allowed) ||
187
+ !Array.isArray(boundary.forbidden) ||
188
+ boundary.forbidden.length === 0 ||
189
+ [...boundary.allowed, ...boundary.forbidden].some(
190
+ (v) => !nonempty(v),
191
+ ) ||
192
+ boundary.allowed.some((v) => boundary.forbidden.includes(v))
193
+ )
194
+ issues.push(`用例 ${test.id} 缺少明确且不冲突的 mockBoundary`);
195
+ const statements = list(test.statements);
196
+ if (
197
+ !statements.length ||
198
+ statements.some(
199
+ (item) =>
200
+ !list(entry.database.statements).some(
201
+ (v) => v.id === item?.id && v.sourceRef === item?.sourceRef,
202
+ ),
203
+ )
204
+ )
205
+ issues.push(`用例 ${test.id} 缺少入口冻结的生产 SQL statement`);
206
+ const assertions = list(test.databaseAssertions);
207
+ if (
208
+ !assertions.length ||
209
+ assertions.some(
210
+ (a) => !nonempty(a?.id) || !nonempty(a?.kind) || !("expected" in a),
211
+ )
212
+ )
213
+ issues.push(`用例 ${test.id} 缺少冻结数据库数据断言`);
214
+
215
+ for (const assertion of assertions) {
216
+ if (assertion.kind !== "unchangedRows") continue;
217
+ const snapshot = assertion.expected;
218
+ if (
219
+ !snapshot ||
220
+ typeof snapshot !== "object" ||
221
+ Array.isArray(snapshot) ||
222
+ !Object.keys(snapshot).length ||
223
+ Object.values(snapshot).some(
224
+ (row) =>
225
+ !row ||
226
+ typeof row !== "object" ||
227
+ Array.isArray(row) ||
228
+ !Object.keys(row).length,
229
+ )
230
+ )
231
+ issues.push(`用例 ${test.id} unchangedRows 缺少按业务键冻结的行快照`);
232
+ }
233
+ if (new Set(assertions.map((a) => a.id)).size !== assertions.length)
234
+ issues.push(`用例 ${test.id} 数据库断言 ID 重复`);
235
+ if (
236
+ list(entry.persistence).length &&
237
+ ["affectedRows", "unchangedRows"].some(
238
+ (kind) => !assertions.some((a) => a.kind === kind),
239
+ )
240
+ )
241
+ issues.push(`用例 ${test.id} 缺少影响行数/非目标与旧轮次不变断言`);
242
+ }
243
+ const writes = test.assertions?.persistence ?? [];
244
+ if (
245
+ !Array.isArray(writes) ||
246
+ writes.some(
247
+ (write) =>
248
+ !nonempty(write?.id) ||
249
+ !nonempty(write.table) ||
250
+ !nonempty(write.field) ||
251
+ !("expected" in write) ||
252
+ (write.expected === null && write.allowNull !== true),
253
+ )
254
+ ) {
255
+ issues.push(
256
+ `用例 ${test.id} 缺少持久化字段与非空预期值;历史 NULL 需明确 allowNull`,
257
+ );
258
+ } else {
259
+ const ids = new Set(writes.map((write) => write.id));
260
+ if (ids.size !== writes.length)
261
+ issues.push(`用例 ${test.id} 持久化断言 ID 重复`);
262
+ for (const field of ["logic", "sql-binding"].includes(layer)
263
+ ? []
264
+ : (entry?.persistence ?? [])) {
265
+ if (
266
+ !writes.some(
267
+ (write) =>
268
+ write.table === field.table && write.field === field.field,
269
+ )
270
+ )
271
+ issues.push(`用例 ${test.id} 缺少入口写入字段的前后值断言`);
272
+ }
273
+ }
141
274
  }
142
275
  const pairs = new Set();
143
276
  for (const decision of list(coverage.decisions)) {
@@ -174,6 +307,39 @@ export function validateCoverage(root, review, issues, checkCode = true) {
174
307
  ) {
175
308
  issues.push(`${key} 缺少对应真实入口的验收用例`);
176
309
  }
310
+
311
+ if (
312
+ decision.disposition !== "EXCLUDED" &&
313
+ entries.get(decision.entryId)?.database &&
314
+ !list(decision.caseIds).some(
315
+ (id) =>
316
+ ["database", "http-entry"].includes(cases.get(id)?.testLayer) &&
317
+ cases.get(id)?.evidenceKind === "real",
318
+ )
319
+ )
320
+ issues.push(`${key} 缺少必要真实数据库用例;Mock/sql-binding 不能替代`);
321
+
322
+ const database = entries.get(decision.entryId)?.database;
323
+ if (decision.disposition !== "EXCLUDED" && database) {
324
+ const executed = list(decision.caseIds).flatMap((id) => {
325
+ const test = cases.get(id);
326
+ return ["database", "http-entry"].includes(test?.testLayer) &&
327
+ test?.evidenceKind === "real"
328
+ ? list(test.statements)
329
+ : [];
330
+ });
331
+ if (
332
+ list(database.statements).some(
333
+ (statement) =>
334
+ !executed.some(
335
+ (item) =>
336
+ item.id === statement.id &&
337
+ item.sourceRef === statement.sourceRef,
338
+ ),
339
+ )
340
+ )
341
+ issues.push(`${key} 生产 SQL 未被必要数据库用例覆盖`);
342
+ }
177
343
  if (
178
344
  decision.disposition === "EXCLUDED" &&
179
345
  list(decision.caseIds).length > 0