@hunter-harness/workflow-harness 0.2.54 → 0.2.56
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/README.md +3 -3
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +8 -4
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +3 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/claude-code/harness-submit/checklist.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 +8 -6
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/claude-code/scripts/harness_context.py +236 -63
- package/harness/bundles/general/claude-code/scripts/harness_events.py +86 -5
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +3 -3
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +8 -4
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +236 -63
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +86 -5
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +3 -3
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +3 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-test/reference.md +2 -2
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/codex/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/codex/scripts/harness_context.py +236 -63
- package/harness/bundles/general/codex/scripts/harness_events.py +86 -5
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/codex/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/codex/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +3 -3
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
- 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 +7 -3
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-run/SKILL.md +9 -7
- package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +3 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -6
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/general/cursor/scripts/harness_archive.py +322 -74
- package/harness/bundles/general/cursor/scripts/harness_context.py +236 -63
- package/harness/bundles/general/cursor/scripts/harness_events.py +86 -5
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/general/cursor/scripts/harness_gate.py +353 -3
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +195 -11
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +3 -3
- package/harness/bundles/java/claude-code/contracts/workflow-policy.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 +21 -33
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +8 -4
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/claude-code/harness-submit/checklist.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 +8 -6
- package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/claude-code/scripts/harness_context.py +236 -63
- package/harness/bundles/java/claude-code/scripts/harness_events.py +86 -5
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +3 -3
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +8 -4
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +236 -63
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +86 -5
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +3 -3
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-run/reference.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/codex/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/codex/scripts/harness_context.py +236 -63
- package/harness/bundles/java/codex/scripts/harness_events.py +86 -5
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/codex/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +3 -3
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
- package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
- 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-package/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-run/SKILL.md +9 -7
- package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -6
- package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
- package/harness/bundles/java/cursor/scripts/harness_archive.py +322 -74
- package/harness/bundles/java/cursor/scripts/harness_context.py +236 -63
- package/harness/bundles/java/cursor/scripts/harness_events.py +86 -5
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +201 -17
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +353 -3
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +195 -11
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +82 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
- package/harness/manifests/general/claude-code.json +32 -32
- package/harness/manifests/general/codebuddy.json +32 -32
- package/harness/manifests/general/codex.json +32 -32
- package/harness/manifests/general/cursor.json +32 -32
- package/harness/manifests/java/claude-code.json +35 -35
- package/harness/manifests/java/codebuddy.json +35 -35
- package/harness/manifests/java/codex.json +35 -35
- package/harness/manifests/java/cursor.json +35 -35
- package/hunter-workflow-family.json +5 -4
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import argparse
|
|
|
17
17
|
import datetime as dt
|
|
18
18
|
import hashlib
|
|
19
19
|
import io
|
|
20
|
+
import ipaddress
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
@@ -31,6 +32,7 @@ import zipfile
|
|
|
31
32
|
from collections.abc import Mapping
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
from typing import Any
|
|
35
|
+
from urllib.parse import urlsplit
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
if hasattr(sys.stdout, "reconfigure"):
|
|
@@ -158,6 +160,39 @@ def infer_change_name(dir_path: Path) -> str:
|
|
|
158
160
|
return name
|
|
159
161
|
|
|
160
162
|
|
|
163
|
+
def resolve_archive_state_root(change_dir: Path) -> Path:
|
|
164
|
+
"""Resolve the dynamic state view shared by status, certify and finalize."""
|
|
165
|
+
change_dir = change_dir.resolve()
|
|
166
|
+
return hp.resolve_state_dir_for_contract(
|
|
167
|
+
change_dir,
|
|
168
|
+
find_project_root(change_dir),
|
|
169
|
+
).resolve()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def archive_read_roots(change_dir: Path) -> list[Path]:
|
|
173
|
+
"""Return dynamic state first and the immutable contract as fallback."""
|
|
174
|
+
contract = change_dir.resolve()
|
|
175
|
+
state = resolve_archive_state_root(contract)
|
|
176
|
+
return [state] if state == contract else [state, contract]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _first_archive_path(change_dir: Path, relative: str) -> Path | None:
|
|
180
|
+
for root in archive_read_roots(change_dir):
|
|
181
|
+
candidate = root / relative
|
|
182
|
+
if candidate.is_file():
|
|
183
|
+
return candidate
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _archive_path_label(change_dir: Path, path: Path) -> str:
|
|
188
|
+
for root in archive_read_roots(change_dir):
|
|
189
|
+
try:
|
|
190
|
+
return path.resolve().relative_to(root).as_posix()
|
|
191
|
+
except ValueError:
|
|
192
|
+
continue
|
|
193
|
+
return path.name
|
|
194
|
+
|
|
195
|
+
|
|
161
196
|
def load_template() -> dict[str, Any]:
|
|
162
197
|
if SUMMARY_TEMPLATE.is_file():
|
|
163
198
|
data = read_json(SUMMARY_TEMPLATE)
|
|
@@ -192,9 +227,9 @@ def append_event(
|
|
|
192
227
|
for key, value in fields.items():
|
|
193
228
|
if value is not None:
|
|
194
229
|
event[key] = value
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
all_events =
|
|
230
|
+
result = he.append_with_auto_seal(path, event, existing_events=existing)
|
|
231
|
+
event = result["event"]
|
|
232
|
+
all_events = he.load_events(path)
|
|
198
233
|
he.write_execution_log(change_dir, he.render_execution_log(all_events))
|
|
199
234
|
return event
|
|
200
235
|
|
|
@@ -216,6 +251,8 @@ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
|
216
251
|
for path in sorted(root.rglob("*")):
|
|
217
252
|
if not path.is_file():
|
|
218
253
|
continue
|
|
254
|
+
if path.name.endswith(".lock"):
|
|
255
|
+
continue
|
|
219
256
|
if exclude is not None and path.resolve() == exclude:
|
|
220
257
|
continue
|
|
221
258
|
rel = path.relative_to(root).as_posix()
|
|
@@ -312,12 +349,14 @@ def compare_manifests(
|
|
|
312
349
|
|
|
313
350
|
|
|
314
351
|
def load_ledger(change_dir: Path) -> dict[str, Any] | None:
|
|
315
|
-
for
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
352
|
+
for root in archive_read_roots(change_dir):
|
|
353
|
+
for rel in (
|
|
354
|
+
"evidence/verification-ledger.json",
|
|
355
|
+
"verification-ledger.json",
|
|
356
|
+
):
|
|
357
|
+
path = root / rel
|
|
358
|
+
if not path.is_file():
|
|
359
|
+
continue
|
|
321
360
|
try:
|
|
322
361
|
data = read_json(path)
|
|
323
362
|
return data if isinstance(data, dict) else None
|
|
@@ -328,14 +367,15 @@ def load_ledger(change_dir: Path) -> dict[str, Any] | None:
|
|
|
328
367
|
|
|
329
368
|
def load_ci_metrics(change_dir: Path) -> tuple[dict[str, Any] | None, str | None]:
|
|
330
369
|
"""Load schema-versioned runner metrics without parsing human CI logs."""
|
|
331
|
-
for
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
370
|
+
for root in archive_read_roots(change_dir):
|
|
371
|
+
for relative in ("evidence/ci-metrics.json", "runtime/ci-metrics.json"):
|
|
372
|
+
path = root / relative
|
|
373
|
+
if not path.is_file():
|
|
374
|
+
continue
|
|
375
|
+
value = read_json(path)
|
|
376
|
+
if value.get("schemaVersion") != 1:
|
|
377
|
+
raise ValueError(f"unsupported ci-metrics schema: {path}")
|
|
378
|
+
return value, relative
|
|
339
379
|
return None, None
|
|
340
380
|
|
|
341
381
|
|
|
@@ -449,8 +489,8 @@ def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
|
|
|
449
489
|
"meta/product-candidate-verification.json",
|
|
450
490
|
"runtime/product-candidate-verification.json",
|
|
451
491
|
):
|
|
452
|
-
path = change_dir
|
|
453
|
-
if
|
|
492
|
+
path = _first_archive_path(change_dir, relative)
|
|
493
|
+
if path is None:
|
|
454
494
|
continue
|
|
455
495
|
try:
|
|
456
496
|
data = read_json(path)
|
|
@@ -855,14 +895,21 @@ def _candidate_value_hash(value: Any) -> str:
|
|
|
855
895
|
def _candidate_evidence_hash(change_dir: Path, value: Any) -> str:
|
|
856
896
|
if isinstance(value, str) and value.strip():
|
|
857
897
|
raw = Path(value.strip())
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
898
|
+
candidates = [raw] if raw.is_absolute() else [
|
|
899
|
+
root / raw for root in archive_read_roots(change_dir)
|
|
900
|
+
]
|
|
901
|
+
for candidate in candidates:
|
|
902
|
+
try:
|
|
903
|
+
resolved = candidate.resolve()
|
|
904
|
+
if not any(
|
|
905
|
+
resolved.is_relative_to(root.resolve())
|
|
906
|
+
for root in archive_read_roots(change_dir)
|
|
907
|
+
):
|
|
908
|
+
continue
|
|
909
|
+
except OSError:
|
|
910
|
+
continue
|
|
911
|
+
if resolved.is_file():
|
|
912
|
+
return f"sha256:{sha256_file(resolved)}"
|
|
866
913
|
return _candidate_value_hash(value)
|
|
867
914
|
|
|
868
915
|
|
|
@@ -978,8 +1025,6 @@ def certify_local_candidate(
|
|
|
978
1025
|
"command",
|
|
979
1026
|
"evidence",
|
|
980
1027
|
"inputsHash",
|
|
981
|
-
"toolchainHash",
|
|
982
|
-
"environmentHash",
|
|
983
1028
|
):
|
|
984
1029
|
if entry.get(field) in (None, "", [], {}):
|
|
985
1030
|
missing.append(field)
|
|
@@ -992,25 +1037,24 @@ def certify_local_candidate(
|
|
|
992
1037
|
"name": name,
|
|
993
1038
|
"command": entry.get("command"),
|
|
994
1039
|
"inputsHash": entry.get("inputsHash"),
|
|
995
|
-
"toolchainHash":
|
|
996
|
-
|
|
1040
|
+
"toolchainHash": (
|
|
1041
|
+
entry.get("toolchainHash")
|
|
1042
|
+
or hl.default_toolchain_hash(project_root)
|
|
1043
|
+
),
|
|
1044
|
+
"environmentHash": (
|
|
1045
|
+
entry.get("environmentHash")
|
|
1046
|
+
or hl.default_environment_hash(project_root)
|
|
1047
|
+
),
|
|
1048
|
+
"inputsFiles": list(entry.get("inputsFiles") or []),
|
|
997
1049
|
"logHash": _candidate_evidence_hash(
|
|
998
1050
|
change_dir, entry.get("evidence")
|
|
999
1051
|
),
|
|
1000
1052
|
}
|
|
1001
1053
|
)
|
|
1002
1054
|
|
|
1003
|
-
ledger_path =
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
for relative in (
|
|
1007
|
-
"evidence/verification-ledger.json",
|
|
1008
|
-
"verification-ledger.json",
|
|
1009
|
-
)
|
|
1010
|
-
if (change_dir / relative).is_file()
|
|
1011
|
-
),
|
|
1012
|
-
None,
|
|
1013
|
-
)
|
|
1055
|
+
ledger_path = _first_archive_path(
|
|
1056
|
+
change_dir, "evidence/verification-ledger.json"
|
|
1057
|
+
) or _first_archive_path(change_dir, "verification-ledger.json")
|
|
1014
1058
|
if ledger_path is None:
|
|
1015
1059
|
raise ValueError("verification ledger path missing")
|
|
1016
1060
|
|
|
@@ -1021,15 +1065,29 @@ def certify_local_candidate(
|
|
|
1021
1065
|
or ""
|
|
1022
1066
|
).strip()
|
|
1023
1067
|
rc, current_head, _stderr = git_run(project_root, "rev-parse", "HEAD")
|
|
1068
|
+
rebound_from_commit: str | None = None
|
|
1024
1069
|
if rc == 0 and current_head:
|
|
1025
1070
|
if product_commit and not (
|
|
1026
1071
|
current_head.startswith(product_commit)
|
|
1027
1072
|
or product_commit.startswith(current_head)
|
|
1028
1073
|
):
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1074
|
+
for item in selected:
|
|
1075
|
+
input_files = [str(path) for path in item.get("inputsFiles") or []]
|
|
1076
|
+
if not input_files:
|
|
1077
|
+
raise ValueError(
|
|
1078
|
+
"product commit is stale and validation inputs are unavailable: "
|
|
1079
|
+
f"ledger={product_commit} current={current_head}"
|
|
1080
|
+
)
|
|
1081
|
+
current_inputs_hash, _paths = hl.compute_inputs_hash(
|
|
1082
|
+
hl.resolve_input_files(input_files, project_root),
|
|
1083
|
+
project_root=project_root,
|
|
1084
|
+
)
|
|
1085
|
+
if current_inputs_hash != item.get("inputsHash"):
|
|
1086
|
+
raise ValueError(
|
|
1087
|
+
"product commit is stale and validated inputs changed: "
|
|
1088
|
+
f"ledger={product_commit} current={current_head}"
|
|
1089
|
+
)
|
|
1090
|
+
rebound_from_commit = product_commit
|
|
1033
1091
|
product_commit = current_head
|
|
1034
1092
|
if not product_commit:
|
|
1035
1093
|
raise ValueError("product commit unavailable")
|
|
@@ -1043,7 +1101,7 @@ def certify_local_candidate(
|
|
|
1043
1101
|
)
|
|
1044
1102
|
current_tree_hash = f"sha256:{detail['hash']}"
|
|
1045
1103
|
recorded_tree_hash = str(ledger.get("productTreeHash") or "").strip()
|
|
1046
|
-
if recorded_tree_hash and recorded_tree_hash.removeprefix(
|
|
1104
|
+
if recorded_tree_hash and rebound_from_commit is None and recorded_tree_hash.removeprefix(
|
|
1047
1105
|
"sha256:"
|
|
1048
1106
|
) != current_tree_hash.removeprefix("sha256:"):
|
|
1049
1107
|
raise ValueError(
|
|
@@ -1088,8 +1146,13 @@ def certify_local_candidate(
|
|
|
1088
1146
|
"logHashes": sorted({str(item["logHash"]) for item in selected}),
|
|
1089
1147
|
},
|
|
1090
1148
|
}
|
|
1149
|
+
if rebound_from_commit is not None:
|
|
1150
|
+
receipt["subject"]["reboundFromCommit"] = rebound_from_commit
|
|
1151
|
+
receipt["verification"]["commitReboundWithoutRerun"] = True
|
|
1091
1152
|
write_json(
|
|
1092
|
-
change_dir
|
|
1153
|
+
resolve_archive_state_root(change_dir)
|
|
1154
|
+
/ "evidence"
|
|
1155
|
+
/ "product-candidate-verification.json",
|
|
1093
1156
|
receipt,
|
|
1094
1157
|
)
|
|
1095
1158
|
return receipt
|
|
@@ -1809,9 +1872,11 @@ def verify_manifest_byte_coverage(
|
|
|
1809
1872
|
}
|
|
1810
1873
|
|
|
1811
1874
|
def load_execution_log(change_dir: Path) -> str:
|
|
1812
|
-
for
|
|
1813
|
-
|
|
1814
|
-
|
|
1875
|
+
for root in archive_read_roots(change_dir):
|
|
1876
|
+
for rel in ("logs/execution-log.md", "execution-log.md"):
|
|
1877
|
+
path = root / rel
|
|
1878
|
+
if not path.is_file():
|
|
1879
|
+
continue
|
|
1815
1880
|
try:
|
|
1816
1881
|
return path.read_text(encoding="utf-8-sig")
|
|
1817
1882
|
except OSError:
|
|
@@ -1820,12 +1885,14 @@ def load_execution_log(change_dir: Path) -> str:
|
|
|
1820
1885
|
|
|
1821
1886
|
|
|
1822
1887
|
def load_existing_summary(change_dir: Path) -> dict[str, Any] | None:
|
|
1823
|
-
for
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1888
|
+
for root in archive_read_roots(change_dir):
|
|
1889
|
+
for rel in (
|
|
1890
|
+
"reports/final/summary-data.json",
|
|
1891
|
+
"summary-data.json",
|
|
1892
|
+
):
|
|
1893
|
+
path = root / rel
|
|
1894
|
+
if not path.is_file():
|
|
1895
|
+
continue
|
|
1829
1896
|
try:
|
|
1830
1897
|
data = read_json(path)
|
|
1831
1898
|
return data if isinstance(data, dict) else None
|
|
@@ -2072,15 +2139,36 @@ def check_status(
|
|
|
2072
2139
|
*,
|
|
2073
2140
|
allow_missing_review: bool = False,
|
|
2074
2141
|
archive_intent: str = "release-candidate",
|
|
2142
|
+
closure_disposition: str = "completed",
|
|
2143
|
+
closure_reason: str = "",
|
|
2075
2144
|
) -> dict[str, Any]:
|
|
2076
2145
|
"""Read-only archive preconditions. Never mutates."""
|
|
2077
2146
|
if archive_intent not in {"release-candidate", "record-only"}:
|
|
2078
2147
|
raise ValueError(
|
|
2079
2148
|
"archive_intent must be release-candidate or record-only"
|
|
2080
2149
|
)
|
|
2150
|
+
if closure_disposition not in {"completed", "abandoned", "superseded"}:
|
|
2151
|
+
raise ValueError(
|
|
2152
|
+
"closure_disposition must be completed, abandoned, or superseded"
|
|
2153
|
+
)
|
|
2154
|
+
closure_reason = closure_reason.strip()
|
|
2081
2155
|
blockers: list[dict[str, str]] = []
|
|
2082
2156
|
warnings: list[dict[str, str]] = []
|
|
2083
2157
|
checks: dict[str, Any] = {}
|
|
2158
|
+
if closure_disposition != "completed" and not closure_reason:
|
|
2159
|
+
blockers.append(
|
|
2160
|
+
{
|
|
2161
|
+
"code": "closure-reason-required",
|
|
2162
|
+
"message": "ending an unfinished change requires a closure reason",
|
|
2163
|
+
}
|
|
2164
|
+
)
|
|
2165
|
+
if closure_disposition != "completed" and archive_intent == "release-candidate":
|
|
2166
|
+
blockers.append(
|
|
2167
|
+
{
|
|
2168
|
+
"code": "closure-not-release-candidate",
|
|
2169
|
+
"message": "abandoned or superseded changes can be archived but cannot be release candidates",
|
|
2170
|
+
}
|
|
2171
|
+
)
|
|
2084
2172
|
|
|
2085
2173
|
if not change_dir.is_dir():
|
|
2086
2174
|
return {
|
|
@@ -2095,6 +2183,8 @@ def check_status(
|
|
|
2095
2183
|
|
|
2096
2184
|
project = find_project_root(change_dir)
|
|
2097
2185
|
checks["project_root"] = str(project)
|
|
2186
|
+
state_root = resolve_archive_state_root(change_dir)
|
|
2187
|
+
checks["state_root"] = str(state_root)
|
|
2098
2188
|
|
|
2099
2189
|
exact_byte = check_archive_exact_byte_policy(project)
|
|
2100
2190
|
checks["archive_exact_byte"] = exact_byte
|
|
@@ -2136,7 +2226,7 @@ def check_status(
|
|
|
2136
2226
|
}
|
|
2137
2227
|
)
|
|
2138
2228
|
|
|
2139
|
-
events_path =
|
|
2229
|
+
events_path = state_root / "events.ndjson"
|
|
2140
2230
|
events_present = events_path.is_file() and events_path.stat().st_size > 0
|
|
2141
2231
|
checks["events_ndjson"] = events_present
|
|
2142
2232
|
if not events_present:
|
|
@@ -2150,10 +2240,19 @@ def check_status(
|
|
|
2150
2240
|
ledger = load_ledger(change_dir)
|
|
2151
2241
|
checks["verification_ledger"] = ledger is not None
|
|
2152
2242
|
if ledger is None:
|
|
2153
|
-
blockers
|
|
2243
|
+
target = warnings if closure_disposition != "completed" else blockers
|
|
2244
|
+
target.append(
|
|
2154
2245
|
{
|
|
2155
|
-
"code":
|
|
2156
|
-
|
|
2246
|
+
"code": (
|
|
2247
|
+
"closure-ledger-not-required"
|
|
2248
|
+
if closure_disposition != "completed"
|
|
2249
|
+
else "missing-verification-ledger"
|
|
2250
|
+
),
|
|
2251
|
+
"message": (
|
|
2252
|
+
"verification was not completed; the closure records it as NOT_RUN"
|
|
2253
|
+
if closure_disposition != "completed"
|
|
2254
|
+
else "evidence/verification-ledger.json is required before archive"
|
|
2255
|
+
),
|
|
2157
2256
|
}
|
|
2158
2257
|
)
|
|
2159
2258
|
|
|
@@ -2423,6 +2522,8 @@ def check_status(
|
|
|
2423
2522
|
change_dir, write=False, for_replay=False
|
|
2424
2523
|
)
|
|
2425
2524
|
release_summary["archiveIntent"] = archive_intent
|
|
2525
|
+
release_summary["closureDisposition"] = closure_disposition
|
|
2526
|
+
release_summary["closureReason"] = closure_reason
|
|
2426
2527
|
report_adequacy = validate_report_adequacy(release_summary)
|
|
2427
2528
|
release_decision = evaluate_release_eligibility(
|
|
2428
2529
|
change_dir,
|
|
@@ -2446,12 +2547,19 @@ def check_status(
|
|
|
2446
2547
|
"ok": True,
|
|
2447
2548
|
"archivable": archivable,
|
|
2448
2549
|
"archiveIntent": archive_intent,
|
|
2550
|
+
"releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
|
|
2551
|
+
"closureDisposition": closure_disposition,
|
|
2552
|
+
"closureReason": closure_reason or None,
|
|
2449
2553
|
"archiveIntegrity": archive_integrity,
|
|
2450
2554
|
"candidateVerification": release_decision["checks"][
|
|
2451
2555
|
"candidateVerification"
|
|
2452
2556
|
],
|
|
2453
2557
|
"releaseDecision": release_decision,
|
|
2454
|
-
"releaseEligible":
|
|
2558
|
+
"releaseEligible": (
|
|
2559
|
+
release_decision["releaseEligible"]
|
|
2560
|
+
if closure_disposition == "completed"
|
|
2561
|
+
else False
|
|
2562
|
+
),
|
|
2455
2563
|
"change_dir": str(change_dir),
|
|
2456
2564
|
"change_name": infer_change_name(change_dir),
|
|
2457
2565
|
"blockers": blockers,
|
|
@@ -2482,7 +2590,10 @@ def archive_auto_gate(
|
|
|
2482
2590
|
"nextAction": "Resolve archive status blockers, then run `harness_archive.py auto-gate` again.",
|
|
2483
2591
|
}
|
|
2484
2592
|
|
|
2485
|
-
|
|
2593
|
+
state_root = resolve_archive_state_root(change_dir)
|
|
2594
|
+
snapshot_path = state_root / "meta" / "state-snapshot.json"
|
|
2595
|
+
if not snapshot_path.is_file():
|
|
2596
|
+
snapshot_path = change_dir / "meta" / "state-snapshot.json"
|
|
2486
2597
|
try:
|
|
2487
2598
|
snapshot = read_json(snapshot_path)
|
|
2488
2599
|
except (OSError, json.JSONDecodeError, TypeError):
|
|
@@ -2494,7 +2605,7 @@ def archive_auto_gate(
|
|
|
2494
2605
|
"autoArchiveAllowed": False,
|
|
2495
2606
|
"reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_MISSING",
|
|
2496
2607
|
"status": status,
|
|
2497
|
-
"nextAction": "Capture the
|
|
2608
|
+
"nextAction": "Capture the archive-boundary state snapshot before auto-archiving.",
|
|
2498
2609
|
}
|
|
2499
2610
|
|
|
2500
2611
|
git_state = snapshot.get("git")
|
|
@@ -2503,33 +2614,65 @@ def archive_auto_gate(
|
|
|
2503
2614
|
if isinstance(git_state, dict)
|
|
2504
2615
|
else snapshot.get("base") or snapshot.get("baseCommit")
|
|
2505
2616
|
)
|
|
2506
|
-
|
|
2617
|
+
content_state = snapshot.get("content")
|
|
2618
|
+
content_hash = (
|
|
2619
|
+
content_state.get("productTreeHash")
|
|
2620
|
+
if isinstance(content_state, dict)
|
|
2621
|
+
else snapshot.get("productTreeHash")
|
|
2622
|
+
)
|
|
2623
|
+
content_boundary_valid = (
|
|
2624
|
+
isinstance(content_hash, str)
|
|
2625
|
+
and re.fullmatch(r"sha256:[0-9a-f]{64}", content_hash.strip()) is not None
|
|
2626
|
+
)
|
|
2627
|
+
git_boundary_valid = isinstance(snapshot_base, str) and bool(snapshot_base.strip())
|
|
2628
|
+
if not git_boundary_valid and not content_boundary_valid:
|
|
2507
2629
|
return {
|
|
2508
2630
|
"ok": False,
|
|
2509
2631
|
"action": "auto-gate",
|
|
2510
2632
|
"autoArchiveAllowed": False,
|
|
2511
2633
|
"reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_INVALID",
|
|
2512
2634
|
"status": status,
|
|
2513
|
-
"nextAction":
|
|
2635
|
+
"nextAction": (
|
|
2636
|
+
"Capture a state snapshot containing either a Git boundary or "
|
|
2637
|
+
"a deterministic productTreeHash content boundary."
|
|
2638
|
+
),
|
|
2514
2639
|
}
|
|
2515
2640
|
|
|
2516
|
-
events_path =
|
|
2641
|
+
events_path = state_root / "events.ndjson"
|
|
2517
2642
|
events = he.load_events(events_path) if events_path.is_file() else []
|
|
2643
|
+
policy = load_gate_policy(change_dir) or {}
|
|
2644
|
+
raw_planned = policy.get("plannedPhases")
|
|
2645
|
+
if not isinstance(raw_planned, list) or not raw_planned:
|
|
2646
|
+
raw_planned = policy.get("defaultPhases")
|
|
2647
|
+
planned_phases = (
|
|
2648
|
+
[str(item).strip() for item in raw_planned if str(item).strip()]
|
|
2649
|
+
if isinstance(raw_planned, list)
|
|
2650
|
+
else []
|
|
2651
|
+
)
|
|
2652
|
+
completed_phase: str | None = None
|
|
2653
|
+
if "archive" in planned_phases:
|
|
2654
|
+
archive_index = planned_phases.index("archive")
|
|
2655
|
+
if archive_index > 0:
|
|
2656
|
+
completed_phase = planned_phases[archive_index - 1]
|
|
2657
|
+
expected_phases = {completed_phase} if completed_phase else {"submit", "merge"}
|
|
2518
2658
|
completed = any(
|
|
2519
|
-
event.get("phase") in
|
|
2659
|
+
event.get("phase") in expected_phases
|
|
2520
2660
|
and event.get("type") == "phase.end"
|
|
2521
2661
|
and str(event.get("status") or "").upper() in {"OK", "WARN"}
|
|
2522
2662
|
for event in events
|
|
2523
2663
|
if isinstance(event, dict)
|
|
2524
2664
|
)
|
|
2525
2665
|
if not completed:
|
|
2666
|
+
phase_label = completed_phase or "Submit or Merge"
|
|
2526
2667
|
return {
|
|
2527
2668
|
"ok": False,
|
|
2528
2669
|
"action": "auto-gate",
|
|
2529
2670
|
"autoArchiveAllowed": False,
|
|
2530
|
-
"reasonCode": "
|
|
2671
|
+
"reasonCode": "PLANNED_PREREQUISITE_NOT_COMPLETED",
|
|
2531
2672
|
"status": status,
|
|
2532
|
-
"
|
|
2673
|
+
"plannedPhases": planned_phases or None,
|
|
2674
|
+
"completedPrerequisitePhase": completed_phase,
|
|
2675
|
+
"nextAction": f"Complete {phase_label} and record its terminal phase event before auto-archiving.",
|
|
2533
2676
|
}
|
|
2534
2677
|
|
|
2535
2678
|
return {
|
|
@@ -2537,6 +2680,8 @@ def archive_auto_gate(
|
|
|
2537
2680
|
"action": "auto-gate",
|
|
2538
2681
|
"autoArchiveAllowed": True,
|
|
2539
2682
|
"reasonCode": "ARCHIVE_AUTO_GATE_SATISFIED",
|
|
2683
|
+
"plannedPhases": planned_phases or None,
|
|
2684
|
+
"completedPrerequisitePhase": completed_phase,
|
|
2540
2685
|
"status": status,
|
|
2541
2686
|
"snapshotPath": str(snapshot_path),
|
|
2542
2687
|
"snapshotBase": snapshot_base,
|
|
@@ -3053,6 +3198,25 @@ def build_verification_projection(
|
|
|
3053
3198
|
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
3054
3199
|
}
|
|
3055
3200
|
db_compatibility = _ledger_db_compat(effective_ledger)
|
|
3201
|
+
if db_compatibility["status"] == "NOT_RUN" and change_dir is not None:
|
|
3202
|
+
gate_policy_path = change_dir / "meta" / "gate-policy.json"
|
|
3203
|
+
try:
|
|
3204
|
+
gate_policy = read_json(gate_policy_path) if gate_policy_path.is_file() else None
|
|
3205
|
+
except (OSError, json.JSONDecodeError):
|
|
3206
|
+
gate_policy = None
|
|
3207
|
+
if isinstance(gate_policy, dict):
|
|
3208
|
+
capabilities = gate_policy.get("capabilities")
|
|
3209
|
+
required = gate_policy.get("requiredValidations")
|
|
3210
|
+
if (
|
|
3211
|
+
isinstance(capabilities, list)
|
|
3212
|
+
and "database" not in capabilities
|
|
3213
|
+
and (not isinstance(required, list) or "dbCompatibility" not in required)
|
|
3214
|
+
):
|
|
3215
|
+
db_compatibility = {
|
|
3216
|
+
"status": "NOT_APPLICABLE",
|
|
3217
|
+
"reason": "项目能力画像未声明数据库能力",
|
|
3218
|
+
"source": "capability-profile",
|
|
3219
|
+
}
|
|
3056
3220
|
projection["dbCompatibility"] = db_compatibility["status"]
|
|
3057
3221
|
projection["dbCompatibilityEvidence"] = db_compatibility
|
|
3058
3222
|
api_contract = validations.get("apiContract")
|
|
@@ -4005,6 +4169,9 @@ def collect_summary_data(
|
|
|
4005
4169
|
"knownRisks",
|
|
4006
4170
|
"manualActions",
|
|
4007
4171
|
"archiveIntent",
|
|
4172
|
+
"releaseIntent",
|
|
4173
|
+
"closureDisposition",
|
|
4174
|
+
"closureReason",
|
|
4008
4175
|
"archiveIntegrity",
|
|
4009
4176
|
"candidateVerification",
|
|
4010
4177
|
"releaseDecision",
|
|
@@ -4143,6 +4310,19 @@ def collect_summary_data(
|
|
|
4143
4310
|
data["archiveIntent"] = str(
|
|
4144
4311
|
intent_data.get("intent") or "release-candidate"
|
|
4145
4312
|
)
|
|
4313
|
+
data["releaseIntent"] = str(
|
|
4314
|
+
intent_data.get("releaseIntent")
|
|
4315
|
+
or ("candidate" if data["archiveIntent"] == "release-candidate" else "none")
|
|
4316
|
+
)
|
|
4317
|
+
data["closureDisposition"] = str(
|
|
4318
|
+
intent_data.get("closureDisposition") or "completed"
|
|
4319
|
+
)
|
|
4320
|
+
raw_closure_reason = intent_data.get("closureReason")
|
|
4321
|
+
data["closureReason"] = (
|
|
4322
|
+
str(raw_closure_reason).strip()
|
|
4323
|
+
if isinstance(raw_closure_reason, str) and raw_closure_reason.strip()
|
|
4324
|
+
else None
|
|
4325
|
+
)
|
|
4146
4326
|
data["archiveIntegrity"] = {"ok": True}
|
|
4147
4327
|
data["candidateVerification"] = candidate_gate
|
|
4148
4328
|
|
|
@@ -5178,6 +5358,9 @@ def _freeze_evidence_cutoff(work_dir: Path) -> dict[str, Any]:
|
|
|
5178
5358
|
"path": "events.ndjson",
|
|
5179
5359
|
}
|
|
5180
5360
|
write_json(work_dir / "evidence" / "evidence-cutoff.json", cutoff)
|
|
5361
|
+
# The staged archive is immutable after this point. The event lock is a
|
|
5362
|
+
# runtime coordination file, not evidence, and must not survive publish.
|
|
5363
|
+
events_file.with_name(events_file.name + ".lock").unlink(missing_ok=True)
|
|
5181
5364
|
return cutoff
|
|
5182
5365
|
|
|
5183
5366
|
|
|
@@ -7281,6 +7464,8 @@ def cmd_finalize(
|
|
|
7281
7464
|
skip_ingest: bool = False,
|
|
7282
7465
|
allow_missing_review: bool = False,
|
|
7283
7466
|
archive_intent: str = "release-candidate",
|
|
7467
|
+
closure_disposition: str = "completed",
|
|
7468
|
+
closure_reason: str = "",
|
|
7284
7469
|
) -> tuple[int, dict[str, Any]]:
|
|
7285
7470
|
"""Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
|
|
7286
7471
|
if archive_intent not in {"release-candidate", "record-only"}:
|
|
@@ -7289,6 +7474,25 @@ def cmd_finalize(
|
|
|
7289
7474
|
"action": "finalize",
|
|
7290
7475
|
"error": "archive_intent must be release-candidate or record-only",
|
|
7291
7476
|
}
|
|
7477
|
+
if closure_disposition not in {"completed", "abandoned", "superseded"}:
|
|
7478
|
+
return 1, {
|
|
7479
|
+
"ok": False,
|
|
7480
|
+
"action": "finalize",
|
|
7481
|
+
"error": "invalid closure disposition",
|
|
7482
|
+
}
|
|
7483
|
+
closure_reason = closure_reason.strip()
|
|
7484
|
+
if closure_disposition != "completed" and not closure_reason:
|
|
7485
|
+
return 1, {
|
|
7486
|
+
"ok": False,
|
|
7487
|
+
"action": "finalize",
|
|
7488
|
+
"error": "closure reason is required for abandoned or superseded changes",
|
|
7489
|
+
}
|
|
7490
|
+
if closure_disposition != "completed" and archive_intent == "release-candidate":
|
|
7491
|
+
return 1, {
|
|
7492
|
+
"ok": False,
|
|
7493
|
+
"action": "finalize",
|
|
7494
|
+
"error": "abandoned or superseded changes cannot be release candidates",
|
|
7495
|
+
}
|
|
7292
7496
|
warnings: list[str] = []
|
|
7293
7497
|
original_change_dir = change_dir.resolve()
|
|
7294
7498
|
change_name = original_change_dir.name
|
|
@@ -7375,6 +7579,9 @@ def cmd_finalize(
|
|
|
7375
7579
|
"operationTempDir": str(operation_temp_dir),
|
|
7376
7580
|
"operationRecord": str(operation_record),
|
|
7377
7581
|
"archiveIntent": archive_intent,
|
|
7582
|
+
"releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
|
|
7583
|
+
"closureDisposition": closure_disposition,
|
|
7584
|
+
"closureReason": closure_reason or None,
|
|
7378
7585
|
"releaseEligible": False,
|
|
7379
7586
|
"warnings": warnings,
|
|
7380
7587
|
"steps": {},
|
|
@@ -7766,6 +7973,11 @@ def cmd_finalize(
|
|
|
7766
7973
|
{
|
|
7767
7974
|
"schemaVersion": 1,
|
|
7768
7975
|
"intent": archive_intent,
|
|
7976
|
+
"releaseIntent": (
|
|
7977
|
+
"candidate" if archive_intent == "release-candidate" else "none"
|
|
7978
|
+
),
|
|
7979
|
+
"closureDisposition": closure_disposition,
|
|
7980
|
+
"closureReason": closure_reason or None,
|
|
7769
7981
|
"candidateVerificationCode": ci_gate.get("code"),
|
|
7770
7982
|
"releaseEligible": False,
|
|
7771
7983
|
"releaseDecisionCode": "PENDING_IDENTITY_AND_REPORT_GATES",
|
|
@@ -8377,6 +8589,27 @@ def _read_optional_text(path: Path) -> str:
|
|
|
8377
8589
|
return ""
|
|
8378
8590
|
|
|
8379
8591
|
|
|
8592
|
+
def _archive_server_url_allowed(value: str | None) -> bool:
|
|
8593
|
+
"""Require TLS remotely while allowing explicit loopback HTTP for local testing."""
|
|
8594
|
+
if value is None or not value.strip() or re.search(r"\s", value):
|
|
8595
|
+
return False
|
|
8596
|
+
try:
|
|
8597
|
+
parsed = urlsplit(value)
|
|
8598
|
+
host = (parsed.hostname or "").lower()
|
|
8599
|
+
except ValueError:
|
|
8600
|
+
return False
|
|
8601
|
+
if parsed.scheme.lower() == "https":
|
|
8602
|
+
return bool(host)
|
|
8603
|
+
if parsed.scheme.lower() != "http":
|
|
8604
|
+
return False
|
|
8605
|
+
if host == "localhost":
|
|
8606
|
+
return True
|
|
8607
|
+
try:
|
|
8608
|
+
return ipaddress.ip_address(host).is_loopback
|
|
8609
|
+
except ValueError:
|
|
8610
|
+
return False
|
|
8611
|
+
|
|
8612
|
+
|
|
8380
8613
|
def _resolve_archive_remote_credentials(
|
|
8381
8614
|
project_root: Path,
|
|
8382
8615
|
environment: Mapping[str, str],
|
|
@@ -8403,10 +8636,7 @@ def _resolve_archive_remote_credentials(
|
|
|
8403
8636
|
else ""
|
|
8404
8637
|
)
|
|
8405
8638
|
token_available = bool(env_token or local_token)
|
|
8406
|
-
url_available =
|
|
8407
|
-
server_url
|
|
8408
|
-
and re.fullmatch(r"https://[^\s]+", server_url, flags=re.IGNORECASE)
|
|
8409
|
-
)
|
|
8639
|
+
url_available = _archive_server_url_allowed(server_url)
|
|
8410
8640
|
return {
|
|
8411
8641
|
"configured": bool(url_available and token_available),
|
|
8412
8642
|
"serverUrl": server_url if url_available else None,
|
|
@@ -9180,6 +9410,8 @@ def cmd_status_cli(args: argparse.Namespace) -> int:
|
|
|
9180
9410
|
change_dir,
|
|
9181
9411
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
9182
9412
|
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
9413
|
+
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
9414
|
+
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
9183
9415
|
)
|
|
9184
9416
|
emit_json(result)
|
|
9185
9417
|
# Checks completed → exit 0; archivable flag conveys the gate result.
|
|
@@ -9250,6 +9482,8 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
9250
9482
|
skip_ingest=bool(args.skip_ingest),
|
|
9251
9483
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
9252
9484
|
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
9485
|
+
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
9486
|
+
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
9253
9487
|
)
|
|
9254
9488
|
emit_json(payload)
|
|
9255
9489
|
return code
|
|
@@ -9458,6 +9692,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
9458
9692
|
action="store_true",
|
|
9459
9693
|
help="downgrade full-tier missing review from blocker to warning",
|
|
9460
9694
|
)
|
|
9695
|
+
p_status.add_argument(
|
|
9696
|
+
"--closure",
|
|
9697
|
+
choices=("completed", "abandoned", "superseded"),
|
|
9698
|
+
default="completed",
|
|
9699
|
+
help="lifecycle outcome, independent from release intent",
|
|
9700
|
+
)
|
|
9701
|
+
p_status.add_argument("--closure-reason", default="")
|
|
9461
9702
|
p_status.add_argument(
|
|
9462
9703
|
"--intent",
|
|
9463
9704
|
choices=("release-candidate", "record-only"),
|
|
@@ -9504,6 +9745,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
9504
9745
|
"the project and is verified before source deletion"
|
|
9505
9746
|
),
|
|
9506
9747
|
)
|
|
9748
|
+
p_fin.add_argument(
|
|
9749
|
+
"--closure",
|
|
9750
|
+
choices=("completed", "abandoned", "superseded"),
|
|
9751
|
+
default="completed",
|
|
9752
|
+
help="lifecycle outcome, independent from release intent",
|
|
9753
|
+
)
|
|
9754
|
+
p_fin.add_argument("--closure-reason", default="")
|
|
9507
9755
|
p_fin.add_argument(
|
|
9508
9756
|
"--retention-policy",
|
|
9509
9757
|
default="unspecified",
|