@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.
- package/README.md +18 -0
- package/README.zh-CN.md +18 -0
- package/assets/manifest.json +29 -0
- package/assets/scripts/install-sql-pre-commit.sh +54 -39
- package/assets/scripts/superflow-delivery-check.sh +62 -11
- package/assets/scripts/superflow-integration-evidence-hook.sh +1 -1
- package/assets/scripts/superflow-managed-executor-preflight.mjs +30 -0
- package/assets/scripts/superflow-managed-work-check.mjs +35 -0
- package/assets/scripts/superflow-review-coverage.mjs +166 -0
- package/assets/scripts/superflow-test-report-lint.py +290 -13
- package/assets/scripts/superflow-verify-integration.sh +4 -9
- package/assets/skills/superflow-design/SKILL.md +6 -0
- package/assets/skills/superflow-docs/SKILL.md +9 -0
- package/assets/skills/superflow-implement/SKILL.md +9 -0
- package/assets/skills/superflow-pipeline/references/document-review-coverage.md +60 -3
- package/assets/skills/superflow-pipeline/references/test-execution-template.md +3 -1
- package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +9 -5
- package/assets/skills/superflow-table-impact-analysis/SKILL.md +6 -0
- package/assets/skills/superflow-verify/SKILL.md +9 -0
- package/assets/skills-en/superflow-design/SKILL.md +6 -0
- package/assets/skills-en/superflow-docs/SKILL.md +9 -0
- package/assets/skills-en/superflow-implement/SKILL.md +9 -0
- package/assets/skills-en/superflow-pipeline/references/document-review-coverage.md +72 -3
- package/assets/skills-en/superflow-pipeline/references/test-execution-template.md +3 -1
- package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +9 -5
- package/assets/skills-en/superflow-table-impact-analysis/SKILL.md +6 -0
- package/assets/skills-en/superflow-verify/SKILL.md +9 -0
- package/dist/app/cli.js +11 -11
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/change-guard.js +4 -0
- package/dist/app/commands/change-guard.js.map +1 -1
- package/dist/app/commands/doctor.js +36 -2
- package/dist/app/commands/doctor.js.map +1 -1
- package/dist/app/commands/hook-migrate.js +48 -8
- package/dist/app/commands/hook-migrate.js.map +1 -1
- package/dist/app/commands/update.js +8 -6
- package/dist/app/commands/update.js.map +1 -1
- package/dist/domains/config/cli-help.js +12 -2
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/config/manifest.js +1 -1
- package/dist/domains/config/manifest.js.map +1 -1
- package/dist/domains/hook-migration.js +70 -5
- package/dist/domains/hook-migration.js.map +1 -1
- package/dist/domains/hook.js +67 -23
- package/dist/domains/hook.js.map +1 -1
- package/dist/domains/skill/scripts.js +140 -11
- package/dist/domains/skill/scripts.js.map +1 -1
- 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 加强文档交付:逐需求×真实入口核对(含排除项)、真实入口验收合同、
|
package/assets/manifest.json
CHANGED
|
@@ -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 -
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 "$
|
|
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
|
-
"$
|
|
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 "$
|
|
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
|
|
328
|
+
VERIFY_ARGS+=("$abs_report")
|
|
295
329
|
done
|
|
296
|
-
if [
|
|
297
|
-
"$
|
|
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
|
-
|
|
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=(--
|
|
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
|
-
"$
|
|
365
|
+
"$LINT" \
|
|
315
366
|
"${root_lint_args[@]}" "$REPO_ROOT/$report"
|
|
316
367
|
if [ $? -ne 0 ]; then
|
|
317
368
|
FAILED=1
|
|
@@ -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
|