@chenmk/superflow 0.5.12 → 0.5.14
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 +26 -0
- package/README.zh-CN.md +8 -0
- package/assets/manifest.json +33 -7
- package/assets/review-rules.json +24 -0
- package/assets/scripts/superflow-dependency-update-hook.sh +29 -0
- package/assets/scripts/superflow-doc-placement-hook.py +38 -0
- package/assets/scripts/superflow-java-build-hook.py +178 -0
- package/assets/scripts/superflow-java-stop-hook.sh +3 -0
- package/assets/scripts/superflow-loop-end-hook.sh +3 -0
- package/assets/scripts/superflow-loop-failure-hook.sh +3 -0
- package/assets/scripts/superflow-loop-observer.py +149 -0
- package/assets/scripts/superflow-sql-sync-hook.py +141 -3
- package/assets/scripts/superflow-test-report-lint.py +80 -2
- package/assets/skill-registry.json +229 -0
- package/assets/skills/openspec-propose/SKILL.md +14 -0
- package/assets/skills/superflow-clarify/SKILL.md +23 -1
- package/assets/skills/superflow-design/SKILL.md +6 -1
- package/assets/skills/superflow-docs/SKILL.md +24 -0
- package/assets/skills/superflow-implement/SKILL.md +8 -0
- package/assets/skills/superflow-pipeline/SKILL.md +31 -1
- package/assets/skills/superflow-pipeline/references/document-review-coverage.md +18 -0
- package/assets/skills/superflow-pipeline/references/superpower-technical-design-template.md +17 -0
- package/assets/skills/superflow-pipeline/references/test-execution-template.md +11 -0
- package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +8 -2
- package/assets/skills/superflow-quick-dev/SKILL.md +44 -0
- package/assets/skills/superflow-verify/SKILL.md +45 -1
- package/assets/skills-en/openspec-propose/SKILL.md +14 -0
- package/assets/skills-en/superflow-clarify/SKILL.md +23 -1
- package/assets/skills-en/superflow-design/SKILL.md +6 -1
- package/assets/skills-en/superflow-docs/SKILL.md +24 -0
- package/assets/skills-en/superflow-implement/SKILL.md +9 -0
- package/assets/skills-en/superflow-pipeline/SKILL.md +34 -0
- package/assets/skills-en/superflow-pipeline/references/document-review-coverage.md +20 -0
- package/assets/skills-en/superflow-pipeline/references/test-execution-template.md +12 -0
- package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +8 -2
- package/assets/skills-en/superflow-quick-dev/SKILL.md +43 -0
- package/assets/skills-en/superflow-verify/SKILL.md +52 -1
- package/dist/app/cli.js +98 -0
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/check.js +7 -3
- package/dist/app/commands/check.js.map +1 -1
- package/dist/app/commands/cleanup-audit.js +13 -0
- package/dist/app/commands/cleanup-audit.js.map +1 -0
- package/dist/app/commands/context-audit.js +43 -0
- package/dist/app/commands/context-audit.js.map +1 -0
- package/dist/app/commands/docs-audit.js +11 -0
- package/dist/app/commands/docs-audit.js.map +1 -0
- package/dist/app/commands/doctor.js +10 -0
- package/dist/app/commands/doctor.js.map +1 -1
- package/dist/app/commands/hook-migrate.js +25 -0
- package/dist/app/commands/hook-migrate.js.map +1 -0
- package/dist/app/commands/init.js +9 -19
- package/dist/app/commands/init.js.map +1 -1
- package/dist/app/commands/map-audit.js +10 -0
- package/dist/app/commands/map-audit.js.map +1 -0
- package/dist/app/commands/pipeline.js +13 -1
- package/dist/app/commands/pipeline.js.map +1 -1
- package/dist/app/commands/quick.js +136 -0
- package/dist/app/commands/quick.js.map +1 -0
- package/dist/app/commands/review-coverage.js +84 -0
- package/dist/app/commands/review-coverage.js.map +1 -0
- package/dist/app/commands/skill-audit.js +36 -0
- package/dist/app/commands/skill-audit.js.map +1 -0
- package/dist/app/commands/status.js +108 -7
- package/dist/app/commands/status.js.map +1 -1
- package/dist/app/commands/update.js +9 -12
- package/dist/app/commands/update.js.map +1 -1
- package/dist/domains/cleanup-audit.js +38 -0
- package/dist/domains/cleanup-audit.js.map +1 -0
- package/dist/domains/config/cli-help.js +14 -0
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/config/context.js +13 -10
- package/dist/domains/config/context.js.map +1 -1
- package/dist/domains/context-audit.js +39 -0
- package/dist/domains/context-audit.js.map +1 -0
- package/dist/domains/deps.js +18 -8
- package/dist/domains/deps.js.map +1 -1
- package/dist/domains/docs-audit.js +34 -0
- package/dist/domains/docs-audit.js.map +1 -0
- package/dist/domains/hook-migration.js +127 -0
- package/dist/domains/hook-migration.js.map +1 -0
- package/dist/domains/hook.js +76 -2
- package/dist/domains/hook.js.map +1 -1
- package/dist/domains/managed-work/control.js +56 -2
- package/dist/domains/managed-work/control.js.map +1 -1
- package/dist/domains/map-audit.js +23 -0
- package/dist/domains/map-audit.js.map +1 -0
- package/dist/domains/quick-dev/gate.js +91 -0
- package/dist/domains/quick-dev/gate.js.map +1 -0
- package/dist/domains/review/coverage.js +119 -0
- package/dist/domains/review/coverage.js.map +1 -0
- package/dist/domains/review/report.js +62 -0
- package/dist/domains/review/report.js.map +1 -0
- package/dist/domains/skill/registry.js +87 -0
- package/dist/domains/skill/registry.js.map +1 -0
- package/package.json +1 -1
- package/scripts/check-design-principles.mjs +4 -0
|
@@ -64,6 +64,7 @@ per finding for the developer to apply):
|
|
|
64
64
|
import json
|
|
65
65
|
import os
|
|
66
66
|
import re
|
|
67
|
+
import shlex
|
|
67
68
|
import subprocess
|
|
68
69
|
import sys
|
|
69
70
|
from pathlib import Path
|
|
@@ -75,6 +76,15 @@ if not (os.path.isdir('openspec') or os.path.isdir('.sdd') or os.path.isfile('.s
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
DB_CODE_FILE = re.compile(r"\.(java|xml)$", re.I)
|
|
79
|
+
MYBATIS_SAFE_EXPRESSIONS = frozenset({
|
|
80
|
+
"ew.sqlSegment", "ew.customSqlSegment", "ew.sqlSet", "ew.sqlComment",
|
|
81
|
+
})
|
|
82
|
+
MYBATIS_INTERPOLATION = re.compile(r"\$\{([^}]+)\}")
|
|
83
|
+
SECRET_PATTERNS = (
|
|
84
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
85
|
+
re.compile(r"\b(?:api[_-]?key|api[_-]?secret)\s*[:=]\s*['\"][^'\"]{8,}", re.I),
|
|
86
|
+
re.compile(r"\b[a-z_][a-z0-9_]*_(?:secret|token)\s*[:=]\s*['\"][^'\"]{16,}['\"]", re.I),
|
|
87
|
+
)
|
|
78
88
|
SQL_SUMMARY_FILE = re.compile(r"(^|/)sql/.*\.sql$", re.I)
|
|
79
89
|
NO_SQL_ACK_FILE = re.compile(
|
|
80
90
|
r"(^|/)(openspec/.*\.(md|yaml|yml)|ReleaseNotes\.md)$",
|
|
@@ -514,10 +524,113 @@ def warn(message, block=False):
|
|
|
514
524
|
print(f"[SDD SQL 收口{prefix}] {message}", file=sys.stderr)
|
|
515
525
|
|
|
516
526
|
|
|
527
|
+
def mapper_interpolations(file_path, content):
|
|
528
|
+
normalized = file_path.replace("\\", "/")
|
|
529
|
+
if not normalized.lower().endswith(".xml"):
|
|
530
|
+
return []
|
|
531
|
+
mapper_path = re.search(r"(?:^|/)(?:mapper|dao)/", normalized, re.I)
|
|
532
|
+
mapper_name = re.search(r"(?:Mapper|Dao)\.xml$", normalized)
|
|
533
|
+
mapper_body = re.search(r"<mapper\b|<!DOCTYPE\s+mapper", content, re.I)
|
|
534
|
+
if not (mapper_path or mapper_name or mapper_body):
|
|
535
|
+
return []
|
|
536
|
+
return [match.group(1).strip() for match in MYBATIS_INTERPOLATION.finditer(content)
|
|
537
|
+
if match.group(1).strip() not in MYBATIS_SAFE_EXPRESSIONS]
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def git_segment_args(segment):
|
|
541
|
+
tokens = shlex.split(segment)
|
|
542
|
+
git_at = next((i for i, token in enumerate(tokens)
|
|
543
|
+
if token == "git" or token.endswith("/git")), -1)
|
|
544
|
+
if git_at < 0:
|
|
545
|
+
return None
|
|
546
|
+
return tokens[:git_at], tokens[git_at + 1:]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def git_subcommand_index(args):
|
|
550
|
+
index = 0
|
|
551
|
+
while index < len(args) and args[index].startswith("-"):
|
|
552
|
+
index += 2 if args[index] in ("-c", "-C") else 1
|
|
553
|
+
return index
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def check_git_bypass(command):
|
|
557
|
+
for segment in re.split(r"\|\||&&|[;|\n]", command):
|
|
558
|
+
try:
|
|
559
|
+
parsed = git_segment_args(segment)
|
|
560
|
+
except ValueError:
|
|
561
|
+
if "git" in segment and ("--no-verify" in segment or "core.hooksPath" in segment):
|
|
562
|
+
return "Git 命令无法解析且包含 Hook 绕过参数"
|
|
563
|
+
continue
|
|
564
|
+
if parsed is None:
|
|
565
|
+
continue
|
|
566
|
+
prefix, args = parsed
|
|
567
|
+
config_override = any(
|
|
568
|
+
arg.startswith("core.hooksPath=") for arg in args
|
|
569
|
+
) or any(token.startswith("GIT_CONFIG_") and "core.hooksPath" in token
|
|
570
|
+
for token in prefix)
|
|
571
|
+
if config_override or "--no-verify" in args:
|
|
572
|
+
return "禁止使用 --no-verify 或 core.hooksPath 绕过 Git Hook"
|
|
573
|
+
index = git_subcommand_index(args)
|
|
574
|
+
if index < len(args) and args[index] == "commit":
|
|
575
|
+
flags = []
|
|
576
|
+
skip_value = False
|
|
577
|
+
for arg in args[index + 1:]:
|
|
578
|
+
if skip_value:
|
|
579
|
+
skip_value = False
|
|
580
|
+
continue
|
|
581
|
+
if arg in ("-m", "--message", "-F", "--file"):
|
|
582
|
+
skip_value = True
|
|
583
|
+
continue
|
|
584
|
+
flags.append(arg)
|
|
585
|
+
if any(re.fullmatch(r"-[a-zA-Z]*n[a-zA-Z]*", arg)
|
|
586
|
+
for arg in flags):
|
|
587
|
+
return "禁止使用 git commit -n 绕过 Git Hook"
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def is_git_commit(command):
|
|
592
|
+
for segment in re.split(r"\|\||&&|[;|\n]", command):
|
|
593
|
+
try:
|
|
594
|
+
parsed = git_segment_args(segment)
|
|
595
|
+
except ValueError:
|
|
596
|
+
continue
|
|
597
|
+
if parsed is None:
|
|
598
|
+
continue
|
|
599
|
+
_, args = parsed
|
|
600
|
+
index = git_subcommand_index(args)
|
|
601
|
+
if index < len(args) and args[index] == "commit":
|
|
602
|
+
return True
|
|
603
|
+
return False
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def check_staged_secrets(root):
|
|
607
|
+
result = run(["git", "diff", "--cached", "--unified=0", "--no-ext-diff"], cwd=root)
|
|
608
|
+
if result.returncode != 0:
|
|
609
|
+
return []
|
|
610
|
+
findings = []
|
|
611
|
+
current_path = ""
|
|
612
|
+
for line in result.stdout.splitlines():
|
|
613
|
+
if line.startswith("+++ b/"):
|
|
614
|
+
current_path = line[6:]
|
|
615
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
616
|
+
if any(pattern.search(line[1:]) for pattern in SECRET_PATTERNS):
|
|
617
|
+
findings.append(current_path)
|
|
618
|
+
return sorted(set(findings))
|
|
619
|
+
|
|
620
|
+
|
|
517
621
|
def handle_commit(command, root):
|
|
518
|
-
|
|
622
|
+
bypass = check_git_bypass(command)
|
|
623
|
+
if bypass:
|
|
624
|
+
warn(bypass, block=True)
|
|
625
|
+
return 2
|
|
626
|
+
if not is_git_commit(command):
|
|
519
627
|
return 0
|
|
520
628
|
|
|
629
|
+
secrets = check_staged_secrets(root)
|
|
630
|
+
if secrets:
|
|
631
|
+
warn("暂存差异包含疑似硬编码密钥:" + ", ".join(secrets), block=True)
|
|
632
|
+
return 2
|
|
633
|
+
|
|
521
634
|
files = staged_files(root)
|
|
522
635
|
if not files:
|
|
523
636
|
return 0
|
|
@@ -595,6 +708,11 @@ def handle_commit(command, root):
|
|
|
595
708
|
def handle_edit(file_path, edit_content):
|
|
596
709
|
if not file_path:
|
|
597
710
|
return 0
|
|
711
|
+
unsafe = mapper_interpolations(file_path, edit_content)
|
|
712
|
+
if unsafe:
|
|
713
|
+
warn("Mapper XML 使用危险 ${} 插值:" + ", ".join(unsafe[:3])
|
|
714
|
+
+ ";请使用 #{},动态标识符须在 Java 侧白名单校验。", block=True)
|
|
715
|
+
return 2
|
|
598
716
|
if SQL_SUMMARY_FILE.search(file_path):
|
|
599
717
|
sql_style_issues = lint_sql_style(file_path, edit_content)
|
|
600
718
|
if sql_style_issues:
|
|
@@ -944,7 +1062,9 @@ def main():
|
|
|
944
1062
|
|
|
945
1063
|
start = Path.cwd()
|
|
946
1064
|
if file_path:
|
|
947
|
-
|
|
1065
|
+
candidate = Path(file_path).expanduser().resolve().parent
|
|
1066
|
+
if candidate.exists():
|
|
1067
|
+
start = candidate
|
|
948
1068
|
root = repo_root(start)
|
|
949
1069
|
if root is None:
|
|
950
1070
|
return 0
|
|
@@ -952,7 +1072,25 @@ def main():
|
|
|
952
1072
|
if command:
|
|
953
1073
|
return handle_commit(command, root)
|
|
954
1074
|
|
|
955
|
-
|
|
1075
|
+
patch = tool_input.get("patch") or ""
|
|
1076
|
+
if patch:
|
|
1077
|
+
sections = re.split(r"(?=^\*\*\* (?:Add|Update) File: )", patch, flags=re.M)
|
|
1078
|
+
for section in sections:
|
|
1079
|
+
target = re.search(r"^\*\*\* (?:Add|Update) File: (.+)$", section, re.M)
|
|
1080
|
+
if not target:
|
|
1081
|
+
continue
|
|
1082
|
+
added = "\n".join(line[1:] for line in section.splitlines()
|
|
1083
|
+
if line.startswith("+") and not line.startswith("+++"))
|
|
1084
|
+
if handle_edit(target.group(1), added) == 2:
|
|
1085
|
+
return 2
|
|
1086
|
+
return 0
|
|
1087
|
+
|
|
1088
|
+
edits = tool_input.get("edits")
|
|
1089
|
+
if isinstance(edits, list):
|
|
1090
|
+
edit_content = "\n".join(str(edit.get("new_string") or "")
|
|
1091
|
+
for edit in edits if isinstance(edit, dict))
|
|
1092
|
+
else:
|
|
1093
|
+
edit_content = tool_input.get("new_string") or tool_input.get("content") or ""
|
|
956
1094
|
return handle_edit(file_path, edit_content)
|
|
957
1095
|
|
|
958
1096
|
|
|
@@ -14,6 +14,7 @@ mechanically:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import argparse
|
|
17
|
+
import json
|
|
17
18
|
import re
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
@@ -99,6 +100,13 @@ EXTERNAL_EVIDENCE_RE = re.compile(
|
|
|
99
100
|
re.I,
|
|
100
101
|
)
|
|
101
102
|
CASE_ID_RE = re.compile(r"\bT\d+(?:\.\d+)+\b")
|
|
103
|
+
CASE_EVIDENCE_HEADER = re.compile(
|
|
104
|
+
r"^\|\s*(?:用例\s*ID|Case\s*ID)\s*\|\s*(?:入口\s*ID|Entry\s*ID)\s*\|"
|
|
105
|
+
r"\s*(?:验收级别|Acceptance\s*level)\s*\|\s*(?:结果|Result)\s*\|"
|
|
106
|
+
r"\s*(?:证据|Evidence)(?:路径|\s*path)?\s*\|$",
|
|
107
|
+
re.I,
|
|
108
|
+
)
|
|
109
|
+
CASE_RESULTS = {"PASS", "FAIL", "BLOCKED", "PARTIAL"}
|
|
102
110
|
OLD_PHRASES = (
|
|
103
111
|
"不会按周期边界拆分子切片",
|
|
104
112
|
"1 秒空窗落入下一周期对账无实质影响",
|
|
@@ -253,6 +261,69 @@ def lint_against_tests_contract(report_path: Path, report_text: str, tests_path:
|
|
|
253
261
|
return issues
|
|
254
262
|
|
|
255
263
|
|
|
264
|
+
def lint_case_evidence(report_path: Path, report_text: str, review_path: Path) -> list[Issue]:
|
|
265
|
+
"""Check the frozen R/E/C index against per-case execution receipts."""
|
|
266
|
+
issues: list[Issue] = []
|
|
267
|
+
try:
|
|
268
|
+
review = json.loads(review_path.read_text(encoding="utf-8"))
|
|
269
|
+
coverage = review["coverage"]
|
|
270
|
+
if coverage["schemaVersion"] != "superflow.review-coverage.v1":
|
|
271
|
+
raise ValueError("unsupported coverage schema")
|
|
272
|
+
cases = {case["id"]: case for case in coverage["cases"]}
|
|
273
|
+
decisions = coverage["decisions"]
|
|
274
|
+
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
275
|
+
return [Issue("FAIL", report_path, 0, f"入口评审凭证无法读取:{error}")]
|
|
276
|
+
|
|
277
|
+
required = {
|
|
278
|
+
case_id
|
|
279
|
+
for decision in decisions
|
|
280
|
+
if decision.get("disposition") != "EXCLUDED"
|
|
281
|
+
for case_id in decision.get("caseIds", [])
|
|
282
|
+
}
|
|
283
|
+
lines = report_text.splitlines()
|
|
284
|
+
header_at = next(
|
|
285
|
+
(i for i, line in enumerate(lines) if CASE_EVIDENCE_HEADER.fullmatch(line.strip())),
|
|
286
|
+
None,
|
|
287
|
+
)
|
|
288
|
+
if header_at is None:
|
|
289
|
+
return [Issue("FAIL", report_path, 0, "缺少逐用例执行证据表;不能用汇总通过数代替 R/E/C 回填")]
|
|
290
|
+
|
|
291
|
+
receipts: dict[str, tuple[str, str, str, str]] = {}
|
|
292
|
+
for index, line in enumerate(lines[header_at + 2 :], start=header_at + 3):
|
|
293
|
+
if not line.startswith("|"):
|
|
294
|
+
break
|
|
295
|
+
columns = [column.strip() for column in line.strip().strip("|").split("|")]
|
|
296
|
+
if len(columns) != 5:
|
|
297
|
+
issues.append(Issue("FAIL", report_path, index, "逐用例执行证据表必须有五列"))
|
|
298
|
+
continue
|
|
299
|
+
case_id, entry_id, level, result, evidence = columns
|
|
300
|
+
if case_id in receipts:
|
|
301
|
+
issues.append(Issue("FAIL", report_path, index, f"用例 {case_id} 重复回填"))
|
|
302
|
+
receipts[case_id] = (entry_id, level, result.upper(), evidence)
|
|
303
|
+
|
|
304
|
+
for case_id in sorted(required):
|
|
305
|
+
if case_id not in receipts:
|
|
306
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 缺少逐项执行结果"))
|
|
307
|
+
continue
|
|
308
|
+
entry_id, level, result, evidence = receipts[case_id]
|
|
309
|
+
case = cases.get(case_id)
|
|
310
|
+
if case is None or entry_id != case.get("entryId") or level != case.get("level"):
|
|
311
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 的入口或验收级别与冻结合同不一致"))
|
|
312
|
+
if result not in CASE_RESULTS or not evidence or evidence.lower() in {"pending", "todo", "待补"}:
|
|
313
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 缺少有效结果或证据位置"))
|
|
314
|
+
elif result == "PASS" and not re.match(r"^https?://", evidence, re.I):
|
|
315
|
+
evidence_file = Path(evidence.split("#", 1)[0])
|
|
316
|
+
if not evidence_file.is_absolute():
|
|
317
|
+
evidence_file = report_path.parent / evidence_file
|
|
318
|
+
if not evidence_file.is_file():
|
|
319
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 的通过证据文件不存在:{evidence}"))
|
|
320
|
+
if result != "PASS" and re.search(r"验证结果\s*[::]\s*PASS|Verification Result\s*[::]\s*PASS", report_text, re.I):
|
|
321
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 为 {result},报告不能声明整体验证 PASS"))
|
|
322
|
+
for case_id in receipts.keys() - required:
|
|
323
|
+
issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 未在冻结入口评审中要求执行"))
|
|
324
|
+
return issues
|
|
325
|
+
|
|
326
|
+
|
|
256
327
|
def infer_tests_path(report: Path) -> Path | None:
|
|
257
328
|
direct = report.parent / "tests.md"
|
|
258
329
|
if direct.exists():
|
|
@@ -268,7 +339,8 @@ def infer_tests_path(report: Path) -> Path | None:
|
|
|
268
339
|
return None
|
|
269
340
|
|
|
270
341
|
|
|
271
|
-
def lint_report(path: Path, repo: Path | None, tests_path: Path | None = None
|
|
342
|
+
def lint_report(path: Path, repo: Path | None, tests_path: Path | None = None,
|
|
343
|
+
review_path: Path | None = None) -> list[Issue]:
|
|
272
344
|
issues: list[Issue] = []
|
|
273
345
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
274
346
|
lines = text.splitlines()
|
|
@@ -278,6 +350,8 @@ def lint_report(path: Path, repo: Path | None, tests_path: Path | None = None) -
|
|
|
278
350
|
blocked = bool(BLOCKED_RE.search(text))
|
|
279
351
|
passed_or_partial = bool(PASS_WORDS.search(text) or re.search(r"Partially verified|部分验证", text, re.I))
|
|
280
352
|
issues.extend(lint_against_tests_contract(path, text, tests_path or infer_tests_path(path)))
|
|
353
|
+
if review_path is not None:
|
|
354
|
+
issues.extend(lint_case_evidence(path, text, review_path))
|
|
281
355
|
|
|
282
356
|
if db_backed and passed_or_partial and not blocked:
|
|
283
357
|
if not TABLE_REVERSE_HEADER_RE.search(text):
|
|
@@ -395,6 +469,7 @@ def main() -> int:
|
|
|
395
469
|
parser = argparse.ArgumentParser(description="Lint SuperBridge Flow test-report evidence")
|
|
396
470
|
parser.add_argument("reports", nargs="+")
|
|
397
471
|
parser.add_argument("--tests", default="")
|
|
472
|
+
parser.add_argument("--review", default="")
|
|
398
473
|
parser.add_argument("--repo-root", default="")
|
|
399
474
|
parser.add_argument("--warn-only", action="store_true")
|
|
400
475
|
args = parser.parse_args()
|
|
@@ -407,7 +482,10 @@ def main() -> int:
|
|
|
407
482
|
continue
|
|
408
483
|
repo = Path(args.repo_root).resolve() if args.repo_root else repo_root_for(path)
|
|
409
484
|
tests_path = Path(args.tests).resolve() if args.tests else None
|
|
410
|
-
|
|
485
|
+
if tests_path is not None and not tests_path.exists():
|
|
486
|
+
all_issues.append(Issue("FAIL", path, 0, f"显式指定的 tests.md 不存在:{tests_path}"))
|
|
487
|
+
review_path = Path(args.review).resolve() if args.review else None
|
|
488
|
+
all_issues.extend(lint_report(path, repo, tests_path, review_path))
|
|
411
489
|
|
|
412
490
|
printable = []
|
|
413
491
|
for issue in all_issues:
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"contracts": [
|
|
4
|
+
{
|
|
5
|
+
"name": "openspec-apply-change",
|
|
6
|
+
"owner": "implement",
|
|
7
|
+
"triggers": [
|
|
8
|
+
"implement approved change"
|
|
9
|
+
],
|
|
10
|
+
"excludes": [],
|
|
11
|
+
"outputs": [
|
|
12
|
+
"tasks.md progress"
|
|
13
|
+
],
|
|
14
|
+
"neighbors": []
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "openspec-archive-change",
|
|
18
|
+
"owner": "archive",
|
|
19
|
+
"triggers": [
|
|
20
|
+
"archive completed change"
|
|
21
|
+
],
|
|
22
|
+
"excludes": [],
|
|
23
|
+
"outputs": [
|
|
24
|
+
"archive result"
|
|
25
|
+
],
|
|
26
|
+
"neighbors": []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "openspec-explore",
|
|
30
|
+
"owner": "explore",
|
|
31
|
+
"triggers": [
|
|
32
|
+
"explore requirements"
|
|
33
|
+
],
|
|
34
|
+
"excludes": [],
|
|
35
|
+
"outputs": [
|
|
36
|
+
"alignment.md"
|
|
37
|
+
],
|
|
38
|
+
"neighbors": [
|
|
39
|
+
"openspec-propose"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "openspec-propose",
|
|
44
|
+
"owner": "propose",
|
|
45
|
+
"triggers": [
|
|
46
|
+
"generate OpenSpec proposal"
|
|
47
|
+
],
|
|
48
|
+
"excludes": [],
|
|
49
|
+
"outputs": [
|
|
50
|
+
"proposal.md specs design.md tasks.md"
|
|
51
|
+
],
|
|
52
|
+
"neighbors": [
|
|
53
|
+
"superflow-clarify",
|
|
54
|
+
"superflow-design",
|
|
55
|
+
"superflow-implement"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "superflow-archive",
|
|
60
|
+
"owner": "archive",
|
|
61
|
+
"triggers": [
|
|
62
|
+
"archive SDD change"
|
|
63
|
+
],
|
|
64
|
+
"excludes": [],
|
|
65
|
+
"outputs": [
|
|
66
|
+
"archive report"
|
|
67
|
+
],
|
|
68
|
+
"neighbors": [
|
|
69
|
+
"superflow-verify"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "superflow-clarify",
|
|
74
|
+
"owner": "clarify",
|
|
75
|
+
"triggers": [
|
|
76
|
+
"clarify requirements"
|
|
77
|
+
],
|
|
78
|
+
"excludes": [],
|
|
79
|
+
"outputs": [
|
|
80
|
+
"source-code-audit.md feature-gates.md brainstorm-summary.md"
|
|
81
|
+
],
|
|
82
|
+
"neighbors": [
|
|
83
|
+
"openspec-propose",
|
|
84
|
+
"superflow-docs"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "superflow-design",
|
|
89
|
+
"owner": "design",
|
|
90
|
+
"triggers": [
|
|
91
|
+
"technical design"
|
|
92
|
+
],
|
|
93
|
+
"excludes": [],
|
|
94
|
+
"outputs": [
|
|
95
|
+
"technical_design"
|
|
96
|
+
],
|
|
97
|
+
"neighbors": [
|
|
98
|
+
"superflow-docs",
|
|
99
|
+
"superflow-implement"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "superflow-docs",
|
|
104
|
+
"owner": "docs",
|
|
105
|
+
"triggers": [
|
|
106
|
+
"SDD contract documents"
|
|
107
|
+
],
|
|
108
|
+
"excludes": [],
|
|
109
|
+
"outputs": [
|
|
110
|
+
"api.md tests.md sdd-quality-gate.md"
|
|
111
|
+
],
|
|
112
|
+
"neighbors": [
|
|
113
|
+
"superflow-clarify",
|
|
114
|
+
"superflow-design",
|
|
115
|
+
"superflow-implement"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "superflow-hotfix",
|
|
120
|
+
"owner": "pipeline",
|
|
121
|
+
"triggers": [
|
|
122
|
+
"urgent small fix"
|
|
123
|
+
],
|
|
124
|
+
"excludes": [],
|
|
125
|
+
"outputs": [
|
|
126
|
+
"bug-fix-plan.md"
|
|
127
|
+
],
|
|
128
|
+
"neighbors": []
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "superflow-implement",
|
|
132
|
+
"owner": "implement",
|
|
133
|
+
"triggers": [
|
|
134
|
+
"generate implementation prompts"
|
|
135
|
+
],
|
|
136
|
+
"excludes": [],
|
|
137
|
+
"outputs": [
|
|
138
|
+
"prompt/ test-report.md"
|
|
139
|
+
],
|
|
140
|
+
"neighbors": [
|
|
141
|
+
"superflow-design",
|
|
142
|
+
"superflow-verify"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "superflow-pipeline",
|
|
147
|
+
"owner": "pipeline",
|
|
148
|
+
"triggers": [
|
|
149
|
+
"Superflow SDD pipeline"
|
|
150
|
+
],
|
|
151
|
+
"excludes": [],
|
|
152
|
+
"outputs": [
|
|
153
|
+
"state.yaml handoff"
|
|
154
|
+
],
|
|
155
|
+
"neighbors": []
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "superflow-requirement-review",
|
|
159
|
+
"owner": "review",
|
|
160
|
+
"triggers": [
|
|
161
|
+
"review requirements"
|
|
162
|
+
],
|
|
163
|
+
"excludes": [],
|
|
164
|
+
"outputs": [
|
|
165
|
+
"requirement-review.md"
|
|
166
|
+
],
|
|
167
|
+
"neighbors": []
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "superflow-table-impact-analysis",
|
|
171
|
+
"owner": "docs",
|
|
172
|
+
"triggers": [
|
|
173
|
+
"table impact analysis"
|
|
174
|
+
],
|
|
175
|
+
"excludes": [],
|
|
176
|
+
"outputs": [
|
|
177
|
+
"impact analysis"
|
|
178
|
+
],
|
|
179
|
+
"neighbors": []
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "superflow-tweak",
|
|
183
|
+
"owner": "pipeline",
|
|
184
|
+
"triggers": [
|
|
185
|
+
"small wording tweak"
|
|
186
|
+
],
|
|
187
|
+
"excludes": [],
|
|
188
|
+
"outputs": [
|
|
189
|
+
"tweak result"
|
|
190
|
+
],
|
|
191
|
+
"neighbors": []
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "superflow-quick-dev",
|
|
195
|
+
"owner": "quick",
|
|
196
|
+
"triggers": [
|
|
197
|
+
"small code task",
|
|
198
|
+
"quick development"
|
|
199
|
+
],
|
|
200
|
+
"excludes": [
|
|
201
|
+
"public API or database change",
|
|
202
|
+
"cross-module change"
|
|
203
|
+
],
|
|
204
|
+
"outputs": [
|
|
205
|
+
".sdd/tasks/<date>-<slug>/spec.md",
|
|
206
|
+
"quick evidence"
|
|
207
|
+
],
|
|
208
|
+
"neighbors": [
|
|
209
|
+
"superflow-clarify",
|
|
210
|
+
"superflow-verify"
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "superflow-verify",
|
|
215
|
+
"owner": "verify",
|
|
216
|
+
"triggers": [
|
|
217
|
+
"verify implementation"
|
|
218
|
+
],
|
|
219
|
+
"excludes": [],
|
|
220
|
+
"outputs": [
|
|
221
|
+
"verification report"
|
|
222
|
+
],
|
|
223
|
+
"neighbors": [
|
|
224
|
+
"superflow-implement",
|
|
225
|
+
"superflow-archive"
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
@@ -22,6 +22,14 @@ When ready to implement, run /opsx:apply
|
|
|
22
22
|
|
|
23
23
|
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
|
24
24
|
|
|
25
|
+
When this skill follows `superflow-clarify`, read the confirmed change-level
|
|
26
|
+
Consensus Summary and its source evidence before creating artifacts. Put the
|
|
27
|
+
goal, scope, material owner decisions, rejected alternatives, migration risks,
|
|
28
|
+
and non-blocking reopen triggers in canonical `proposal.md`; cite the existing
|
|
29
|
+
source audit rather than copying it. A pending owner decision that changes the
|
|
30
|
+
solution blocks downstream spec/design/tasks. An explicit decision in the
|
|
31
|
+
user's original request needs no second confirmation.
|
|
32
|
+
|
|
25
33
|
### OpenSpec canonical change 目录(阻塞级)
|
|
26
34
|
|
|
27
35
|
- change 必须只有一个实体目录:`openspec/changes/<name>/`。
|
|
@@ -108,6 +116,12 @@ After completing all artifacts, summarize:
|
|
|
108
116
|
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
|
109
117
|
- The schema defines what each artifact should contain - follow it
|
|
110
118
|
- Read dependency artifacts for context before creating new ones
|
|
119
|
+
- For a Superflow change, keep ordinary OpenSpec checkbox task syntax. For each
|
|
120
|
+
independently verifiable task, state its observable delivery and acceptance
|
|
121
|
+
signal; record real predecessor task IDs as `Blocked by` and link the relevant
|
|
122
|
+
requirement/scenario or traceability row. A free-text blocker does not change
|
|
123
|
+
Superflow's `[local_required]` / `[environment_required]` /
|
|
124
|
+
`[release_required]` machine classification. Do not invent dependencies.
|
|
111
125
|
- Use `template` as the structure for your output file - fill in its sections
|
|
112
126
|
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
|
113
127
|
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
|
@@ -13,7 +13,7 @@ any full SDD document or implementation prompt is generated.
|
|
|
13
13
|
|
|
14
14
|
- Before freezing any new requirement or bug fix, perform platform-level impact
|
|
15
15
|
discovery. Prefer understand-anything artifacts when present, such as
|
|
16
|
-
`.understand-anything/`, `understand-anything.md`, `understand anything.md`,
|
|
16
|
+
`.ua/`, `.understand-anything/`, `understand-anything.md`, `understand anything.md`,
|
|
17
17
|
or the project-specific graph output, but treat them only as locator evidence.
|
|
18
18
|
If artifacts are missing, stale, incomplete, or unavailable, degrade to `rg`,
|
|
19
19
|
source reading, Mapper/XML, configuration, database table names, log
|
|
@@ -171,6 +171,28 @@ Use the detailed workflow in:
|
|
|
171
171
|
indexed feature. Do not batch-confirm multiple features from memory or a
|
|
172
172
|
compressed chat summary.
|
|
173
173
|
|
|
174
|
+
### Change-level alignment before OpenSpec artifacts
|
|
175
|
+
|
|
176
|
+
After all in-scope features are frozen, present one concise change-level
|
|
177
|
+
`Consensus Summary` covering the goal, in/out scope, verified source facts,
|
|
178
|
+
owner decisions with rejected alternatives, production and migration risks,
|
|
179
|
+
and remaining non-blocking questions with owner and reopen trigger. Derive it
|
|
180
|
+
from `source-code-audit.md`, `feature-gates.md`, and the confirmed decisions in
|
|
181
|
+
`.sdd/handoff/brainstorm-summary.md`; the summary is a handoff view, not a
|
|
182
|
+
second source of requirements. Record unresolved decisions as blockers and
|
|
183
|
+
ask only the next owner decision. Do not ask the user to reconfirm facts or
|
|
184
|
+
choices already explicit in the original request.
|
|
185
|
+
|
|
186
|
+
When a material owner choice was made during clarification, show the current
|
|
187
|
+
summary before generating OpenSpec artifacts. Reuse an explicit confirmation
|
|
188
|
+
already given for that exact choice and summary; ask once only if the summary
|
|
189
|
+
introduces a new choice or changes its consequence. The later `proposal.md`
|
|
190
|
+
must carry the confirmed change-level
|
|
191
|
+
decisions and cite their evidence. If a later finding changes a confirmed
|
|
192
|
+
choice, return to that decision, update affected canonical documents, refresh
|
|
193
|
+
the handoff hash and repeat document review and Coding Ready. Task checkbox
|
|
194
|
+
progress alone does not change the agreed contract.
|
|
195
|
+
|
|
174
196
|
## Handoff
|
|
175
197
|
|
|
176
198
|
When the feature is frozen, tell the user to continue with `$openspec-propose`
|
|
@@ -64,7 +64,12 @@ Superpowers own source-level HOW without weakening OpenSpec/SDD contracts.
|
|
|
64
64
|
5. Write:
|
|
65
65
|
`docs/superpowers/specs/YYYY-MM-DD-<change-id>-technical-design.md`
|
|
66
66
|
using `../superflow-pipeline/references/superpower-technical-design-template.md`.
|
|
67
|
-
6.
|
|
67
|
+
6. For each new or changed module interface, add a `Design-It-Twice` card to
|
|
68
|
+
the technical design: two materially different interfaces, seam and adapter
|
|
69
|
+
ownership, the real test surface, reuse evidence, caller consequences, and
|
|
70
|
+
an explicit choice. Renaming the same interface does not count as two
|
|
71
|
+
options; revisit it. This card is advisory and does not add a state gate.
|
|
72
|
+
7. Complete `复杂度减法评审` before expanding source-level HOW. For every new
|
|
68
73
|
table, field, API, Service/component, abstraction, cache, async/MQ/event
|
|
69
74
|
flow, scheduled job, or compatibility layer, prove existing reuse options,
|
|
70
75
|
necessity, and why the simplest direct implementation is insufficient.
|
|
@@ -37,6 +37,12 @@ Do not invent new fields or requirements.
|
|
|
37
37
|
|
|
38
38
|
For complex requirements, verify these before writing docs:
|
|
39
39
|
|
|
40
|
+
- `proposal.md` carries the confirmed change-level Consensus Summary from
|
|
41
|
+
clarification: goal, scope, owner choices and rejected alternatives,
|
|
42
|
+
production/migration risks, and non-blocking reopen triggers. Check it
|
|
43
|
+
against original sources and `source-code-audit.md`; do not treat the
|
|
44
|
+
handoff summary as a second authority. If a material choice remains open,
|
|
45
|
+
return to `$superflow-clarify` before finalizing design or tasks.
|
|
40
46
|
- `source-ingestion.md`, `feature-inventory.md`, and `feature-gates.md` prove
|
|
41
47
|
that the original PRD/Lark/Feishu/screenshots were processed section by
|
|
42
48
|
section, not summarized into tasks in one whole-document pass. Each feature
|
|
@@ -240,6 +246,12 @@ applicable:
|
|
|
240
246
|
- `tasks.md` 每个 checkbox 必须带且只带一个机器分类:`[local_required]`、
|
|
241
247
|
`[environment_required]` 或 `[release_required]`。能够在本地完成的编码、测试、启动和浏览器
|
|
242
248
|
验证必须是 local_required;不得用自由文本 Blocker/owner 代替分类。
|
|
249
|
+
- For each implementation task, review observable delivery, acceptance signal,
|
|
250
|
+
requirement/scenario coverage, and actual predecessors. Show the currently
|
|
251
|
+
executable tasks whose predecessors are complete. Reuse
|
|
252
|
+
`traceability-matrix.md` for detailed coverage; do not create another task
|
|
253
|
+
state file. `Blocked by` expresses execution order and never changes the
|
|
254
|
+
machine task classification above.
|
|
243
255
|
- `tests.md`
|
|
244
256
|
- `test-report.md` skeleton
|
|
245
257
|
- `sdd-quality-gate.md`
|
|
@@ -368,6 +380,18 @@ DB 状态 | 结算/通知/展示消费点 | 真实验证方式`.
|
|
|
368
380
|
`用例ID | 需求/Scenario | 层级L1/L2/L3/L4 | 前置数据 | 操作步骤 |
|
|
369
381
|
自动化命令 | 响应断言 | DB断言 | 日志断言 | RED预期失败 | GREEN预期通过 |
|
|
370
382
|
test-report证据位置`.
|
|
383
|
+
- For cross-page management or database-backed display changes, reconcile the
|
|
384
|
+
frontend action/list inventory with real Controller, Service, exact Mapper
|
|
385
|
+
statement and table anchors before freezing tasks. Put the confirmed chain
|
|
386
|
+
and entry ID in `source-code-audit.md` and reuse it in `design.md`, `tasks.md`
|
|
387
|
+
and `tests.md`. A similarly named query is not evidence for the page route.
|
|
388
|
+
Include every visible mutation button, such as enable/disable, as an entry
|
|
389
|
+
even when the original task list omitted it.
|
|
390
|
+
- For a returned persisted field, `tests.md` must freeze at least one non-null
|
|
391
|
+
fixture and an allowed historical-NULL fixture separately. Assert API and DB
|
|
392
|
+
values for the same record ID, then browser rendering where a page is in
|
|
393
|
+
scope. Key-presence, HTTP 200, empty-page and NULL-only assertions cannot
|
|
394
|
+
close the non-null data path. Name the exact query/command and evidence file.
|
|
371
395
|
- 对 L3/L4 的输入校验、协议拒绝或异常分支,`tests.md` 还必须冻结真实注入载荷及其到达
|
|
372
396
|
目标解析/校验分支的证据;仅断言同一个 HTTP 状态码不算覆盖。原始畸形 JSON、错误
|
|
373
397
|
Content-Type、越权身份或非法字段必须按真实线协议发送,并同时断言无副作用。
|