@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
@@ -14,6 +14,7 @@ mechanically:
14
14
  from __future__ import annotations
15
15
 
16
16
  import argparse
17
+ import hashlib
17
18
  import json
18
19
  import re
19
20
  import subprocess
@@ -261,7 +262,265 @@ def lint_against_tests_contract(report_path: Path, report_text: str, tests_path:
261
262
  return issues
262
263
 
263
264
 
264
- def lint_case_evidence(report_path: Path, report_text: str, review_path: Path) -> list[Issue]:
265
+ def final_report_result(text: str) -> str:
266
+ # Only the latest explicit summary per language is authoritative. Historical
267
+ # notes and fenced examples do not invalidate the final execution ledger.
268
+ text = re.sub(r"```.*?```", "", text, flags=re.S)
269
+ results = {}
270
+ for match in re.finditer(r"^(验证结果|Verification Result)\s*[::]\s*([A-Z_]+)\s*$", text, re.I | re.M):
271
+ results[match[1].lower()] = match[2].upper()
272
+ if len(set(results.values())) > 1:
273
+ return "CONFLICT"
274
+ return next(iter(results.values()), "MISSING")
275
+
276
+
277
+ def verified_file(base: Path, value: str) -> Path:
278
+ """Local retained artifacts only. Hashes prove identity, not truthfulness."""
279
+ if not isinstance(value, str) or not value.strip() or re.match(r"^[a-z]+://", value, re.I):
280
+ raise ValueError("需要本地可读取的执行证据,远端 URL 不能作为通过凭据")
281
+ file = (base / value).resolve()
282
+ if not file.is_file() or file.stat().st_size == 0:
283
+ raise ValueError(f"证据文件不存在或无内容:{value}")
284
+ return file
285
+
286
+
287
+ def hashed_file(base: Path, value: str, sha256: str) -> Path:
288
+ file = verified_file(base, value)
289
+ if not isinstance(sha256, str) or not re.fullmatch(r"[a-f0-9]{64}", sha256):
290
+ raise ValueError(f"缺少有效 SHA-256:{value}")
291
+ if hashlib.sha256(file.read_bytes()).hexdigest() != sha256:
292
+ raise ValueError(f"证据指纹不一致或源码已变化:{value}")
293
+ return file
294
+
295
+
296
+ def json_equal(left, right) -> bool:
297
+ """JSON numbers are distinct from booleans, recursively at every level."""
298
+ if type(left) in (int, float) and type(right) in (int, float):
299
+ return left == right
300
+ if type(left) is not type(right):
301
+ return False
302
+ if isinstance(left, dict):
303
+ return left.keys() == right.keys() and all(json_equal(left[k], right[k]) for k in left)
304
+ if isinstance(left, list):
305
+ return len(left) == len(right) and all(json_equal(a, b) for a, b in zip(left, right))
306
+ return left == right
307
+
308
+
309
+ def validate_database_receipt(receipt: dict, event: dict, case: dict,
310
+ entry: dict, coverage: dict, change: Path,
311
+ paths: set) -> None:
312
+ """Compare frozen database obligations, never infer SQL semantics."""
313
+ layer = case.get("testLayer")
314
+ database = entry.get("database")
315
+ if (entry.get("persistence") or case.get("assertions", {}).get("persistence")) and not database:
316
+ raise ValueError("旧持久化 PASS 缺少数据库合同,应保留为 PARTIAL")
317
+ for key in ("testLayer", "mockBoundary", "database"):
318
+ if not json_equal(event.get(key), receipt.get(key)):
319
+ raise ValueError(f"原始执行输出与凭证 {key} 不一致")
320
+ if layer is not None and receipt.get("testLayer") != layer:
321
+ raise ValueError("testLayer 与冻结合同不一致")
322
+ if database and layer not in {"logic", "sql-binding", "database", "http-entry"}:
323
+ raise ValueError("数据库义务缺少 testLayer,应保留 PARTIAL")
324
+ if layer not in {"database", "http-entry"} or not database:
325
+ return
326
+ if case.get("evidenceKind") != "real" or receipt.get("evidenceKind") != "real":
327
+ raise ValueError("数据库义务必须使用 real 证据")
328
+ actual = receipt.get("database", {})
329
+ for key in ("engine", "major"):
330
+ if not json_equal(actual.get(key), database.get(key)):
331
+ raise ValueError(f"数据库 {key} 与冻结合同不一致;替代引擎不能 PASS")
332
+ boundary = case.get("mockBoundary")
333
+ if not boundary or not json_equal(receipt.get("mockBoundary"), boundary):
334
+ raise ValueError("mockBoundary 与冻结合同不一致")
335
+ if (not isinstance(boundary.get("allowed"), list)
336
+ or not boundary.get("forbidden")
337
+ or set(boundary["allowed"]) & set(boundary["forbidden"])):
338
+ raise ValueError("必测生产链不得 mock")
339
+ statements = case.get("statements")
340
+ if not statements or not json_equal(actual.get("statements"), statements):
341
+ raise ValueError("生产 SQL statement 身份与冻结用例不一致")
342
+ source_index = {s["id"]: s for s in coverage.get("sources", [])}
343
+ refs = database.get("schemaSourceRefs", []) + [s["sourceRef"] for s in statements]
344
+ if not database.get("schemaSourceRefs"):
345
+ raise ValueError("数据库合同缺少 schema 来源")
346
+ for ref in refs:
347
+ source = source_index[ref]
348
+ source_path = hashed_file(change, source["path"], source["sha256"])
349
+ if source_path not in paths:
350
+ raise ValueError("数据库 schema/生产 SQL 来源未纳入执行源码指纹")
351
+ expected = case.get("databaseAssertions")
352
+ assertions = actual.get("assertions")
353
+ if not expected or not isinstance(assertions, list) or len(assertions) != len(expected):
354
+ raise ValueError("缺少冻结数据库数据断言")
355
+ for frozen, observed in zip(expected, assertions):
356
+ if (any(not json_equal(observed.get(k), frozen.get(k)) for k in ("id", "kind", "expected"))
357
+ or "actual" not in observed or observed.get("result") != "PASS"):
358
+ raise ValueError("数据库数据/影响行数/不变断言未达到冻结预期")
359
+ wanted, found = frozen["expected"], observed["actual"]
360
+ if frozen["kind"] == "unchangedRows":
361
+ if (not isinstance(wanted, dict) or not wanted
362
+ or any(not isinstance(row, dict) or not row for row in wanted.values())
363
+ or not isinstance(found, dict) or set(found) != {"before", "after"}
364
+ or not json_equal(found["before"], wanted) or not json_equal(found["after"], wanted)):
365
+ raise ValueError("非目标/旧轮次缺少 keyed before/after 快照或值发生变化")
366
+ elif not json_equal(found, wanted):
367
+ raise ValueError("数据库数据/影响行数/不变断言未达到冻结预期")
368
+
369
+
370
+ def validate_execution_receipt(file: Path, case: dict, coverage: dict, change: Path) -> None:
371
+ receipt = json.loads(file.read_text(encoding="utf-8"))
372
+ if receipt.get("schemaVersion") != "superflow.execution-receipt.v1":
373
+ raise ValueError("缺少 execution-receipt.v1;旧证据应保留为 PARTIAL")
374
+ for key, expected in [("caseId", case["id"]), ("entryId", case["entryId"]),
375
+ ("level", case["level"]), ("status", "PASS")]:
376
+ if receipt.get(key) != expected:
377
+ raise ValueError(f"执行凭证 {key} 与冻结用例不一致")
378
+ if type(receipt.get("executed")) is not int or receipt["executed"] < 1:
379
+ raise ValueError("用例零执行/占位不能 PASS")
380
+ kind = case.get("evidenceKind", "real")
381
+ if kind not in {"real", "controlled-simulation", "unit"} or receipt.get("evidenceKind") != kind:
382
+ raise ValueError("执行证据等级与冻结合同不一致;模拟不能替代真实入口")
383
+ command = receipt["command"]
384
+ if (not isinstance(command.get("argv"), list) or not command["argv"]
385
+ or any(not isinstance(arg, str) or not arg.strip() for arg in command["argv"])
386
+ or type(command.get("exitCode")) is not int or command["exitCode"] != 0):
387
+ raise ValueError("缺少真实命令或命令退出码非零")
388
+ output = hashed_file(file.parent, command["output"], command["sha256"])
389
+ events = []
390
+ for line in output.read_text(encoding="utf-8").splitlines():
391
+ try:
392
+ event = json.loads(line)
393
+ if isinstance(event, dict) and event.get("caseId") == case["id"]:
394
+ events.append(event)
395
+ except ValueError:
396
+ continue
397
+ if len(events) != 1:
398
+ raise ValueError("原始输出缺少唯一用例执行事件;未知 ID/零执行不可 PASS")
399
+ event = events[0]
400
+ for key in ["caseId", "entryId", "level", "status", "executed", "assertions", "persistence",
401
+ "evidenceKind", "sources", "build", "target"]:
402
+ if not json_equal(event.get(key), receipt.get(key)):
403
+ raise ValueError(f"原始执行输出与凭证 {key} 不一致")
404
+ if not json_equal(event.get("command"), {"argv": command["argv"], "exitCode": command["exitCode"]}):
405
+ raise ValueError("原始执行命令与凭证不一致")
406
+ sources = receipt["sources"]
407
+ if not isinstance(sources, list) or not sources:
408
+ raise ValueError("缺少当前源码/构建指纹")
409
+ paths = set()
410
+ source_tokens = []
411
+ for source in sources:
412
+ paths.add(hashed_file(file.parent, source["path"], source["sha256"]))
413
+ source_tokens.append(source["path"] + ":" + source["sha256"])
414
+ entry = next((e for e in coverage.get("entries", []) if e["id"] == case["entryId"]), None)
415
+ if entry is None:
416
+ raise ValueError("执行凭证缺少冻结入口定义")
417
+ source_index = {s["id"]: s for s in coverage.get("sources", [])}
418
+ for ref in entry.get("sourceRefs", []):
419
+ source = source_index[ref]
420
+ if source.get("role") == "code" and (change / source["path"]).resolve() not in paths:
421
+ raise ValueError("执行凭证未覆盖该入口冻结的源码写入链")
422
+ validate_database_receipt(receipt, event, case, entry, coverage, change, paths)
423
+ build = receipt["build"]
424
+ target = receipt["target"]
425
+ fingerprint = hashlib.sha256("\n".join(sorted(source_tokens)).encode()).hexdigest()
426
+ if build.get("sourceFingerprint") != fingerprint or target.get("sourceFingerprint") != fingerprint:
427
+ raise ValueError("构建与运行目标未绑定当前源码指纹")
428
+ hashed_file(file.parent, build["artifact"], build["sha256"])
429
+ build_command = build["command"]
430
+ if (not isinstance(build_command.get("argv"), list) or not build_command["argv"]
431
+ or type(build_command.get("exitCode")) is not int or build_command["exitCode"] != 0):
432
+ raise ValueError("缺少成功构建命令")
433
+ build_output = hashed_file(file.parent, build_command["output"], build_command["sha256"])
434
+ expected_build = {"event": "build", "buildId": build["id"], "sourceFingerprint": fingerprint,
435
+ "artifactSha256": build["sha256"], "argv": build_command["argv"], "exitCode": 0}
436
+ build_events = []
437
+ for line in build_output.read_text(encoding="utf-8").splitlines():
438
+ try:
439
+ value = json.loads(line)
440
+ if isinstance(value, dict) and value.get("event") == "build" and value.get("buildId") == build["id"]:
441
+ build_events.append(value)
442
+ except ValueError:
443
+ continue
444
+ if len(build_events) != 1 or not json_equal(build_events[0], expected_build):
445
+ raise ValueError("构建原始输出未关联当前源码与构建产物")
446
+ if target.get("buildSha256") != build["sha256"]:
447
+ raise ValueError("运行目标不是本次验证的构建产物")
448
+ if not isinstance(build.get("id"), str) or not build["id"].strip() or target.get("buildId") != build["id"]:
449
+ raise ValueError("真实运行目标与构建指纹不一致")
450
+ for key, expected in [("service", entry.get("service", entry["id"])),
451
+ ("route", entry["route"]), ("kind", case["level"])]:
452
+ if target.get(key) != expected:
453
+ raise ValueError(f"真实运行目标 {key} 与冻结入口不一致")
454
+ assertions = receipt["assertions"]
455
+ required = set(case.get("assertions", {})) - {"persistence"}
456
+ required |= {"response", "state", "forbiddenEffects"}
457
+ if not isinstance(assertions, list) or {a["id"] for a in assertions} != required or len(assertions) != len(required):
458
+ raise ValueError("缺少冻结响应/状态/禁止副作用断言")
459
+ for assertion in assertions:
460
+ if (assertion.get("result") != "PASS" or "expected" not in assertion
461
+ or "actual" not in assertion or not json_equal(assertion["expected"], assertion["actual"])):
462
+ raise ValueError("断言失败或缺少预期值/实际值")
463
+ expected_writes = case.get("assertions", {}).get("persistence", [])
464
+ writes = receipt.get("persistence", [])
465
+ if not isinstance(writes, list) or len(writes) != len(expected_writes):
466
+ raise ValueError("缺少持久化字段同业务 ID 的调用前后对照")
467
+ for expected, actual in zip(expected_writes, writes):
468
+ for key in ["id", "table", "field", "expected"]:
469
+ if key not in actual or not json_equal(actual[key], expected[key]):
470
+ raise ValueError("持久化断言与冻结字段/预期值不一致")
471
+ before, after = actual["before"], actual["after"]
472
+ if (not before.get("businessId") or before["businessId"] != after.get("businessId")
473
+ or "value" not in before or "value" not in after
474
+ or not json_equal(after["value"], expected["expected"])
475
+ or (after["value"] is None and not expected.get("allowNull", False))
476
+ or actual.get("result") != "PASS"):
477
+ raise ValueError("持久化值未达到预期;结构存在/全 NULL/跨业务 ID 不能替代写入")
478
+
479
+
480
+ def check_git_index_files(root: Path, files: list[Path]) -> None:
481
+ """Compare version-controlled dependencies with the actual commit snapshot.
482
+
483
+ Untracked/ignored local evidence may remain private. This does not require
484
+ publishing logs, and does not replace review of source/build applicability.
485
+ """
486
+ root = root.resolve()
487
+ for file in set(files):
488
+ file = file.resolve()
489
+ try:
490
+ relative = file.relative_to(root).as_posix()
491
+ except ValueError:
492
+ continue
493
+ tracked = subprocess.run(["git", "-C", str(root), "ls-files", "--error-unmatch", "--", relative],
494
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
495
+ if tracked.returncode:
496
+ previous = subprocess.run(["git", "-C", str(root), "cat-file", "-e", "HEAD:" + relative],
497
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
498
+ if previous.returncode:
499
+ continue
500
+ indexed = subprocess.run(["git", "-C", str(root), "rev-parse", ":" + relative], capture_output=True)
501
+ working = subprocess.run(["git", "-C", str(root), "hash-object", "--path=" + relative, "--stdin"],
502
+ input=file.read_bytes(), capture_output=True)
503
+ if indexed.returncode or working.returncode or indexed.stdout.strip() != working.stdout.strip():
504
+ raise ValueError(f"Git index 与报告依赖工作树不一致:{relative};请暂存已验证快照")
505
+
506
+
507
+ def check_receipt_index(file: Path, report: Path, review: Path, root: Path) -> None:
508
+ receipt = json.loads(file.read_text())
509
+ dependencies = [file, report, review]
510
+ command = receipt["command"]
511
+ build = receipt["build"]
512
+ dependencies.extend((file.parent / value).resolve() for value in
513
+ [command["output"], build["artifact"], build["command"]["output"]])
514
+ dependencies.extend((file.parent / source["path"]).resolve() for source in receipt["sources"])
515
+ coverage = json.loads(review.read_text())["coverage"]
516
+ dependencies.extend((report.parent / source["path"]).resolve() for source in coverage.get("sources", []))
517
+ tests = infer_tests_path(report)
518
+ if tests is not None:
519
+ dependencies.append(tests)
520
+ check_git_index_files(root, dependencies)
521
+
522
+
523
+ def lint_case_evidence(report_path: Path, report_text: str, review_path: Path, index_root: Path | None = None) -> list[Issue]:
265
524
  """Check the frozen R/E/C index against per-case execution receipts."""
266
525
  issues: list[Issue] = []
267
526
  try:
@@ -280,6 +539,8 @@ def lint_case_evidence(report_path: Path, report_text: str, review_path: Path) -
280
539
  if decision.get("disposition") != "EXCLUDED"
281
540
  for case_id in decision.get("caseIds", [])
282
541
  }
542
+ if final_report_result(report_text) == "CONFLICT":
543
+ issues.append(Issue("FAIL", report_path, 0, "中英文最终验证结果冲突"))
283
544
  lines = report_text.splitlines()
284
545
  header_at = next(
285
546
  (i for i, line in enumerate(lines) if CASE_EVIDENCE_HEADER.fullmatch(line.strip())),
@@ -311,13 +572,15 @@ def lint_case_evidence(report_path: Path, report_text: str, review_path: Path) -
311
572
  issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 的入口或验收级别与冻结合同不一致"))
312
573
  if result not in CASE_RESULTS or not evidence or evidence.lower() in {"pending", "todo", "待补"}:
313
574
  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):
575
+ elif result == "PASS":
576
+ try:
577
+ evidence_file = verified_file(report_path.parent, evidence.split("#", 1)[0])
578
+ validate_execution_receipt(evidence_file, case or {}, coverage, report_path.parent)
579
+ if index_root is not None:
580
+ check_receipt_index(evidence_file, report_path, review_path, index_root)
581
+ except (OSError, ValueError, KeyError, TypeError, AttributeError) as error:
582
+ issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 的通过证据不可验证:{error}"))
583
+ if result != "PASS" and final_report_result(report_text) == "PASS":
321
584
  issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 为 {result},报告不能声明整体验证 PASS"))
322
585
  for case_id in receipts.keys() - required:
323
586
  issues.append(Issue("FAIL", report_path, 0, f"用例 {case_id} 未在冻结入口评审中要求执行"))
@@ -340,7 +603,7 @@ def infer_tests_path(report: Path) -> Path | None:
340
603
 
341
604
 
342
605
  def lint_report(path: Path, repo: Path | None, tests_path: Path | None = None,
343
- review_path: Path | None = None) -> list[Issue]:
606
+ review_path: Path | None = None, index_root: Path | None = None) -> list[Issue]:
344
607
  issues: list[Issue] = []
345
608
  text = path.read_text(encoding="utf-8", errors="replace")
346
609
  lines = text.splitlines()
@@ -349,11 +612,19 @@ def lint_report(path: Path, repo: Path | None, tests_path: Path | None = None,
349
612
  db_backed = bool(DB_BACKED_RE.search(text))
350
613
  blocked = bool(BLOCKED_RE.search(text))
351
614
  passed_or_partial = bool(PASS_WORDS.search(text) or re.search(r"Partially verified|部分验证", text, re.I))
352
- issues.extend(lint_against_tests_contract(path, text, tests_path or infer_tests_path(path)))
615
+ adjacent_review = path.parent / ".sdd/reviews/document-review.json"
616
+ if review_path is None and adjacent_review.is_file():
617
+ review_path = adjacent_review
618
+ state_path = path.parent / ".sdd/state.yaml"
619
+ full = state_path.is_file() and re.search(r"^workflow:\s*[\"']?full[\"']?\s*$", state_path.read_text(), re.M)
620
+ if full and review_path is None:
621
+ issues.append(Issue("FAIL", path, 0, "完整 SDD 缺少冻结入口评审凭证,不能以旧关键词报告交付"))
353
622
  if review_path is not None:
354
- issues.extend(lint_case_evidence(path, text, review_path))
623
+ issues.extend(lint_case_evidence(path, text, review_path, index_root))
624
+ else:
625
+ issues.extend(lint_against_tests_contract(path, text, tests_path or infer_tests_path(path)))
355
626
 
356
- if db_backed and passed_or_partial and not blocked:
627
+ if review_path is None and db_backed and passed_or_partial and not blocked:
357
628
  if not TABLE_REVERSE_HEADER_RE.search(text):
358
629
  issues.append(
359
630
  Issue(
@@ -471,6 +742,8 @@ def main() -> int:
471
742
  parser.add_argument("--tests", default="")
472
743
  parser.add_argument("--review", default="")
473
744
  parser.add_argument("--repo-root", default="")
745
+ parser.add_argument("--git-index-root", default="")
746
+ parser.add_argument("--require-complete", action="store_true")
474
747
  parser.add_argument("--warn-only", action="store_true")
475
748
  args = parser.parse_args()
476
749
 
@@ -485,7 +758,11 @@ def main() -> int:
485
758
  if tests_path is not None and not tests_path.exists():
486
759
  all_issues.append(Issue("FAIL", path, 0, f"显式指定的 tests.md 不存在:{tests_path}"))
487
760
  review_path = Path(args.review).resolve() if args.review else None
488
- all_issues.extend(lint_report(path, repo, tests_path, review_path))
761
+ all_issues.extend(lint_report(path, repo, tests_path, review_path, Path(args.git_index_root) if args.git_index_root else None))
762
+ if args.require_complete:
763
+ text = path.read_text(encoding="utf-8")
764
+ if final_report_result(text) != "PASS":
765
+ all_issues.append(Issue("FAIL", path, 0, "完整交付要求最终验证结果 PASS;局部证据保留为 PARTIAL"))
489
766
 
490
767
  printable = []
491
768
  for issue in all_issues:
@@ -58,17 +58,12 @@ for REPORT in "$@"; do
58
58
  check_pattern "$REPORT" "证据等级/真实验证结论" \
59
59
  "Real integration passed|Partial real entry|Partially verified|Blocked|真实入口|API 集成|冒烟|Smoke"
60
60
 
61
- if [ -x "$HOME/.codex/hooks/superflow-test-report-lint.py" ]; then
62
- "$HOME/.codex/hooks/superflow-test-report-lint.py" "$REPORT"
63
- if [ $? -ne 0 ]; then
64
- FAILED=1
65
- fi
66
- fi
67
-
68
- if grep -Eiq "跳过|未执行|待补充|TODO|后续测试|建议.*测试|是否.*测试" "$REPORT"; then
69
- echo "FAIL [$REPORT] 存在跳过/待补充/后续测试类表述,不能标记完成"
61
+ # Always use the linter from this installation, on both Codex and Claude.
62
+ LINT="$(dirname "${BASH_SOURCE[0]}")/superflow-test-report-lint.py"
63
+ if [ ! -f "$LINT" ] || ! python3 "$LINT" --require-complete "$REPORT"; then
70
64
  FAILED=1
71
65
  fi
66
+
72
67
  done
73
68
 
74
69
  if [ "$FAILED" -ne 0 ]; then
@@ -169,3 +169,9 @@ Impact` and prove writers, readers, filters, derived sync paths, consumers,
169
169
  Do not continue to `$superflow-implement` until the design guard passes. The next
170
170
  phase may generate prompts, but this phase must not skip any required SDD
171
171
  contract document or replace API/DB/tests with Superpowers prose.
172
+
173
+ ### 数据库分层证据
174
+
175
+ 按 [数据库合同](../superflow-pipeline/references/document-review-coverage.md#数据库分层合同)
176
+ 冻结并继承 `testLayer`、entry.database、生产 statement、mockBoundary 和数据断言。
177
+ 确定 schema 来源、引擎主版本、生产 Service/Mapper 边界、影响行数和非目标/旧轮次隔离断言。
@@ -486,3 +486,12 @@ DB 状态 | 结算/通知/展示消费点 | 真实验证方式`.
486
486
 
487
487
  When SDD docs pass quality gates, continue with `$superflow-design` for Superpowers
488
488
  technical design before prompt splitting or coding orchestration.
489
+
490
+
491
+ 逐用例 PASS 必须提供可重放本地执行凭证;冻结服务/入口、源码写入链与持久化前后值断言。沿用 pipeline/references/document-review-coverage.md 的 execution-receipt.v1 合同,旧证据保留 PARTIAL;独立评审核对精确 Mapper statement 与原始输出,完整交付运行当前安装的 lint --require-complete。哈希不等同防伪或业务充分性。
492
+
493
+ ### 数据库分层证据
494
+
495
+ 按 [数据库合同](../superflow-pipeline/references/document-review-coverage.md#数据库分层合同)
496
+ 冻结并继承 `testLayer`、entry.database、生产 statement、mockBoundary 和数据断言。
497
+ 生成文档后用必要DB仅Mock反例和真实生产SQL正例审查;辅助层不强迫DB。
@@ -484,3 +484,12 @@ Use a CR/Px prompt when frontend integration discovers:
484
484
  - SQL/schema drift
485
485
 
486
486
  CR/Px prompts must include the real frontend payload, screenshot or page path, error response, and validation evidence required after the fix.
487
+
488
+
489
+ 逐用例 PASS 必须提供可重放本地执行凭证;冻结服务/入口、源码写入链与持久化前后值断言。沿用 pipeline/references/document-review-coverage.md 的 execution-receipt.v1 合同,旧证据保留 PARTIAL;独立评审核对精确 Mapper statement 与原始输出,完整交付运行当前安装的 lint --require-complete。哈希不等同防伪或业务充分性。
490
+
491
+ ### 数据库分层证据
492
+
493
+ 按 [数据库合同](../superflow-pipeline/references/document-review-coverage.md#数据库分层合同)
494
+ 冻结并继承 `testLayer`、entry.database、生产 statement、mockBoundary 和数据断言。
495
+ 只执行冻结层级和断言;DB环境缺失报告PARTIAL/BLOCKED,不能自行用替代引擎或Mock补齐。
@@ -92,9 +92,66 @@ coding-ready 复检失败会写 BLOCKED 撤销旧 READY。
92
92
 
93
93
  | 用例 ID | 入口 ID | 验收级别 | 结果 | 证据路径 |
94
94
  |---|---|---|---|---|
95
- | C1 | E1 | browser | PASS | logs/C1-trace.zip |
95
+ | C1 | E1 | browser | PASS | logs/C1-receipt.json |
96
96
 
97
97
  结果只用 `PASS/FAIL/BLOCKED/PARTIAL`;证据路径指向实际命令输出、浏览器 trace、
98
- 响应或数据库对账记录。PASS 的本地证据文件必须保留到验收并可读取;远端证据可用
99
- HTTP(S) 链接。阻塞项在报告正文写明原因。表格仅证明用例逐项回填,不能证明
98
+ 响应或数据库对账记录。PASS 的本地证据文件必须保留到验收并可读取;远端资料须保存为本地可核验证据。阻塞项在报告正文写明原因。表格仅证明用例逐项回填,不能证明
100
99
  证据内容正确。任何非 PASS 用例存在时,不得声明整体验证 PASS。
100
+
101
+ ## 可重放执行凭证(0.5.17)
102
+
103
+ PASS 的证据路径现在必须指向本地 `superflow.execution-receipt.v1` JSON;不得以空文件、任意文本、远端 URL 或测试总数代替。旧报告保留 PARTIAL,补齐受影响证据后再整体 PASS。
104
+
105
+ 凭证字段:`caseId/entryId/level/status=PASS/executed>=1/evidenceKind`;`command{argv,exitCode=0,output,sha256}`;`sources[{path,sha256}]`;`build{id,artifact,sha256,sourceFingerprint}`;`target{service,route,kind,buildId,buildSha256,sourceFingerprint}`;`assertions[{id,expected,actual,result=PASS}]`。路径相对凭证文件,可使用授权跨仓源码;输出需保留可读取的原始日志。源码指纹为 sources 按 `path:sha256` 排序后以换行连接的 SHA-256(末尾无换行)。构建与运行目标必须关联同一指纹和产物;Agent 仍核对构建命令与实际进程/镜像。
106
+
107
+ 命令输出中每个用例包含一个 JSON 行执行事件,携带 `caseId/entryId/level/status/executed/assertions/persistence`,与凭证相同。普通日志可混排;未知 ID、零执行、SKIP、占位或缺少断言事件不可 PASS。一个日志可包含多个用例,各凭证分别定位自己的事件。
108
+
109
+ 冻结入口可增加 `service`(默认 entryId)、`persistence:[{table,field}]`;用例默认 `evidenceKind=real`,只有冻结合同明确允许时才填 `controlled-simulation` 或 `unit`。两个服务写同表也必须分别登记入口和源码链,不得交换证据。
110
+
111
+ 持久化写入用例在 `assertions.persistence` 冻结 `[{id,table,field,expected,allowNull?}]`,凭证/原始事件同名数组追加 `before{businessId,value}`、`after{businessId,value}`、`result`。业务 ID 一致且 after.value 等于冻结预期;新增/改字段用非空预期,历史 NULL 明确 allowNull。字段索引存在、mock 参数赋值、全 NULL 数据不能证明写入。
112
+
113
+ 独立评审必须检查实际 Service 调用的精确 Mapper statement 和所有写入链、运行目标、原始断言及旧通知/新申请隔离;报告的“实现已补齐”必须与源码和运行证据一致。哈希和结构不保证日志真实或断言充分,不能当作防伪签名。该扩展不增加托管协议或状态 owner;纯文档/非行为轻量任务不强加 DB 验收。
114
+
115
+ 原始用例事件还必须携带 evidenceKind/sources/build/target 和 command{argv,exitCode},与凭证完全一致。build.command{argv,exitCode,output,sha256} 引用构建原始日志,其中包含 JSON 行 {event:"build",buildId,sourceFingerprint,artifactSha256,argv,exitCode:0};日志与实际构建/运行仍由独立评审核验。最终汇总仅取最后一个中英文一致的“验证结果/Verification Result”,历史记录与代码块示例不用于晋升。
116
+
117
+ ## 数据库分层合同
118
+
119
+ 本节扩展已有 coverage/receipt v1,不增加 Skill、Hook 或状态 owner。
120
+ 语义评审发现持久化、字段展示、CAS、事务或查询语义影响时,必须在相应入口声明
121
+ `database`;纯逻辑任务明确无持久化影响的理由后可免数据库验证。不能靠 SQL 关键词猜风险。
122
+
123
+ - `case.testLayer` 为 `logic|sql-binding|database|http-entry`,与真实入口 `level` 分开。
124
+ logic 可 Mock 隔离逻辑;sql-binding 只证明 SQL 生成/绑定。必要 case 集合必须至少包含
125
+ 一个 `database` 或 `http-entry` 且 `evidenceKind=real` 的用例;辅助层不强迫连接数据库。
126
+ - `entry.database={engine,major,schemaSourceRefs,statements:[{sourceRef,id}]}` 冻结实际引擎、
127
+ 主版本、schema 来源和精确生产 statement。MySQL 不得被 H2/SQLite 替代。
128
+ schema 与 Mapper XML 引用既有 sources,纳入 receipt 源码指纹;匿名 SQLite 仅验证 harness。
129
+ - 数据库用例冻结 `statements:[{sourceRef,id}]`、
130
+ `mockBoundary={allowed:[外部客户端],forbidden:[必测Service/Mapper]}` 及
131
+ `databaseAssertions:[{id,kind,expected}]`。`kind` 是合同定义的观测类别:
132
+ 写入包括 `affectedRows` 和 `unchangedRows`,后者必须冻结非目标记录和旧轮次实际快照;
133
+ 其他风险由 Agent 按需求冻结断言,不由脚本推测业务规则。
134
+ - receipt 和原始用例事件回填同一 `testLayer`、`mockBoundary`、
135
+ `database={engine,major,statements,assertions:[{id,kind,expected,actual,result}]}`。
136
+ 保留原始命令、构建、入口与源码版本绑定;数据库 assertions 实际值必须等于冻结预期。
137
+ 字段写入继续使用 `assertions.persistence` 与同业务 ID 的 before/after/expected,
138
+ 不能用 Mock 参数、NULL、字段键或影响行数独自证明写入。
139
+ - `http-entry` 若承担 DB 义务,必须走真实 HTTP → 生产 Service/Mapper → 同类型 DB,
140
+ 同时执行返回、持久化和隔离断言;直接 Mapper 回放不能冒充 HTTP 或 browser 验收。
141
+ - 旧非持久化合同保持兼容;旧持久化 PASS 缺新合同保留 PARTIAL。必要 DB 环境不可用时
142
+ 记录 PARTIAL/BLOCKED 和原因,不把辅助 Mock/sql-binding 绿灯晋升为完整 PASS。
143
+ 历史证据绑定固定入口、源码/构建版本和 DB 引擎时仍有效;当前源码改动需要新证据,
144
+ 不得把历史版实际测试误说成没测试,也不能把旧结果用于当前新版本。
145
+
146
+ 正例:逻辑与 SQL 绑定辅助用例 + 真实生产 XML/MySQL 数据断言;纯逻辑有明确无持久化影响理由。
147
+ 反例:必要集合只有 Mock、HTTP 绕过生产 Mapper、引擎/statement/边界/来源错配、
148
+ 漏字段实际 NULL 却填 PASS。文档生成后在 source-contract 与 e2e-environment 两轮独立审查
149
+ 这些正反例;结构检查只确认冻结与观测一致,充分性和真实生产链留给独立 Agent。
150
+
151
+ `unchangedRows.expected` 必须是按稳定业务 ID/轮次键索引的非空行快照对象;
152
+ `actual={before:{业务键:行快照},after:{业务键:行快照}}` 保留两次真实查询值,
153
+ before 与 after 均须匹配冻结 expected。布尔成功标志不能替代快照。
154
+ 所有 JSON 观测递归保留类型:boolean 不等于 number(如 true 不能证明影响行数 1),
155
+ 数组/对象中的字段同样适用;具体业务键与快照字段由文档与独立评审决定。
156
+
157
+ Git 交付路径还会对 tracked 报告依赖(receipt、原始事件、构建、源码、schema/合同)核对标准化 index 对象;仅在工作树修正不能证明待提交快照。未受版本控制的私有证据不强制提交;当前源码/构建适用性仍由独立评审判断。
@@ -76,7 +76,7 @@ mvn test -Dtest=UserServiceTest
76
76
 
77
77
  | 用例 ID | 入口 ID | 验收级别 | 结果 | 证据路径 |
78
78
  |---|---|---|---|---|
79
- | C1 | E1 | browser | PASS | logs/C1-trace.zip |
79
+ | C1 | E1 | browser | PASS | logs/C1-receipt.json |
80
80
 
81
81
  `PASS/FAIL/BLOCKED/PARTIAL` 之外的值无效;非 PASS 项在正文说明原因。
82
82
  数据库字段展示要分别记录非空数据按同一 ID 的 API/DB 值对账、历史 NULL 结果和
@@ -261,3 +261,5 @@ HTTP 200、字段非空、mock-only、DB-only、`BUILD SUCCESS`、`Tests are ski
261
261
  - 不允许把泛化外部失败当作通过,除非需求明确要求该失败结果
262
262
  - 不允许只检查主仓数据库结构就关闭跨仓共享表任务;必须覆盖全部消费仓实体/Mapper/SQL
263
263
  - **不允许"编译成功 + 单元测试通过"就标记任务完成,必须完成上述 checklist 全部项**
264
+
265
+ 执行凭证格式与持久化前后对照遵循 [document-review-coverage.md](document-review-coverage.md),不要另建格式;历史局部结论保留,以最后一个中英文一致的验证结果为最终汇总。
@@ -91,7 +91,7 @@ require_test_report_lint() {
91
91
  local -a lint_args
92
92
  root="$(project_root_for_change)"
93
93
  review="$CHANGE_DIR/.sdd/reviews/document-review.json"
94
- lint_args=(--tests "$CHANGE_DIR/tests.md")
94
+ lint_args=(--tests "$CHANGE_DIR/tests.md" --require-complete)
95
95
  if [[ "$(state_get workflow)" == "full" && ! -f "$review" ]]; then
96
96
  issues+=("full SDD verify requires .sdd/reviews/document-review.json; complete the docs entry review before verify")
97
97
  return
@@ -101,8 +101,8 @@ require_test_report_lint() {
101
101
  fi
102
102
 
103
103
  for lint in \
104
- "$root/.codex/hooks/superflow-test-report-lint.py" \
105
- "$HOME/.codex/hooks/superflow-test-report-lint.py"; do
104
+ "${SUPERFLOW_REPORT_LINT:-$SCRIPT_DIR/../../../scripts/superflow-test-report-lint.py}" \
105
+ "$SCRIPT_DIR/../../../hooks/superflow-test-report-lint.py"; do
106
106
  if [[ -x "$lint" ]]; then
107
107
  output="$(mktemp)"
108
108
  if ! "$lint" "${lint_args[@]}" "$report" >"$output" 2>&1; then
@@ -907,8 +907,12 @@ case "$PHASE" in
907
907
  require_file test-report.md
908
908
  require_grep 'RED|失败证据' test-report.md "RED evidence"
909
909
  require_grep 'GREEN|通过证据' test-report.md "GREEN evidence"
910
- require_grep '接口自动化|curl|Postman|Newman|pytest|RestAssured' test-report.md "interface automation evidence"
911
- require_grep 'DB|数据库|SELECT|SHOW CREATE' test-report.md "DB evidence"
910
+ if [[ "$(state_get workflow)" == "full" ]]; then
911
+ require_grep '接口自动化|curl|Postman|Newman|pytest|RestAssured' test-report.md "interface automation evidence"
912
+ fi
913
+ if change_has_database_change; then
914
+ require_grep 'DB|数据库|SELECT|SHOW CREATE' test-report.md "DB evidence"
915
+ fi
912
916
  require_grep 'superflow-verify-integration|superflow-delivery-check|superflow-test-report-lint' test-report.md "SuperBridge Flow hook/script evidence"
913
917
  if change_has_external_enum_risk; then
914
918
  require_grep 'External Enum Binding|外部枚举绑定|第三方字段语义绑定|外部展示|展示文案|业务语义|财务语义|BEM|真实入口' test-report.md "external enum binding runtime evidence"
@@ -75,3 +75,9 @@ Mark the task `Blocked` when:
75
75
  `~/.codex/hooks/superflow-test-report-lint.py` enforces this for database/status
76
76
  reports that claim pass or partial pass. A report that only says "sync
77
77
  succeeded" without consumer-path evidence is blocked.
78
+
79
+ ### 数据库分层证据
80
+
81
+ 按 [数据库合同](../superflow-pipeline/references/document-review-coverage.md#数据库分层合同)
82
+ 冻结并继承 `testLayer`、entry.database、生产 statement、mockBoundary 和数据断言。
83
+ 从表、字段、CAS与事务反向发现DB义务并声明entry.database;纯逻辑明确无持久化理由可免DB。
@@ -180,3 +180,12 @@ This advances to `phase: archive`, sets `verify_result: pass`, and records
180
180
 
181
181
  Passing verification does not authorize automatic archive. Enter `$superflow-archive`
182
182
  and wait for explicit user confirmation.
183
+
184
+
185
+ 逐用例 PASS 必须提供可重放本地执行凭证;冻结服务/入口、源码写入链与持久化前后值断言。沿用 pipeline/references/document-review-coverage.md 的 execution-receipt.v1 合同,旧证据保留 PARTIAL;独立评审核对精确 Mapper statement 与原始输出,完整交付运行当前安装的 lint --require-complete。哈希不等同防伪或业务充分性。
186
+
187
+ ### 数据库分层证据
188
+
189
+ 按 [数据库合同](../superflow-pipeline/references/document-review-coverage.md#数据库分层合同)
190
+ 冻结并继承 `testLayer`、entry.database、生产 statement、mockBoundary 和数据断言。
191
+ 核对原始事件与receipt、源码/构建/入口/引擎版本;旧固定版本证据保留,当前源码变化需新证据。
@@ -156,3 +156,9 @@ Superpowers own source-level HOW without weakening OpenSpec/SDD contracts.
156
156
  Do not continue to `$superflow-implement` until the design guard passes. The next
157
157
  phase may generate prompts, but this phase must not skip any required SDD
158
158
  contract document or replace API/DB/tests with Superpowers prose.
159
+
160
+ ### Database layer evidence
161
+
162
+ Freeze and inherit `testLayer`, entry.database, production statements, mockBoundary,
163
+ and data assertions from the [database contract](../superflow-pipeline/references/document-review-coverage.md#database-layer-contract).
164
+ Resolve schema sources, engine major, production Service/Mapper boundaries, affected rows, and non-target/previous-round isolation assertions.
@@ -486,3 +486,12 @@ Use references only as needed:
486
486
 
487
487
  When SDD docs pass quality gates, continue with `$superflow-design` for Superpowers
488
488
  technical design before prompt splitting or coding orchestration.
489
+
490
+
491
+ Per-case PASS requires retained local execution receipts. Freeze service/entry, source write chains and before/after persistence assertions using pipeline/references/document-review-coverage.md and execution-receipt.v1. Keep legacy evidence PARTIAL. Independent review checks the exact Mapper statement and raw output; full delivery runs the current installation's lint --require-complete. Hashes do not prove authenticity or business sufficiency.
492
+
493
+ ### Database layer evidence
494
+
495
+ Freeze and inherit `testLayer`, entry.database, production statements, mockBoundary,
496
+ and data assertions from the [database contract](../superflow-pipeline/references/document-review-coverage.md#database-layer-contract).
497
+ Review generated documents against mock-only required-DB negatives and real production-SQL positives; auxiliary layers need no DB.
@@ -134,3 +134,12 @@ Before moving to verify, run:
134
134
  Do not claim completion from unit tests alone. Runtime code changes require
135
135
  compile/build evidence, app start evidence when applicable, real API or event
136
136
  execution, DB/log checks, hook output, and test-report updates.
137
+
138
+
139
+ Per-case PASS requires retained local execution receipts. Freeze service/entry, source write chains and before/after persistence assertions using pipeline/references/document-review-coverage.md and execution-receipt.v1. Keep legacy evidence PARTIAL. Independent review checks the exact Mapper statement and raw output; full delivery runs the current installation's lint --require-complete. Hashes do not prove authenticity or business sufficiency.
140
+
141
+ ### Database layer evidence
142
+
143
+ Freeze and inherit `testLayer`, entry.database, production statements, mockBoundary,
144
+ and data assertions from the [database contract](../superflow-pipeline/references/document-review-coverage.md#database-layer-contract).
145
+ Execute frozen layers/assertions; missing DB environments are PARTIAL/BLOCKED. Do not substitute another engine or mocks.