@hunter-harness/workflow-harness 0.2.39 → 0.2.41
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/manifests/general/claude-code.json +29 -29
- package/harness/manifests/general/codebuddy.json +29 -29
- package/harness/manifests/general/codex.json +29 -29
- package/harness/manifests/general/cursor.json +29 -29
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +30 -30
- package/harness/manifests/java/cursor.json +30 -30
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -888,6 +888,181 @@ def ownership_hash(contract: dict[str, Any]) -> str:
|
|
|
888
888
|
return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
889
889
|
|
|
890
890
|
|
|
891
|
+
class LedgerMigrationError(ValueError):
|
|
892
|
+
"""A legacy ledger could not be upgraded without inventing identity."""
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def ledger_evidence_identity(ledger: dict[str, Any]) -> str:
|
|
896
|
+
"""Stable identity of evidence-bearing fields, excluding schema metadata."""
|
|
897
|
+
evidence = {
|
|
898
|
+
key: ledger[key]
|
|
899
|
+
for key in (
|
|
900
|
+
"validations",
|
|
901
|
+
"verificationTargets",
|
|
902
|
+
"integration",
|
|
903
|
+
"artifacts",
|
|
904
|
+
)
|
|
905
|
+
if key in ledger
|
|
906
|
+
}
|
|
907
|
+
canonical = json.dumps(
|
|
908
|
+
evidence,
|
|
909
|
+
ensure_ascii=False,
|
|
910
|
+
sort_keys=True,
|
|
911
|
+
separators=(",", ":"),
|
|
912
|
+
).encode("utf-8")
|
|
913
|
+
return "sha256:" + hashlib.sha256(canonical).hexdigest()
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def migrate_ledger_for_write(
|
|
917
|
+
change_dir: Path,
|
|
918
|
+
ledger: dict[str, Any],
|
|
919
|
+
*,
|
|
920
|
+
project_root: Path | None,
|
|
921
|
+
base_commit: str | None,
|
|
922
|
+
diff_hash: str | None,
|
|
923
|
+
record_migration: bool,
|
|
924
|
+
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
925
|
+
"""Upgrade one supported legacy window to v3 before the caller mutates evidence."""
|
|
926
|
+
raw_schema = ledger.get("schemaVersion")
|
|
927
|
+
source_schema = raw_schema if isinstance(raw_schema, int) else 1
|
|
928
|
+
if source_schema not in {1, 2, LEDGER_SCHEMA_VERSION}:
|
|
929
|
+
raise LedgerMigrationError(
|
|
930
|
+
f"unsupported ledger schemaVersion={raw_schema!r}; "
|
|
931
|
+
f"supported source versions are 1, 2, and {LEDGER_SCHEMA_VERSION}"
|
|
932
|
+
)
|
|
933
|
+
repo_probe = project_root or change_dir
|
|
934
|
+
repo_root_raw = _git_text(repo_probe, "rev-parse", "--show-toplevel")
|
|
935
|
+
current_head = _git_text(repo_probe, "rev-parse", "--verify", "HEAD")
|
|
936
|
+
if not repo_root_raw or not current_head:
|
|
937
|
+
raise LedgerMigrationError(
|
|
938
|
+
"repository root/current HEAD could not be established"
|
|
939
|
+
)
|
|
940
|
+
repo_root = Path(repo_root_raw).resolve()
|
|
941
|
+
try:
|
|
942
|
+
contract = harness_paths.load_change_contract(change_dir)
|
|
943
|
+
except (OSError, ValueError) as exc:
|
|
944
|
+
raise LedgerMigrationError(
|
|
945
|
+
f"change ownership contract could not be loaded: {exc}"
|
|
946
|
+
) from exc
|
|
947
|
+
|
|
948
|
+
resolved_base = base_commit
|
|
949
|
+
if not _nonempty_str(resolved_base):
|
|
950
|
+
resolved_base = ledger.get("baseCommit")
|
|
951
|
+
if not _nonempty_str(resolved_base):
|
|
952
|
+
resolved_base = current_head
|
|
953
|
+
resolved_diff = diff_hash
|
|
954
|
+
if not _nonempty_str(resolved_diff):
|
|
955
|
+
try:
|
|
956
|
+
resolved_diff = compute_ownership_diff(
|
|
957
|
+
repo_root,
|
|
958
|
+
base=str(resolved_base).strip(),
|
|
959
|
+
change_dir=change_dir,
|
|
960
|
+
)["diffHash"]
|
|
961
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
962
|
+
raise LedgerMigrationError(
|
|
963
|
+
f"ownership diff identity could not be computed: {exc}"
|
|
964
|
+
) from exc
|
|
965
|
+
|
|
966
|
+
before_identity = ledger_evidence_identity(ledger)
|
|
967
|
+
migrated = dict(ledger)
|
|
968
|
+
migrated["schemaVersion"] = LEDGER_SCHEMA_VERSION
|
|
969
|
+
try:
|
|
970
|
+
migrated["repositoryId"] = harness_paths.repository_identity(repo_root)
|
|
971
|
+
migrated["ownershipHash"] = ownership_hash(contract)
|
|
972
|
+
except (OSError, TypeError, ValueError, RuntimeError) as exc:
|
|
973
|
+
raise LedgerMigrationError(
|
|
974
|
+
f"repository/ownership identity could not be established: {exc}"
|
|
975
|
+
) from exc
|
|
976
|
+
migrated["changeName"] = change_dir.name
|
|
977
|
+
migrated["baseCommit"] = str(resolved_base).strip()
|
|
978
|
+
migrated["currentHead"] = str(current_head).strip()
|
|
979
|
+
migrated["diffHash"] = str(resolved_diff).strip()
|
|
980
|
+
missing = validate_ledger_identity(migrated)
|
|
981
|
+
if missing:
|
|
982
|
+
raise LedgerMigrationError(
|
|
983
|
+
"migrated ledger identity is incomplete: " + ", ".join(missing)
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
receipt: dict[str, Any] | None = None
|
|
987
|
+
if source_schema != LEDGER_SCHEMA_VERSION and record_migration:
|
|
988
|
+
after_identity = ledger_evidence_identity(migrated)
|
|
989
|
+
if after_identity != before_identity:
|
|
990
|
+
raise LedgerMigrationError(
|
|
991
|
+
"migration changed pre-existing evidence identity"
|
|
992
|
+
)
|
|
993
|
+
receipt = {
|
|
994
|
+
"schemaVersion": 1,
|
|
995
|
+
"action": "ledger-schema-migration",
|
|
996
|
+
"originalSchemaVersion": source_schema,
|
|
997
|
+
"targetSchemaVersion": LEDGER_SCHEMA_VERSION,
|
|
998
|
+
"migratedAt": now_iso(),
|
|
999
|
+
"evidenceIdentityBefore": before_identity,
|
|
1000
|
+
"evidenceIdentityAfter": after_identity,
|
|
1001
|
+
"repositoryId": migrated["repositoryId"],
|
|
1002
|
+
"baseCommit": migrated["baseCommit"],
|
|
1003
|
+
"currentHead": migrated["currentHead"],
|
|
1004
|
+
"diffHash": migrated["diffHash"],
|
|
1005
|
+
}
|
|
1006
|
+
receipt_identity = json.dumps(
|
|
1007
|
+
receipt,
|
|
1008
|
+
ensure_ascii=False,
|
|
1009
|
+
sort_keys=True,
|
|
1010
|
+
separators=(",", ":"),
|
|
1011
|
+
).encode("utf-8")
|
|
1012
|
+
receipt["receiptId"] = (
|
|
1013
|
+
"sha256:" + hashlib.sha256(receipt_identity).hexdigest()
|
|
1014
|
+
)
|
|
1015
|
+
history = migrated.get("migrationHistory")
|
|
1016
|
+
if history is None:
|
|
1017
|
+
history = []
|
|
1018
|
+
if not isinstance(history, list):
|
|
1019
|
+
raise LedgerMigrationError("migrationHistory must be an array")
|
|
1020
|
+
migrated["migrationHistory"] = [*history, receipt]
|
|
1021
|
+
return migrated, receipt
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _deterministic_rerecord_command(
|
|
1025
|
+
args: argparse.Namespace,
|
|
1026
|
+
*,
|
|
1027
|
+
files: list[str],
|
|
1028
|
+
project_root: Path | None,
|
|
1029
|
+
) -> list[str]:
|
|
1030
|
+
command = [
|
|
1031
|
+
sys.executable,
|
|
1032
|
+
str(Path(__file__).resolve()),
|
|
1033
|
+
"--json",
|
|
1034
|
+
"record",
|
|
1035
|
+
"--change-dir",
|
|
1036
|
+
str(resolve_path(args.change_dir)),
|
|
1037
|
+
"--verification",
|
|
1038
|
+
str(args.verification),
|
|
1039
|
+
"--status",
|
|
1040
|
+
str(args.status),
|
|
1041
|
+
"--command",
|
|
1042
|
+
str(args.command),
|
|
1043
|
+
"--exit-code",
|
|
1044
|
+
str(args.exit_code),
|
|
1045
|
+
"--duration-ms",
|
|
1046
|
+
str(args.duration_ms),
|
|
1047
|
+
"--evidence",
|
|
1048
|
+
str(args.evidence),
|
|
1049
|
+
"--files",
|
|
1050
|
+
",".join(sorted(str(Path(item).resolve()) for item in files)),
|
|
1051
|
+
]
|
|
1052
|
+
if project_root is not None:
|
|
1053
|
+
command.extend(["--project", str(project_root)])
|
|
1054
|
+
for flag, attr in (
|
|
1055
|
+
("--scope", "scope"),
|
|
1056
|
+
("--coverage", "coverage"),
|
|
1057
|
+
("--base-commit", "base_commit"),
|
|
1058
|
+
("--diff-hash", "diff_hash"),
|
|
1059
|
+
):
|
|
1060
|
+
value = getattr(args, attr, None)
|
|
1061
|
+
if _nonempty_str(value):
|
|
1062
|
+
command.extend([flag, str(value).strip()])
|
|
1063
|
+
return command
|
|
1064
|
+
|
|
1065
|
+
|
|
891
1066
|
_METRICS_SCHEMAS: dict[str, dict[str, tuple[str, ...]]] = {
|
|
892
1067
|
"unitTest": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
|
|
893
1068
|
"unitTestFull": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
|
|
@@ -1125,6 +1300,266 @@ def _nonempty_str(value: Any) -> bool:
|
|
|
1125
1300
|
return isinstance(value, str) and bool(value.strip())
|
|
1126
1301
|
|
|
1127
1302
|
|
|
1303
|
+
def _scenario_receipt_error(code: str, message: str) -> dict[str, Any]:
|
|
1304
|
+
return {"ok": False, "code": code, "error": message}
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _canonical_string_list(
|
|
1308
|
+
receipt: dict[str, Any],
|
|
1309
|
+
field: str,
|
|
1310
|
+
) -> tuple[list[str] | None, dict[str, Any] | None]:
|
|
1311
|
+
raw = receipt.get(field)
|
|
1312
|
+
if not isinstance(raw, list) or any(not _nonempty_str(item) for item in raw):
|
|
1313
|
+
return None, _scenario_receipt_error(
|
|
1314
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1315
|
+
f"scenario receipt field '{field}' must be an array of non-empty strings",
|
|
1316
|
+
)
|
|
1317
|
+
values = [str(item).strip() for item in raw]
|
|
1318
|
+
if len(values) != len(set(values)):
|
|
1319
|
+
return None, _scenario_receipt_error(
|
|
1320
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1321
|
+
f"scenario receipt field '{field}' contains duplicate test IDs",
|
|
1322
|
+
)
|
|
1323
|
+
return values, None
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def validate_scenario_execution_receipt(
|
|
1327
|
+
*,
|
|
1328
|
+
change_dir: Path,
|
|
1329
|
+
scenario_ids: list[str],
|
|
1330
|
+
receipt: dict[str, Any],
|
|
1331
|
+
) -> dict[str, Any]:
|
|
1332
|
+
"""Validate and minimize runner output before it can satisfy scenario evidence."""
|
|
1333
|
+
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
1334
|
+
try:
|
|
1335
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
|
1336
|
+
except FileNotFoundError:
|
|
1337
|
+
return _scenario_receipt_error(
|
|
1338
|
+
"SCENARIO_MANIFEST_MISSING",
|
|
1339
|
+
f"scenario manifest does not exist: {manifest_path}",
|
|
1340
|
+
)
|
|
1341
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1342
|
+
return _scenario_receipt_error("SCENARIO_MANIFEST_INVALID", str(exc))
|
|
1343
|
+
if not isinstance(manifest, dict) or not isinstance(manifest.get("scenarios"), list):
|
|
1344
|
+
return _scenario_receipt_error(
|
|
1345
|
+
"SCENARIO_MANIFEST_INVALID",
|
|
1346
|
+
"scenario-manifest.json must contain a scenarios array",
|
|
1347
|
+
)
|
|
1348
|
+
if int(manifest.get("schemaVersion") or 0) < 2:
|
|
1349
|
+
return _scenario_receipt_error(
|
|
1350
|
+
"SCENARIO_RECEIPT_UNSUPPORTED",
|
|
1351
|
+
"structured scenario receipts require scenario-manifest schemaVersion 2",
|
|
1352
|
+
)
|
|
1353
|
+
if receipt.get("schemaVersion") != 1:
|
|
1354
|
+
return _scenario_receipt_error(
|
|
1355
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1356
|
+
"scenario receipt schemaVersion must be 1",
|
|
1357
|
+
)
|
|
1358
|
+
runner = receipt.get("runner")
|
|
1359
|
+
if not isinstance(runner, dict) or not _nonempty_str(runner.get("name")):
|
|
1360
|
+
return _scenario_receipt_error(
|
|
1361
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1362
|
+
"scenario receipt runner.name is required",
|
|
1363
|
+
)
|
|
1364
|
+
if "version" in runner and not _nonempty_str(runner.get("version")):
|
|
1365
|
+
return _scenario_receipt_error(
|
|
1366
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1367
|
+
"scenario receipt runner.version must be a non-empty string when present",
|
|
1368
|
+
)
|
|
1369
|
+
attempt = receipt.get("attempt")
|
|
1370
|
+
if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
|
|
1371
|
+
return _scenario_receipt_error(
|
|
1372
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1373
|
+
"scenario receipt attempt must be a positive integer",
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
declared, error = _canonical_string_list(receipt, "declared")
|
|
1377
|
+
if error:
|
|
1378
|
+
return error
|
|
1379
|
+
selected, error = _canonical_string_list(receipt, "selected")
|
|
1380
|
+
if error:
|
|
1381
|
+
return error
|
|
1382
|
+
assert declared is not None and selected is not None
|
|
1383
|
+
|
|
1384
|
+
collected_raw = receipt.get("collected")
|
|
1385
|
+
executed_raw = receipt.get("executed")
|
|
1386
|
+
if not isinstance(collected_raw, list) or not isinstance(executed_raw, list):
|
|
1387
|
+
return _scenario_receipt_error(
|
|
1388
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1389
|
+
"scenario receipt collected and executed fields must be arrays",
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
collected: list[dict[str, str]] = []
|
|
1393
|
+
collected_keys: set[tuple[str, str, str]] = set()
|
|
1394
|
+
for index, item in enumerate(collected_raw):
|
|
1395
|
+
if not isinstance(item, dict) or any(
|
|
1396
|
+
not _nonempty_str(item.get(field))
|
|
1397
|
+
for field in ("testId", "file", "title")
|
|
1398
|
+
):
|
|
1399
|
+
return _scenario_receipt_error(
|
|
1400
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1401
|
+
f"scenario receipt collected[{index}] has an invalid test identity",
|
|
1402
|
+
)
|
|
1403
|
+
canonical = {
|
|
1404
|
+
"testId": str(item["testId"]).strip(),
|
|
1405
|
+
"file": str(item["file"]).strip(),
|
|
1406
|
+
"title": str(item["title"]).strip(),
|
|
1407
|
+
}
|
|
1408
|
+
key = (canonical["testId"], canonical["file"], canonical["title"])
|
|
1409
|
+
if key in collected_keys:
|
|
1410
|
+
return _scenario_receipt_error(
|
|
1411
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1412
|
+
f"scenario receipt collected[{index}] duplicates a test identity",
|
|
1413
|
+
)
|
|
1414
|
+
collected_keys.add(key)
|
|
1415
|
+
collected.append(canonical)
|
|
1416
|
+
|
|
1417
|
+
executed: list[dict[str, Any]] = []
|
|
1418
|
+
executed_keys: set[tuple[str, str, str, int]] = set()
|
|
1419
|
+
allowed_statuses = {"PASSED", "FAILED", "SKIPPED"}
|
|
1420
|
+
for index, item in enumerate(executed_raw):
|
|
1421
|
+
if not isinstance(item, dict) or any(
|
|
1422
|
+
not _nonempty_str(item.get(field))
|
|
1423
|
+
for field in ("testId", "file", "title", "status")
|
|
1424
|
+
):
|
|
1425
|
+
return _scenario_receipt_error(
|
|
1426
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1427
|
+
f"scenario receipt executed[{index}] has an invalid test identity",
|
|
1428
|
+
)
|
|
1429
|
+
item_attempt = item.get("attempt")
|
|
1430
|
+
if (
|
|
1431
|
+
not isinstance(item_attempt, int)
|
|
1432
|
+
or isinstance(item_attempt, bool)
|
|
1433
|
+
or item_attempt != attempt
|
|
1434
|
+
):
|
|
1435
|
+
return _scenario_receipt_error(
|
|
1436
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1437
|
+
f"scenario receipt executed[{index}].attempt must equal receipt attempt",
|
|
1438
|
+
)
|
|
1439
|
+
item_status = str(item["status"]).strip().upper()
|
|
1440
|
+
if item_status not in allowed_statuses:
|
|
1441
|
+
return _scenario_receipt_error(
|
|
1442
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1443
|
+
f"scenario receipt executed[{index}].status is unsupported",
|
|
1444
|
+
)
|
|
1445
|
+
canonical = {
|
|
1446
|
+
"testId": str(item["testId"]).strip(),
|
|
1447
|
+
"file": str(item["file"]).strip(),
|
|
1448
|
+
"title": str(item["title"]).strip(),
|
|
1449
|
+
"attempt": item_attempt,
|
|
1450
|
+
"status": item_status,
|
|
1451
|
+
}
|
|
1452
|
+
key = (
|
|
1453
|
+
canonical["testId"],
|
|
1454
|
+
canonical["file"],
|
|
1455
|
+
canonical["title"],
|
|
1456
|
+
item_attempt,
|
|
1457
|
+
)
|
|
1458
|
+
if key in executed_keys:
|
|
1459
|
+
return _scenario_receipt_error(
|
|
1460
|
+
"SCENARIO_RECEIPT_INVALID",
|
|
1461
|
+
f"scenario receipt executed[{index}] duplicates a test attempt",
|
|
1462
|
+
)
|
|
1463
|
+
executed_keys.add(key)
|
|
1464
|
+
executed.append(canonical)
|
|
1465
|
+
|
|
1466
|
+
manifest_scenarios = {
|
|
1467
|
+
str(item.get("id") or "").strip(): item
|
|
1468
|
+
for item in manifest["scenarios"]
|
|
1469
|
+
if isinstance(item, dict) and _nonempty_str(item.get("id"))
|
|
1470
|
+
}
|
|
1471
|
+
unknown = sorted(set(scenario_ids) - set(manifest_scenarios))
|
|
1472
|
+
if unknown:
|
|
1473
|
+
return _scenario_receipt_error(
|
|
1474
|
+
"SCENARIO_ID_UNKNOWN",
|
|
1475
|
+
"scenario IDs are not declared in scenario-manifest.json: "
|
|
1476
|
+
+ ", ".join(unknown),
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
coverage = {
|
|
1480
|
+
key: []
|
|
1481
|
+
for key in (
|
|
1482
|
+
"declared",
|
|
1483
|
+
"selected",
|
|
1484
|
+
"collected",
|
|
1485
|
+
"executed",
|
|
1486
|
+
"passed",
|
|
1487
|
+
"skipped",
|
|
1488
|
+
"failed",
|
|
1489
|
+
"unexecuted",
|
|
1490
|
+
)
|
|
1491
|
+
}
|
|
1492
|
+
for scenario_id in scenario_ids:
|
|
1493
|
+
scenario = manifest_scenarios[scenario_id]
|
|
1494
|
+
if scenario.get("requiredEvidenceKind") != "ledger":
|
|
1495
|
+
continue
|
|
1496
|
+
if any(
|
|
1497
|
+
not _nonempty_str(scenario.get(field))
|
|
1498
|
+
for field in ("executableTestId", "testFile", "testTitle")
|
|
1499
|
+
):
|
|
1500
|
+
return _scenario_receipt_error(
|
|
1501
|
+
"SCENARIO_MANIFEST_INVALID",
|
|
1502
|
+
f"scenario {scenario_id} is missing its executable test identity",
|
|
1503
|
+
)
|
|
1504
|
+
test_id = str(scenario["executableTestId"]).strip()
|
|
1505
|
+
identity = (
|
|
1506
|
+
test_id,
|
|
1507
|
+
str(scenario["testFile"]).strip(),
|
|
1508
|
+
str(scenario["testTitle"]).strip(),
|
|
1509
|
+
)
|
|
1510
|
+
if test_id in declared:
|
|
1511
|
+
coverage["declared"].append(scenario_id)
|
|
1512
|
+
if test_id in selected:
|
|
1513
|
+
coverage["selected"].append(scenario_id)
|
|
1514
|
+
if identity in collected_keys:
|
|
1515
|
+
coverage["collected"].append(scenario_id)
|
|
1516
|
+
selection_closed = (
|
|
1517
|
+
test_id in declared
|
|
1518
|
+
and test_id in selected
|
|
1519
|
+
and identity in collected_keys
|
|
1520
|
+
)
|
|
1521
|
+
matches = [
|
|
1522
|
+
item
|
|
1523
|
+
for item in executed
|
|
1524
|
+
if (
|
|
1525
|
+
item["testId"],
|
|
1526
|
+
item["file"],
|
|
1527
|
+
item["title"],
|
|
1528
|
+
)
|
|
1529
|
+
== identity
|
|
1530
|
+
]
|
|
1531
|
+
if matches:
|
|
1532
|
+
coverage["executed"].append(scenario_id)
|
|
1533
|
+
terminal = matches[-1]["status"]
|
|
1534
|
+
if terminal == "PASSED" and selection_closed:
|
|
1535
|
+
coverage["passed"].append(scenario_id)
|
|
1536
|
+
elif terminal == "SKIPPED":
|
|
1537
|
+
coverage["skipped"].append(scenario_id)
|
|
1538
|
+
elif terminal == "FAILED":
|
|
1539
|
+
coverage["failed"].append(scenario_id)
|
|
1540
|
+
if not selection_closed:
|
|
1541
|
+
coverage["unexecuted"].append(scenario_id)
|
|
1542
|
+
else:
|
|
1543
|
+
coverage["unexecuted"].append(scenario_id)
|
|
1544
|
+
|
|
1545
|
+
canonical_runner = {"name": str(runner["name"]).strip()}
|
|
1546
|
+
if _nonempty_str(runner.get("version")):
|
|
1547
|
+
canonical_runner["version"] = str(runner["version"]).strip()
|
|
1548
|
+
return {
|
|
1549
|
+
"ok": True,
|
|
1550
|
+
"receipt": {
|
|
1551
|
+
"schemaVersion": 1,
|
|
1552
|
+
"runner": canonical_runner,
|
|
1553
|
+
"attempt": attempt,
|
|
1554
|
+
"declared": declared,
|
|
1555
|
+
"selected": selected,
|
|
1556
|
+
"collected": collected,
|
|
1557
|
+
"executed": executed,
|
|
1558
|
+
},
|
|
1559
|
+
"coverage": coverage,
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
|
|
1128
1563
|
def _scope_covers(ledger_scope: Any, requested_scope: str | None) -> bool:
|
|
1129
1564
|
"""unitTest: ledger scope must cover requested scope (broad scopes cover all)."""
|
|
1130
1565
|
if not _nonempty_str(ledger_scope) and not isinstance(ledger_scope, list):
|
|
@@ -1702,14 +2137,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1702
2137
|
change_dir = resolve_path(args.change_dir)
|
|
1703
2138
|
verification = args.verification
|
|
1704
2139
|
contract_v2 = _contract_is_v2(change_dir)
|
|
1705
|
-
|
|
1706
|
-
_nonempty_str(getattr(args, field, None))
|
|
1707
|
-
for field in ("base_commit", "diff_hash")
|
|
1708
|
-
):
|
|
1709
|
-
return emit_error(
|
|
1710
|
-
"IDENTITY_UNSUPPORTED: explicit ledger identity requires a v2 change contract",
|
|
1711
|
-
as_json=as_json,
|
|
1712
|
-
)
|
|
2140
|
+
migration_receipt: dict[str, Any] | None = None
|
|
1713
2141
|
try:
|
|
1714
2142
|
files, project_root = input_files_from_args(args)
|
|
1715
2143
|
except (OSError, ValueError) as exc:
|
|
@@ -1742,7 +2170,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1742
2170
|
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1743
2171
|
files, project_root=project_root
|
|
1744
2172
|
)
|
|
1745
|
-
ledger,
|
|
2173
|
+
ledger, existing_path = load_ledger(change_dir)
|
|
1746
2174
|
if ledger is None:
|
|
1747
2175
|
ledger = {
|
|
1748
2176
|
"changeName": change_dir.name,
|
|
@@ -1751,6 +2179,41 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1751
2179
|
}
|
|
1752
2180
|
elif not isinstance(ledger.get("validations"), dict):
|
|
1753
2181
|
ledger["validations"] = {}
|
|
2182
|
+
explicit_identity = any(
|
|
2183
|
+
_nonempty_str(getattr(args, field, None))
|
|
2184
|
+
for field in ("base_commit", "diff_hash")
|
|
2185
|
+
)
|
|
2186
|
+
existing_schema = ledger.get("schemaVersion")
|
|
2187
|
+
identity_write = (
|
|
2188
|
+
contract_v2
|
|
2189
|
+
or explicit_identity
|
|
2190
|
+
or existing_schema in {2, LEDGER_SCHEMA_VERSION}
|
|
2191
|
+
)
|
|
2192
|
+
if identity_write:
|
|
2193
|
+
try:
|
|
2194
|
+
ledger, migration_receipt = migrate_ledger_for_write(
|
|
2195
|
+
change_dir,
|
|
2196
|
+
ledger,
|
|
2197
|
+
project_root=project_root,
|
|
2198
|
+
base_commit=getattr(args, "base_commit", None),
|
|
2199
|
+
diff_hash=getattr(args, "diff_hash", None),
|
|
2200
|
+
record_migration=existing_path is not None,
|
|
2201
|
+
)
|
|
2202
|
+
except LedgerMigrationError as exc:
|
|
2203
|
+
return emit_error(
|
|
2204
|
+
f"ledger migration failed: {exc}",
|
|
2205
|
+
as_json=as_json,
|
|
2206
|
+
error_code="LEDGER_MIGRATION_REQUIRED",
|
|
2207
|
+
extra={
|
|
2208
|
+
"originalSchemaVersion": ledger.get("schemaVersion", 1),
|
|
2209
|
+
"targetSchemaVersion": LEDGER_SCHEMA_VERSION,
|
|
2210
|
+
"rerecordCommand": _deterministic_rerecord_command(
|
|
2211
|
+
args,
|
|
2212
|
+
files=files,
|
|
2213
|
+
project_root=project_root,
|
|
2214
|
+
),
|
|
2215
|
+
},
|
|
2216
|
+
)
|
|
1754
2217
|
|
|
1755
2218
|
# Preserve top-level diffHash and all other existing fields (backward compatible).
|
|
1756
2219
|
entry = {}
|
|
@@ -1857,13 +2320,115 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1857
2320
|
|
|
1858
2321
|
# C9: bind scenario IDs from --scenario-ids to this ledger entry.
|
|
1859
2322
|
scenario_ids_raw = getattr(args, "scenario_ids", None)
|
|
2323
|
+
scenario_receipt_file = getattr(args, "scenario_receipt_file", None)
|
|
2324
|
+
if _nonempty_str(scenario_receipt_file) and not _nonempty_str(
|
|
2325
|
+
scenario_ids_raw
|
|
2326
|
+
):
|
|
2327
|
+
return emit_error(
|
|
2328
|
+
"--scenario-receipt-file requires --scenario-ids",
|
|
2329
|
+
as_json=as_json,
|
|
2330
|
+
error_code="SCENARIO_IDS_REQUIRED",
|
|
2331
|
+
)
|
|
1860
2332
|
if _nonempty_str(scenario_ids_raw):
|
|
1861
2333
|
ids = [s.strip() for s in str(scenario_ids_raw).split(",") if s.strip()]
|
|
2334
|
+
if len(ids) != len(set(ids)):
|
|
2335
|
+
return emit_error(
|
|
2336
|
+
"scenario IDs must be unique",
|
|
2337
|
+
as_json=as_json,
|
|
2338
|
+
error_code="SCENARIO_ID_DUPLICATE",
|
|
2339
|
+
)
|
|
1862
2340
|
if ids:
|
|
1863
2341
|
entry["scenarioIds"] = ids
|
|
2342
|
+
receipt_file = scenario_receipt_file
|
|
2343
|
+
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
2344
|
+
manifest_schema = 0
|
|
2345
|
+
if manifest_path.is_file():
|
|
2346
|
+
try:
|
|
2347
|
+
manifest_probe = json.loads(
|
|
2348
|
+
manifest_path.read_text(encoding="utf-8-sig")
|
|
2349
|
+
)
|
|
2350
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
2351
|
+
return emit_error(
|
|
2352
|
+
f"invalid scenario manifest: {exc}",
|
|
2353
|
+
as_json=as_json,
|
|
2354
|
+
error_code="SCENARIO_MANIFEST_INVALID",
|
|
2355
|
+
)
|
|
2356
|
+
raw_schema = (
|
|
2357
|
+
manifest_probe.get("schemaVersion")
|
|
2358
|
+
if isinstance(manifest_probe, dict)
|
|
2359
|
+
else None
|
|
2360
|
+
)
|
|
2361
|
+
if (
|
|
2362
|
+
isinstance(raw_schema, int)
|
|
2363
|
+
and not isinstance(raw_schema, bool)
|
|
2364
|
+
):
|
|
2365
|
+
manifest_schema = raw_schema
|
|
2366
|
+
if manifest_schema >= 2 and not _nonempty_str(receipt_file):
|
|
2367
|
+
return emit_error(
|
|
2368
|
+
"scenario-manifest schemaVersion 2 requires "
|
|
2369
|
+
"--scenario-receipt-file",
|
|
2370
|
+
as_json=as_json,
|
|
2371
|
+
error_code="SCENARIO_RECEIPT_REQUIRED",
|
|
2372
|
+
)
|
|
2373
|
+
if _nonempty_str(receipt_file):
|
|
2374
|
+
try:
|
|
2375
|
+
receipt_payload = json.loads(
|
|
2376
|
+
Path(str(receipt_file))
|
|
2377
|
+
.expanduser()
|
|
2378
|
+
.resolve()
|
|
2379
|
+
.read_text(encoding="utf-8-sig")
|
|
2380
|
+
)
|
|
2381
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
2382
|
+
return emit_error(
|
|
2383
|
+
f"invalid scenario receipt: {exc}",
|
|
2384
|
+
as_json=as_json,
|
|
2385
|
+
error_code="SCENARIO_RECEIPT_INVALID",
|
|
2386
|
+
)
|
|
2387
|
+
if not isinstance(receipt_payload, dict):
|
|
2388
|
+
return emit_error(
|
|
2389
|
+
"scenario receipt top level must be an object",
|
|
2390
|
+
as_json=as_json,
|
|
2391
|
+
error_code="SCENARIO_RECEIPT_INVALID",
|
|
2392
|
+
)
|
|
2393
|
+
scenario_result = validate_scenario_execution_receipt(
|
|
2394
|
+
change_dir=change_dir,
|
|
2395
|
+
scenario_ids=ids,
|
|
2396
|
+
receipt=receipt_payload,
|
|
2397
|
+
)
|
|
2398
|
+
if not scenario_result.get("ok"):
|
|
2399
|
+
return emit_error(
|
|
2400
|
+
str(scenario_result.get("error") or "invalid scenario receipt"),
|
|
2401
|
+
as_json=as_json,
|
|
2402
|
+
error_code=str(
|
|
2403
|
+
scenario_result.get("code")
|
|
2404
|
+
or "SCENARIO_RECEIPT_INVALID"
|
|
2405
|
+
),
|
|
2406
|
+
)
|
|
2407
|
+
entry["scenarioReceipt"] = scenario_result["receipt"]
|
|
2408
|
+
entry["scenarioCoverage"] = scenario_result["coverage"]
|
|
2409
|
+
if status == "OK":
|
|
2410
|
+
passed = set(scenario_result["coverage"]["passed"])
|
|
2411
|
+
required = set(ids)
|
|
2412
|
+
missing = sorted(required - passed)
|
|
2413
|
+
if missing:
|
|
2414
|
+
return emit_error(
|
|
2415
|
+
"required scenarios were not executed successfully: "
|
|
2416
|
+
+ ", ".join(missing),
|
|
2417
|
+
as_json=as_json,
|
|
2418
|
+
error_code="REQUIRED_SCENARIO_NOT_EXECUTED",
|
|
2419
|
+
extra={
|
|
2420
|
+
"missing": missing,
|
|
2421
|
+
"scenarioCoverage": scenario_result["coverage"],
|
|
2422
|
+
},
|
|
2423
|
+
)
|
|
2424
|
+
else:
|
|
2425
|
+
entry.pop("scenarioReceipt", None)
|
|
2426
|
+
entry.pop("scenarioCoverage", None)
|
|
1864
2427
|
elif "scenarioIds" in entry:
|
|
1865
2428
|
# Fresh record without scenario-ids must not keep stale ids from prev.
|
|
1866
2429
|
entry.pop("scenarioIds", None)
|
|
2430
|
+
entry.pop("scenarioReceipt", None)
|
|
2431
|
+
entry.pop("scenarioCoverage", None)
|
|
1867
2432
|
|
|
1868
2433
|
target_identity = {
|
|
1869
2434
|
"verification": verification,
|
|
@@ -1908,52 +2473,6 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1908
2473
|
ledger["stateDir"] = str(change_dir)
|
|
1909
2474
|
|
|
1910
2475
|
out_path = preferred_write_path(change_dir)
|
|
1911
|
-
if contract_v2:
|
|
1912
|
-
# Ledger v3: forced top-level identity (RET-15/16, COM-002).
|
|
1913
|
-
repo_probe = project_root or change_dir
|
|
1914
|
-
repo_root_raw = _git_text(repo_probe, "rev-parse", "--show-toplevel")
|
|
1915
|
-
if not repo_root_raw:
|
|
1916
|
-
return emit_error(
|
|
1917
|
-
"record failed: change dir is not inside a git repository",
|
|
1918
|
-
as_json=as_json,
|
|
1919
|
-
)
|
|
1920
|
-
repo_root = Path(repo_root_raw).resolve()
|
|
1921
|
-
try:
|
|
1922
|
-
contract = harness_paths.load_change_contract(change_dir)
|
|
1923
|
-
except (OSError, ValueError) as exc:
|
|
1924
|
-
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
1925
|
-
base_commit = getattr(args, "base_commit", None)
|
|
1926
|
-
if not _nonempty_str(base_commit):
|
|
1927
|
-
base_commit = ledger.get("baseCommit")
|
|
1928
|
-
if not _nonempty_str(base_commit):
|
|
1929
|
-
base_commit = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
|
|
1930
|
-
current_head = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
|
|
1931
|
-
diff_hash = getattr(args, "diff_hash", None)
|
|
1932
|
-
if not _nonempty_str(diff_hash) and _nonempty_str(base_commit):
|
|
1933
|
-
try:
|
|
1934
|
-
diff_hash = compute_ownership_diff(
|
|
1935
|
-
repo_root, base=str(base_commit).strip(), change_dir=change_dir
|
|
1936
|
-
)["diffHash"]
|
|
1937
|
-
except (OSError, ValueError, RuntimeError):
|
|
1938
|
-
diff_hash = None
|
|
1939
|
-
ledger["schemaVersion"] = LEDGER_SCHEMA_VERSION
|
|
1940
|
-
ledger["repositoryId"] = harness_paths.repository_identity(repo_root)
|
|
1941
|
-
ledger["changeName"] = change_dir.name
|
|
1942
|
-
if _nonempty_str(base_commit):
|
|
1943
|
-
ledger["baseCommit"] = str(base_commit).strip()
|
|
1944
|
-
if _nonempty_str(current_head):
|
|
1945
|
-
ledger["currentHead"] = str(current_head).strip()
|
|
1946
|
-
if _nonempty_str(diff_hash):
|
|
1947
|
-
ledger["diffHash"] = str(diff_hash).strip()
|
|
1948
|
-
ledger["ownershipHash"] = ownership_hash(contract)
|
|
1949
|
-
missing = validate_ledger_identity(ledger)
|
|
1950
|
-
if missing:
|
|
1951
|
-
return emit_error(
|
|
1952
|
-
"ledger identity incomplete: " + ", ".join(missing),
|
|
1953
|
-
as_json=as_json,
|
|
1954
|
-
error_code="LEDGER_IDENTITY_INVALID",
|
|
1955
|
-
extra={"missing": missing},
|
|
1956
|
-
)
|
|
1957
2476
|
write_ledger(out_path, ledger)
|
|
1958
2477
|
try:
|
|
1959
2478
|
upsert_verification_graph_node(
|
|
@@ -2004,6 +2523,8 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2004
2523
|
"ledger_path": str(out_path),
|
|
2005
2524
|
"diffHash": ledger.get("diffHash"),
|
|
2006
2525
|
"resolvedProjectRoot": str(project_root) if project_root else None,
|
|
2526
|
+
"scenarioCoverage": entry.get("scenarioCoverage"),
|
|
2527
|
+
"migrationReceipt": migration_receipt,
|
|
2007
2528
|
}
|
|
2008
2529
|
emit_compact_or_verbose(
|
|
2009
2530
|
payload, as_json=as_json, verbose=verbose,
|
|
@@ -2209,6 +2730,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2209
2730
|
default=None,
|
|
2210
2731
|
help="comma-separated scenario IDs from scenario-manifest.json to bind to this entry",
|
|
2211
2732
|
)
|
|
2733
|
+
p_record.add_argument(
|
|
2734
|
+
"--scenario-receipt-file",
|
|
2735
|
+
default=None,
|
|
2736
|
+
help="UTF-8 JSON runner receipt proving declared/selected/collected/executed tests",
|
|
2737
|
+
)
|
|
2212
2738
|
p_record.add_argument(
|
|
2213
2739
|
"--verbose",
|
|
2214
2740
|
action="store_true",
|