@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
|
@@ -3864,6 +3864,7 @@ def collect_summary_data(
|
|
|
3864
3864
|
"timing",
|
|
3865
3865
|
"skillCalls",
|
|
3866
3866
|
"verification",
|
|
3867
|
+
"scenarioCoverage",
|
|
3867
3868
|
"timeline",
|
|
3868
3869
|
"changedFiles",
|
|
3869
3870
|
"artifacts",
|
|
@@ -3887,6 +3888,7 @@ def collect_summary_data(
|
|
|
3887
3888
|
"changeIdentity",
|
|
3888
3889
|
"artifactStorage",
|
|
3889
3890
|
"retention",
|
|
3891
|
+
"archiveDurability",
|
|
3890
3892
|
"remoteCost",
|
|
3891
3893
|
"projection",
|
|
3892
3894
|
):
|
|
@@ -3977,6 +3979,9 @@ def collect_summary_data(
|
|
|
3977
3979
|
ver.setdefault("dbCompatibilityEvidence", {})
|
|
3978
3980
|
ver.setdefault("coverageDisplay", ver.get("coverageDisplay", NOT_AVAILABLE))
|
|
3979
3981
|
|
|
3982
|
+
if not for_replay or not isinstance(data.get("scenarioCoverage"), dict):
|
|
3983
|
+
data["scenarioCoverage"] = hgate._validate_scenario_coverage(change_dir)
|
|
3984
|
+
|
|
3980
3985
|
# stageStatus / finalStatus
|
|
3981
3986
|
if not for_replay or not isinstance(data.get("stageStatus"), dict):
|
|
3982
3987
|
data["stageStatus"] = _stage_status_from_sources(events, ledger, change_dir)
|
|
@@ -4801,6 +4806,18 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4801
4806
|
remote_totals = rec(remote.get("totals"))
|
|
4802
4807
|
projection = rec(summary.get("projection"))
|
|
4803
4808
|
archive_integrity = rec(summary.get("archiveIntegrity"))
|
|
4809
|
+
durability = rec(summary.get("archiveDurability"))
|
|
4810
|
+
durability_status = str(
|
|
4811
|
+
durability.get("status") or "ARCHIVED_LOCAL_ONLY"
|
|
4812
|
+
)
|
|
4813
|
+
scenario_coverage = rec(summary.get("scenarioCoverage"))
|
|
4814
|
+
unexecuted_scenarios = [
|
|
4815
|
+
str(item) for item in seq(scenario_coverage.get("unexecuted"))
|
|
4816
|
+
]
|
|
4817
|
+
scenario_status = str(
|
|
4818
|
+
scenario_coverage.get("code")
|
|
4819
|
+
or ("SCENARIO_COVERAGE_OK" if scenario_coverage.get("ok") else "NOT_RUN")
|
|
4820
|
+
)
|
|
4804
4821
|
total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
|
|
4805
4822
|
total_files_text = "" if total_files is None else str(total_files)
|
|
4806
4823
|
|
|
@@ -4823,6 +4840,9 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4823
4840
|
<article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
|
|
4824
4841
|
<article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
|
|
4825
4842
|
<article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
|
|
4843
|
+
<article class="card"><h2>场景执行闭环</h2>
|
|
4844
|
+
<p>结构化运行回执:{esc(scenario_status)}</p>
|
|
4845
|
+
<p>未执行 / 未通过:{esc(", ".join(unexecuted_scenarios) or "无")}</p></article>
|
|
4826
4846
|
<article class="card"><h2>风险与动作</h2><div class="risk"><section><h3>当前风险</h3><ul>{risk_rows}</ul></section><section><h3>人工动作</h3><ul>{action_rows}</ul></section></div></article>
|
|
4827
4847
|
{release_card}
|
|
4828
4848
|
<details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
|
|
@@ -4833,8 +4853,10 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4833
4853
|
<p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
|
|
4834
4854
|
<p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
|
|
4835
4855
|
<details><summary>技术证据 · 发布与归档治理</summary><div>
|
|
4836
|
-
<p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
|
|
4837
|
-
<p
|
|
4856
|
+
<p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
|
|
4857
|
+
<p>归档持久性:{esc(durability_status)} · {esc(durability.get("risk") or "已通过持久介质回读校验")}</p>
|
|
4858
|
+
<p>保留策略:{esc(durability.get("retentionPolicy") or "N/A")}</p>
|
|
4859
|
+
<p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
|
|
4838
4860
|
<p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
|
|
4839
4861
|
<details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
|
|
4840
4862
|
<details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
|
|
@@ -6896,6 +6918,262 @@ def run_service_stop(change_dir: Path) -> dict[str, Any]:
|
|
|
6896
6918
|
return {"ran": True, "skipped": False, "ok": False, "warning": str(exc)}
|
|
6897
6919
|
|
|
6898
6920
|
|
|
6921
|
+
def _path_is_within(path: Path, root: Path) -> bool:
|
|
6922
|
+
try:
|
|
6923
|
+
path.resolve().relative_to(root.resolve())
|
|
6924
|
+
return True
|
|
6925
|
+
except ValueError:
|
|
6926
|
+
return False
|
|
6927
|
+
|
|
6928
|
+
|
|
6929
|
+
def _archive_tree_digest(root: Path) -> str:
|
|
6930
|
+
"""Hash a complete archive tree without following links.
|
|
6931
|
+
|
|
6932
|
+
Directory entries are included so an empty directory cannot disappear
|
|
6933
|
+
unnoticed. Durable archives reject links rather than copying data from
|
|
6934
|
+
outside the finalized archive boundary.
|
|
6935
|
+
"""
|
|
6936
|
+
root = root.resolve()
|
|
6937
|
+
if not root.is_dir():
|
|
6938
|
+
raise OSError(f"archive payload directory not found: {root}")
|
|
6939
|
+
digest = hashlib.sha256()
|
|
6940
|
+
for directory, dir_names, file_names in os.walk(root, followlinks=False):
|
|
6941
|
+
current = Path(directory)
|
|
6942
|
+
dir_names.sort()
|
|
6943
|
+
file_names.sort()
|
|
6944
|
+
for name in dir_names:
|
|
6945
|
+
child = current / name
|
|
6946
|
+
if child.is_symlink():
|
|
6947
|
+
raise OSError(f"durable archive refuses directory link: {child}")
|
|
6948
|
+
relative = child.relative_to(root).as_posix()
|
|
6949
|
+
digest.update(b"D\0")
|
|
6950
|
+
digest.update(relative.encode("utf-8"))
|
|
6951
|
+
digest.update(b"\0")
|
|
6952
|
+
for name in file_names:
|
|
6953
|
+
child = current / name
|
|
6954
|
+
if child.is_symlink():
|
|
6955
|
+
raise OSError(f"durable archive refuses file link: {child}")
|
|
6956
|
+
relative = child.relative_to(root).as_posix()
|
|
6957
|
+
file_hash = hashlib.sha256()
|
|
6958
|
+
size = 0
|
|
6959
|
+
with child.open("rb") as handle:
|
|
6960
|
+
while True:
|
|
6961
|
+
chunk = handle.read(1024 * 1024)
|
|
6962
|
+
if not chunk:
|
|
6963
|
+
break
|
|
6964
|
+
size += len(chunk)
|
|
6965
|
+
file_hash.update(chunk)
|
|
6966
|
+
digest.update(b"F\0")
|
|
6967
|
+
digest.update(relative.encode("utf-8"))
|
|
6968
|
+
digest.update(b"\0")
|
|
6969
|
+
digest.update(str(size).encode("ascii"))
|
|
6970
|
+
digest.update(b"\0")
|
|
6971
|
+
digest.update(file_hash.digest())
|
|
6972
|
+
digest.update(b"\0")
|
|
6973
|
+
return "sha256:" + digest.hexdigest()
|
|
6974
|
+
|
|
6975
|
+
|
|
6976
|
+
def _atomic_write_json(path: Path, data: dict[str, Any]) -> None:
|
|
6977
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
6978
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
|
|
6979
|
+
encoded = (
|
|
6980
|
+
json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
6981
|
+
).encode("utf-8")
|
|
6982
|
+
try:
|
|
6983
|
+
with temporary.open("wb") as handle:
|
|
6984
|
+
handle.write(encoded)
|
|
6985
|
+
handle.flush()
|
|
6986
|
+
os.fsync(handle.fileno())
|
|
6987
|
+
os.replace(temporary, path)
|
|
6988
|
+
except BaseException:
|
|
6989
|
+
temporary.unlink(missing_ok=True)
|
|
6990
|
+
raise
|
|
6991
|
+
|
|
6992
|
+
|
|
6993
|
+
def write_durable_archive(
|
|
6994
|
+
archive_payload: Path,
|
|
6995
|
+
durable_root: Path,
|
|
6996
|
+
*,
|
|
6997
|
+
archive_id: str,
|
|
6998
|
+
retention_policy: str,
|
|
6999
|
+
) -> dict[str, Any]:
|
|
7000
|
+
"""Publish and read back a content-addressed durable archive.
|
|
7001
|
+
|
|
7002
|
+
The receipt is outside the payload so the payload hash is not
|
|
7003
|
+
self-referential. An existing object is reusable only when its full tree
|
|
7004
|
+
hash matches the requested content.
|
|
7005
|
+
"""
|
|
7006
|
+
source = archive_payload.resolve()
|
|
7007
|
+
root = durable_root.resolve()
|
|
7008
|
+
if not retention_policy.strip():
|
|
7009
|
+
raise ValueError("retention policy must not be empty")
|
|
7010
|
+
if (
|
|
7011
|
+
not archive_id
|
|
7012
|
+
or archive_id in {".", ".."}
|
|
7013
|
+
or Path(archive_id).name != archive_id
|
|
7014
|
+
):
|
|
7015
|
+
raise ValueError(f"invalid durable archive id: {archive_id!r}")
|
|
7016
|
+
|
|
7017
|
+
payload_hash = _archive_tree_digest(source)
|
|
7018
|
+
digest_hex = payload_hash.removeprefix("sha256:")
|
|
7019
|
+
object_dir = root / "objects" / "sha256" / digest_hex
|
|
7020
|
+
durable_payload = object_dir / "payload"
|
|
7021
|
+
receipt_path = root / "receipts" / f"{archive_id}.json"
|
|
7022
|
+
staging_root = root / ".staging" / f"d-{uuid.uuid4().hex}"
|
|
7023
|
+
staged_payload = staging_root / "payload"
|
|
7024
|
+
object_reused = False
|
|
7025
|
+
|
|
7026
|
+
try:
|
|
7027
|
+
if durable_payload.exists():
|
|
7028
|
+
if not durable_payload.is_dir():
|
|
7029
|
+
raise OSError(f"durable object path is not a directory: {durable_payload}")
|
|
7030
|
+
readback_hash = _archive_tree_digest(durable_payload)
|
|
7031
|
+
if readback_hash != payload_hash:
|
|
7032
|
+
raise OSError(
|
|
7033
|
+
"existing durable object failed readback verification: "
|
|
7034
|
+
f"expected {payload_hash}, got {readback_hash}"
|
|
7035
|
+
)
|
|
7036
|
+
object_reused = True
|
|
7037
|
+
else:
|
|
7038
|
+
staging_root.mkdir(parents=True, exist_ok=False)
|
|
7039
|
+
shutil.copytree(source, staged_payload, copy_function=shutil.copy2)
|
|
7040
|
+
staged_hash = _archive_tree_digest(staged_payload)
|
|
7041
|
+
if staged_hash != payload_hash:
|
|
7042
|
+
raise OSError(
|
|
7043
|
+
"durable staging readback mismatch: "
|
|
7044
|
+
f"expected {payload_hash}, got {staged_hash}"
|
|
7045
|
+
)
|
|
7046
|
+
object_dir.mkdir(parents=True, exist_ok=True)
|
|
7047
|
+
try:
|
|
7048
|
+
os.replace(staged_payload, durable_payload)
|
|
7049
|
+
except OSError:
|
|
7050
|
+
# A concurrent writer may have published the same object.
|
|
7051
|
+
if not durable_payload.is_dir():
|
|
7052
|
+
raise
|
|
7053
|
+
readback_hash = _archive_tree_digest(durable_payload)
|
|
7054
|
+
if readback_hash != payload_hash:
|
|
7055
|
+
raise
|
|
7056
|
+
object_reused = True
|
|
7057
|
+
|
|
7058
|
+
verified_hash = _archive_tree_digest(durable_payload)
|
|
7059
|
+
if verified_hash != payload_hash:
|
|
7060
|
+
raise OSError(
|
|
7061
|
+
"durable object verification failed: "
|
|
7062
|
+
f"expected {payload_hash}, got {verified_hash}"
|
|
7063
|
+
)
|
|
7064
|
+
|
|
7065
|
+
receipt = {
|
|
7066
|
+
"schemaVersion": 1,
|
|
7067
|
+
"archiveId": archive_id,
|
|
7068
|
+
"status": "ARCHIVED_DURABLE",
|
|
7069
|
+
"payloadHash": payload_hash,
|
|
7070
|
+
"payloadPath": str(durable_payload),
|
|
7071
|
+
"retentionPolicy": retention_policy,
|
|
7072
|
+
"verifiedAt": now_iso(),
|
|
7073
|
+
"verification": {
|
|
7074
|
+
"algorithm": "sha256-tree-v1",
|
|
7075
|
+
"readBack": True,
|
|
7076
|
+
},
|
|
7077
|
+
"sourceDeletion": {
|
|
7078
|
+
"authorizedOnlyAfterVerification": True,
|
|
7079
|
+
},
|
|
7080
|
+
}
|
|
7081
|
+
if receipt_path.exists():
|
|
7082
|
+
existing = read_json(receipt_path)
|
|
7083
|
+
if (
|
|
7084
|
+
not isinstance(existing, dict)
|
|
7085
|
+
or existing.get("payloadHash") != payload_hash
|
|
7086
|
+
or existing.get("archiveId") != archive_id
|
|
7087
|
+
):
|
|
7088
|
+
raise OSError(
|
|
7089
|
+
f"durable receipt already exists for different content: {receipt_path}"
|
|
7090
|
+
)
|
|
7091
|
+
receipt = existing
|
|
7092
|
+
else:
|
|
7093
|
+
_atomic_write_json(receipt_path, receipt)
|
|
7094
|
+
|
|
7095
|
+
return {
|
|
7096
|
+
"status": "ARCHIVED_DURABLE",
|
|
7097
|
+
"payloadHash": payload_hash,
|
|
7098
|
+
"payloadPath": str(durable_payload),
|
|
7099
|
+
"receiptPath": str(receipt_path),
|
|
7100
|
+
"retentionPolicy": retention_policy,
|
|
7101
|
+
"readBackVerified": True,
|
|
7102
|
+
"objectReused": object_reused,
|
|
7103
|
+
}
|
|
7104
|
+
finally:
|
|
7105
|
+
shutil.rmtree(staging_root, ignore_errors=True)
|
|
7106
|
+
staging_parent = root / ".staging"
|
|
7107
|
+
try:
|
|
7108
|
+
staging_parent.rmdir()
|
|
7109
|
+
except OSError:
|
|
7110
|
+
pass
|
|
7111
|
+
|
|
7112
|
+
|
|
7113
|
+
def restore_durable_archive(receipt_path: Path, target_root: Path) -> dict[str, Any]:
|
|
7114
|
+
"""Restore a verified durable object without overwriting an existing archive."""
|
|
7115
|
+
receipt_file = receipt_path.resolve()
|
|
7116
|
+
receipt = read_json(receipt_file)
|
|
7117
|
+
if not isinstance(receipt, dict) or receipt.get("schemaVersion") != 1:
|
|
7118
|
+
raise ValueError(f"invalid durable archive receipt: {receipt_file}")
|
|
7119
|
+
if receipt.get("status") != "ARCHIVED_DURABLE":
|
|
7120
|
+
raise ValueError(f"receipt is not durable: {receipt_file}")
|
|
7121
|
+
|
|
7122
|
+
archive_id = str(receipt.get("archiveId") or "")
|
|
7123
|
+
payload_hash = str(receipt.get("payloadHash") or "")
|
|
7124
|
+
if (
|
|
7125
|
+
not archive_id
|
|
7126
|
+
or Path(archive_id).name != archive_id
|
|
7127
|
+
or not re.fullmatch(r"sha256:[0-9a-f]{64}", payload_hash)
|
|
7128
|
+
):
|
|
7129
|
+
raise ValueError(f"invalid durable archive receipt fields: {receipt_file}")
|
|
7130
|
+
|
|
7131
|
+
durable_root = receipt_file.parent.parent.resolve()
|
|
7132
|
+
expected_payload = (
|
|
7133
|
+
durable_root
|
|
7134
|
+
/ "objects"
|
|
7135
|
+
/ "sha256"
|
|
7136
|
+
/ payload_hash.removeprefix("sha256:")
|
|
7137
|
+
/ "payload"
|
|
7138
|
+
).resolve()
|
|
7139
|
+
declared_payload = Path(str(receipt.get("payloadPath") or "")).resolve()
|
|
7140
|
+
if declared_payload != expected_payload or not _path_is_within(
|
|
7141
|
+
expected_payload, durable_root / "objects"
|
|
7142
|
+
):
|
|
7143
|
+
raise ValueError("durable receipt payload path is outside its object store")
|
|
7144
|
+
actual_hash = _archive_tree_digest(expected_payload)
|
|
7145
|
+
if actual_hash != payload_hash:
|
|
7146
|
+
raise OSError(
|
|
7147
|
+
f"durable payload hash mismatch: expected {payload_hash}, got {actual_hash}"
|
|
7148
|
+
)
|
|
7149
|
+
|
|
7150
|
+
restore_root = target_root.resolve()
|
|
7151
|
+
destination = restore_root / archive_id
|
|
7152
|
+
if destination.exists():
|
|
7153
|
+
raise FileExistsError(f"restore destination already exists: {destination}")
|
|
7154
|
+
restore_root.mkdir(parents=True, exist_ok=True)
|
|
7155
|
+
staging = restore_root / f".restore-{uuid.uuid4().hex}"
|
|
7156
|
+
try:
|
|
7157
|
+
shutil.copytree(expected_payload, staging, copy_function=shutil.copy2)
|
|
7158
|
+
restored_hash = _archive_tree_digest(staging)
|
|
7159
|
+
if restored_hash != payload_hash:
|
|
7160
|
+
raise OSError(
|
|
7161
|
+
f"restored payload hash mismatch: expected {payload_hash}, got {restored_hash}"
|
|
7162
|
+
)
|
|
7163
|
+
os.replace(staging, destination)
|
|
7164
|
+
finally:
|
|
7165
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
7166
|
+
|
|
7167
|
+
return {
|
|
7168
|
+
"ok": True,
|
|
7169
|
+
"action": "restore-durable",
|
|
7170
|
+
"archiveId": archive_id,
|
|
7171
|
+
"payloadHash": payload_hash,
|
|
7172
|
+
"readBackVerified": True,
|
|
7173
|
+
"restoredArchive": str(destination),
|
|
7174
|
+
}
|
|
7175
|
+
|
|
7176
|
+
|
|
6899
7177
|
# ---------------------------------------------------------------------------
|
|
6900
7178
|
# finalize
|
|
6901
7179
|
# ---------------------------------------------------------------------------
|
|
@@ -6905,6 +7183,8 @@ def cmd_finalize(
|
|
|
6905
7183
|
change_dir: Path,
|
|
6906
7184
|
archive_root: Path,
|
|
6907
7185
|
*,
|
|
7186
|
+
durable_root: Path | None = None,
|
|
7187
|
+
retention_policy: str = "unspecified",
|
|
6908
7188
|
skip_ingest: bool = False,
|
|
6909
7189
|
allow_missing_review: bool = False,
|
|
6910
7190
|
archive_intent: str = "release-candidate",
|
|
@@ -6923,6 +7203,26 @@ def cmd_finalize(
|
|
|
6923
7203
|
archive_root.mkdir(parents=True, exist_ok=True)
|
|
6924
7204
|
archive_dir = archive_root / f"{today_date()}-{change_name}"
|
|
6925
7205
|
project_root = find_project_root(original_change_dir)
|
|
7206
|
+
if durable_root is not None:
|
|
7207
|
+
durable_root = durable_root.resolve()
|
|
7208
|
+
if _path_is_within(durable_root, project_root):
|
|
7209
|
+
return 1, {
|
|
7210
|
+
"ok": False,
|
|
7211
|
+
"action": "finalize",
|
|
7212
|
+
"change_dir": str(original_change_dir),
|
|
7213
|
+
"archive_dir": str(archive_dir),
|
|
7214
|
+
"issues": [
|
|
7215
|
+
{
|
|
7216
|
+
"code": "DURABLE_ROOT_NOT_INDEPENDENT",
|
|
7217
|
+
"severity": "error",
|
|
7218
|
+
"message": (
|
|
7219
|
+
"durable root must be outside the project so project-local "
|
|
7220
|
+
"state loss cannot delete both copies"
|
|
7221
|
+
),
|
|
7222
|
+
}
|
|
7223
|
+
],
|
|
7224
|
+
"error": "durable root is inside the project",
|
|
7225
|
+
}
|
|
6926
7226
|
try:
|
|
6927
7227
|
resolved_state_dir = hp.resolve_state_dir_for_contract(
|
|
6928
7228
|
original_change_dir, project_root
|
|
@@ -7281,6 +7581,29 @@ def cmd_finalize(
|
|
|
7281
7581
|
for_replay=False,
|
|
7282
7582
|
)
|
|
7283
7583
|
summary_path = work_dir / "reports" / "final" / "summary-data.json"
|
|
7584
|
+
summary["archiveDurability"] = {
|
|
7585
|
+
"status": (
|
|
7586
|
+
"ARCHIVED_DURABLE"
|
|
7587
|
+
if durable_root is not None
|
|
7588
|
+
else "ARCHIVED_LOCAL_ONLY"
|
|
7589
|
+
),
|
|
7590
|
+
"retentionPolicy": (
|
|
7591
|
+
retention_policy if durable_root is not None else "project-local"
|
|
7592
|
+
),
|
|
7593
|
+
"readBackRequiredBeforeSourceDeletion": durable_root is not None,
|
|
7594
|
+
"risk": (
|
|
7595
|
+
None
|
|
7596
|
+
if durable_root is not None
|
|
7597
|
+
else (
|
|
7598
|
+
"The archive exists only under the project-local .harness tree "
|
|
7599
|
+
"and may be lost with that workspace."
|
|
7600
|
+
)
|
|
7601
|
+
),
|
|
7602
|
+
}
|
|
7603
|
+
payload["archiveDurability"] = _deepcopy_json(
|
|
7604
|
+
summary["archiveDurability"]
|
|
7605
|
+
)
|
|
7606
|
+
write_json(summary_path, summary)
|
|
7284
7607
|
if allow_missing_review:
|
|
7285
7608
|
reasons = list(summary.get("finalStatusReasons") or [])
|
|
7286
7609
|
reason = "review missing on full tier (allowed by user)"
|
|
@@ -7606,6 +7929,43 @@ def cmd_finalize(
|
|
|
7606
7929
|
payload["ok"] = False
|
|
7607
7930
|
return 1, payload
|
|
7608
7931
|
|
|
7932
|
+
# A durable sink, when requested, is committed and read back before either
|
|
7933
|
+
# publishing the local archive or deleting the source change directory.
|
|
7934
|
+
if durable_root is not None:
|
|
7935
|
+
try:
|
|
7936
|
+
durability = write_durable_archive(
|
|
7937
|
+
operation_temp_dir,
|
|
7938
|
+
durable_root,
|
|
7939
|
+
archive_id=archive_dir.name,
|
|
7940
|
+
retention_policy=retention_policy,
|
|
7941
|
+
)
|
|
7942
|
+
payload["archiveDurability"] = durability
|
|
7943
|
+
payload["steps"]["durable_archive"] = {
|
|
7944
|
+
"ok": True,
|
|
7945
|
+
**durability,
|
|
7946
|
+
}
|
|
7947
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
7948
|
+
payload["error"] = f"durable archive write failed: {exc}"
|
|
7949
|
+
payload["issues"] = [
|
|
7950
|
+
{
|
|
7951
|
+
"code": "DURABLE_ARCHIVE_WRITE_FAILED",
|
|
7952
|
+
"severity": "error",
|
|
7953
|
+
"message": str(exc),
|
|
7954
|
+
}
|
|
7955
|
+
]
|
|
7956
|
+
payload["steps"]["durable_archive"] = {
|
|
7957
|
+
"ok": False,
|
|
7958
|
+
"error": str(exc),
|
|
7959
|
+
}
|
|
7960
|
+
payload["steps"]["delete_original"] = {
|
|
7961
|
+
"ok": False,
|
|
7962
|
+
"deleted": False,
|
|
7963
|
+
}
|
|
7964
|
+
_restore_finalize_failure()
|
|
7965
|
+
payload["warnings"] = warnings
|
|
7966
|
+
payload["ok"] = False
|
|
7967
|
+
return 1, payload
|
|
7968
|
+
|
|
7609
7969
|
# Publish only after every validator passes. The archive path is never used
|
|
7610
7970
|
# as mutable staging, so a failed attempt cannot poison the next retry.
|
|
7611
7971
|
try:
|
|
@@ -7679,6 +8039,7 @@ def cmd_finalize(
|
|
|
7679
8039
|
"manifestSha256": sha256_file(
|
|
7680
8040
|
archive_dir / "evidence" / "archive-manifest-after.json"
|
|
7681
8041
|
),
|
|
8042
|
+
"archiveDurability": payload.get("archiveDurability"),
|
|
7682
8043
|
},
|
|
7683
8044
|
)
|
|
7684
8045
|
except OSError as exc:
|
|
@@ -7854,9 +8215,18 @@ def cmd_certify_local_cli(args: argparse.Namespace) -> int:
|
|
|
7854
8215
|
def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
7855
8216
|
change_dir = resolve_path(args.change_dir)
|
|
7856
8217
|
archive_root = resolve_path(args.archive_root)
|
|
8218
|
+
durable_root = (
|
|
8219
|
+
resolve_path(args.durable_root)
|
|
8220
|
+
if getattr(args, "durable_root", None)
|
|
8221
|
+
else None
|
|
8222
|
+
)
|
|
7857
8223
|
code, payload = cmd_finalize(
|
|
7858
8224
|
change_dir,
|
|
7859
8225
|
archive_root,
|
|
8226
|
+
durable_root=durable_root,
|
|
8227
|
+
retention_policy=str(
|
|
8228
|
+
getattr(args, "retention_policy", "unspecified")
|
|
8229
|
+
),
|
|
7860
8230
|
skip_ingest=bool(args.skip_ingest),
|
|
7861
8231
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
7862
8232
|
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
@@ -7865,6 +8235,25 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
7865
8235
|
return code
|
|
7866
8236
|
|
|
7867
8237
|
|
|
8238
|
+
def cmd_restore_durable_cli(args: argparse.Namespace) -> int:
|
|
8239
|
+
receipt_path = resolve_path(args.receipt)
|
|
8240
|
+
target_root = resolve_path(args.target_root)
|
|
8241
|
+
try:
|
|
8242
|
+
payload = restore_durable_archive(receipt_path, target_root)
|
|
8243
|
+
code = 0
|
|
8244
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
8245
|
+
code = 1
|
|
8246
|
+
payload = {
|
|
8247
|
+
"ok": False,
|
|
8248
|
+
"action": "restore-durable",
|
|
8249
|
+
"receipt": str(receipt_path),
|
|
8250
|
+
"targetRoot": str(target_root),
|
|
8251
|
+
"error": str(exc),
|
|
8252
|
+
}
|
|
8253
|
+
emit_json(payload)
|
|
8254
|
+
return code
|
|
8255
|
+
|
|
8256
|
+
|
|
7868
8257
|
def cmd_replay_cli(args: argparse.Namespace) -> int:
|
|
7869
8258
|
archive_dir = resolve_path(args.archive_dir)
|
|
7870
8259
|
out_path = resolve_path(args.out) if getattr(args, "out", None) else None
|
|
@@ -8087,6 +8476,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
8087
8476
|
p_fin = sub.add_parser("finalize", help="single-process archive finalize")
|
|
8088
8477
|
p_fin.add_argument("--change-dir", required=True)
|
|
8089
8478
|
p_fin.add_argument("--archive-root", required=True)
|
|
8479
|
+
p_fin.add_argument(
|
|
8480
|
+
"--durable-root",
|
|
8481
|
+
default=None,
|
|
8482
|
+
help=(
|
|
8483
|
+
"independent content-addressed archive store; it must be outside "
|
|
8484
|
+
"the project and is verified before source deletion"
|
|
8485
|
+
),
|
|
8486
|
+
)
|
|
8487
|
+
p_fin.add_argument(
|
|
8488
|
+
"--retention-policy",
|
|
8489
|
+
default="unspecified",
|
|
8490
|
+
help="retention policy recorded in the durable archive receipt",
|
|
8491
|
+
)
|
|
8090
8492
|
p_fin.add_argument("--skip-ingest", action="store_true")
|
|
8091
8493
|
p_fin.add_argument(
|
|
8092
8494
|
"--allow-missing-review",
|
|
@@ -8102,6 +8504,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
8102
8504
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
8103
8505
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
8104
8506
|
|
|
8507
|
+
p_restore = sub.add_parser(
|
|
8508
|
+
"restore-durable",
|
|
8509
|
+
help="verify a durable receipt and restore its archive without overwriting",
|
|
8510
|
+
)
|
|
8511
|
+
p_restore.add_argument("--receipt", required=True)
|
|
8512
|
+
p_restore.add_argument("--target-root", required=True)
|
|
8513
|
+
p_restore.add_argument("--json", action="store_true", default=True)
|
|
8514
|
+
p_restore.set_defaults(func=cmd_restore_durable_cli)
|
|
8515
|
+
|
|
8105
8516
|
p_rep = sub.add_parser("replay", help="read-only re-collect + validate")
|
|
8106
8517
|
p_rep.add_argument("--archive-dir", required=True)
|
|
8107
8518
|
p_rep.add_argument("--out", default=None, help="write summary-data JSON outside archive")
|
|
@@ -316,48 +316,6 @@ def validate_build_paths(skills_root: Path, out_dir: Path) -> tuple[Path, Path]:
|
|
|
316
316
|
return skills_root, out_dir
|
|
317
317
|
|
|
318
318
|
|
|
319
|
-
def _git_repo_root(start: Path) -> Path | None:
|
|
320
|
-
"""Return the git toplevel containing ``start``, or None if not a git repo."""
|
|
321
|
-
try:
|
|
322
|
-
r = subprocess.run(
|
|
323
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
324
|
-
cwd=str(start),
|
|
325
|
-
capture_output=True,
|
|
326
|
-
text=True,
|
|
327
|
-
timeout=5,
|
|
328
|
-
)
|
|
329
|
-
except (OSError, subprocess.SubprocessError):
|
|
330
|
-
return None
|
|
331
|
-
if r.returncode != 0 or not r.stdout.strip():
|
|
332
|
-
return None
|
|
333
|
-
return Path(r.stdout.strip())
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _git_blob_sha(path: Path, repo_root: Path) -> str | None:
|
|
337
|
-
"""Return the git blob sha for a tracked file (CRLF-normalized by .gitattributes),
|
|
338
|
-
or None if untracked / git unavailable. Caller falls back to working-copy sha256
|
|
339
|
-
for untracked build inputs (deterministic enough outside git's normalization)."""
|
|
340
|
-
try:
|
|
341
|
-
rel = path.resolve().relative_to(repo_root.resolve())
|
|
342
|
-
except ValueError:
|
|
343
|
-
return None
|
|
344
|
-
try:
|
|
345
|
-
r = subprocess.run(
|
|
346
|
-
["git", "ls-files", "-s", "--", rel.as_posix()],
|
|
347
|
-
cwd=str(repo_root),
|
|
348
|
-
capture_output=True,
|
|
349
|
-
text=True,
|
|
350
|
-
timeout=5,
|
|
351
|
-
)
|
|
352
|
-
except (OSError, subprocess.SubprocessError):
|
|
353
|
-
return None
|
|
354
|
-
if r.returncode != 0 or not r.stdout.strip():
|
|
355
|
-
return None
|
|
356
|
-
# format: "<mode> <sha> <stage>\t<path>"
|
|
357
|
-
parts = r.stdout.strip().splitlines()[0].split()
|
|
358
|
-
return parts[1] if len(parts) >= 2 else None
|
|
359
|
-
|
|
360
|
-
|
|
361
319
|
def core_content_hash(
|
|
362
320
|
skills_root: Path,
|
|
363
321
|
overlay_dir: Path | None,
|
|
@@ -387,17 +345,18 @@ def core_content_hash(
|
|
|
387
345
|
if agent_overlay_dir and agent_overlay_dir.is_dir():
|
|
388
346
|
files.extend(p for p in agent_overlay_dir.rglob("*") if p.is_file())
|
|
389
347
|
skills_root_resolved = skills_root.resolve()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
348
|
+
files_by_relative_path = {
|
|
349
|
+
f.resolve().relative_to(skills_root_resolved).as_posix(): f
|
|
350
|
+
for f in files
|
|
351
|
+
}
|
|
352
|
+
for rel in sorted(files_by_relative_path):
|
|
353
|
+
f = files_by_relative_path[rel]
|
|
393
354
|
h.update(rel.encode("utf-8"))
|
|
394
355
|
h.update(b"\0")
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
blob = _git_blob_sha(f, repo_root) if repo_root else None
|
|
400
|
-
h.update((blob or sha256_file(f)).encode("ascii"))
|
|
356
|
+
# Hash the actual build input with one algorithm regardless of whether the
|
|
357
|
+
# source happens to live inside a Git checkout. Relative POSIX-path sorting
|
|
358
|
+
# keeps the order identical on case-insensitive Windows and Linux.
|
|
359
|
+
h.update(sha256_file(f).encode("ascii"))
|
|
401
360
|
h.update(b"\0")
|
|
402
361
|
return h.hexdigest()[:16]
|
|
403
362
|
|