@hunter-harness/workflow-harness 0.2.60 → 0.2.62
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 +7 -7
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +35 -35
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +5 -2
- package/harness/bundles/general/claude-code/harness-archive/reference.md +23 -7
- 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 +4 -4
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +829 -68
- package/harness/bundles/general/claude-code/scripts/harness_context.py +8 -2
- package/harness/bundles/general/claude-code/scripts/harness_events.py +5 -0
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +79 -2
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +7 -0
- package/harness/bundles/general/claude-code/scripts/harness_state.py +22 -3
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +7 -7
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +35 -35
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +5 -2
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +23 -7
- 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 +4 -4
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +829 -68
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +8 -2
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +5 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +79 -2
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +7 -0
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +22 -3
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +7 -7
- package/harness/bundles/general/codex/harness-archive/SKILL.md +35 -35
- package/harness/bundles/general/codex/harness-archive/checklist.md +5 -2
- package/harness/bundles/general/codex/harness-archive/reference.md +23 -7
- 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 +4 -4
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/general/codex/scripts/harness_archive.py +829 -68
- package/harness/bundles/general/codex/scripts/harness_context.py +8 -2
- package/harness/bundles/general/codex/scripts/harness_events.py +5 -0
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/general/codex/scripts/harness_ledger.py +79 -2
- package/harness/bundles/general/codex/scripts/harness_profile.py +7 -0
- package/harness/bundles/general/codex/scripts/harness_state.py +22 -3
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +7 -7
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +35 -35
- package/harness/bundles/general/cursor/harness-archive/checklist.md +5 -2
- package/harness/bundles/general/cursor/harness-archive/reference.md +23 -7
- 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 +4 -4
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/general/cursor/scripts/harness_archive.py +829 -68
- package/harness/bundles/general/cursor/scripts/harness_context.py +8 -2
- package/harness/bundles/general/cursor/scripts/harness_events.py +5 -0
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +79 -2
- package/harness/bundles/general/cursor/scripts/harness_profile.py +7 -0
- package/harness/bundles/general/cursor/scripts/harness_state.py +22 -3
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +7 -7
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +35 -35
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +5 -2
- package/harness/bundles/java/claude-code/harness-archive/reference.md +23 -7
- 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 +2 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +829 -68
- package/harness/bundles/java/claude-code/scripts/harness_context.py +8 -2
- package/harness/bundles/java/claude-code/scripts/harness_events.py +5 -0
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +79 -2
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +7 -0
- package/harness/bundles/java/claude-code/scripts/harness_state.py +22 -3
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +7 -7
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +35 -35
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +5 -2
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +23 -7
- 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 +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +829 -68
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +8 -2
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +5 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +79 -2
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +7 -0
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +22 -3
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +7 -7
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +35 -35
- package/harness/bundles/java/codex/harness-archive/checklist.md +5 -2
- package/harness/bundles/java/codex/harness-archive/reference.md +23 -7
- 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 +2 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/java/codex/scripts/harness_archive.py +829 -68
- package/harness/bundles/java/codex/scripts/harness_context.py +8 -2
- package/harness/bundles/java/codex/scripts/harness_events.py +5 -0
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/java/codex/scripts/harness_ledger.py +79 -2
- package/harness/bundles/java/codex/scripts/harness_profile.py +7 -0
- package/harness/bundles/java/codex/scripts/harness_state.py +22 -3
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +7 -7
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +35 -35
- package/harness/bundles/java/cursor/harness-archive/checklist.md +5 -2
- package/harness/bundles/java/cursor/harness-archive/reference.md +23 -7
- 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 +2 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +2 -2
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +7 -3
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +1 -1
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +6 -4
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +3 -2
- package/harness/bundles/java/cursor/scripts/harness_archive.py +829 -68
- package/harness/bundles/java/cursor/scripts/harness_context.py +8 -2
- package/harness/bundles/java/cursor/scripts/harness_events.py +5 -0
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +191 -26
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +79 -2
- package/harness/bundles/java/cursor/scripts/harness_profile.py +7 -0
- package/harness/bundles/java/cursor/scripts/harness_state.py +22 -3
- package/harness/manifests/general/claude-code.json +27 -27
- package/harness/manifests/general/codebuddy.json +27 -27
- package/harness/manifests/general/codex.json +27 -27
- package/harness/manifests/general/cursor.json +27 -27
- package/harness/manifests/java/claude-code.json +29 -29
- package/harness/manifests/java/codebuddy.json +29 -29
- package/harness/manifests/java/codex.json +29 -29
- package/harness/manifests/java/cursor.json +29 -29
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ import subprocess
|
|
|
27
27
|
import sys
|
|
28
28
|
import tarfile
|
|
29
29
|
import tempfile
|
|
30
|
+
import time
|
|
30
31
|
import uuid
|
|
31
32
|
import zipfile
|
|
32
33
|
from collections.abc import Mapping
|
|
@@ -1907,8 +1908,13 @@ def find_test_reports(change_dir: Path) -> list[Path]:
|
|
|
1907
1908
|
"reports/test/*.md",
|
|
1908
1909
|
]
|
|
1909
1910
|
found: list[Path] = []
|
|
1911
|
+
seen: set[Path] = set()
|
|
1910
1912
|
for pattern in patterns:
|
|
1911
|
-
|
|
1913
|
+
for path in sorted(change_dir.glob(pattern)):
|
|
1914
|
+
if path in seen:
|
|
1915
|
+
continue
|
|
1916
|
+
seen.add(path)
|
|
1917
|
+
found.append(path)
|
|
1912
1918
|
return found
|
|
1913
1919
|
|
|
1914
1920
|
|
|
@@ -2184,6 +2190,21 @@ def check_status(
|
|
|
2184
2190
|
checks["project_root"] = str(project)
|
|
2185
2191
|
state_root = resolve_archive_state_root(change_dir)
|
|
2186
2192
|
checks["state_root"] = str(state_root)
|
|
2193
|
+
range_adoption = validate_existing_range_adoption(change_dir)
|
|
2194
|
+
checks["range_adoption"] = range_adoption
|
|
2195
|
+
if not range_adoption.get("ok"):
|
|
2196
|
+
blockers.append(
|
|
2197
|
+
{
|
|
2198
|
+
"code": str(
|
|
2199
|
+
range_adoption.get("reasonCode")
|
|
2200
|
+
or "EXISTING_RANGE_RECEIPT_INVALID"
|
|
2201
|
+
),
|
|
2202
|
+
"message": str(
|
|
2203
|
+
range_adoption.get("message")
|
|
2204
|
+
or "既有提交范围认领收据无效。"
|
|
2205
|
+
),
|
|
2206
|
+
}
|
|
2207
|
+
)
|
|
2187
2208
|
|
|
2188
2209
|
exact_byte = check_archive_exact_byte_policy(project)
|
|
2189
2210
|
checks["archive_exact_byte"] = exact_byte
|
|
@@ -2202,14 +2223,33 @@ def check_status(
|
|
|
2202
2223
|
)
|
|
2203
2224
|
checks["sensitive_evidence_publication"] = sensitive_gate
|
|
2204
2225
|
if not sensitive_gate.get("ok"):
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
"
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2226
|
+
sensitive_issue_codes = {
|
|
2227
|
+
str(item.get("code") or "")
|
|
2228
|
+
for item in sensitive_gate.get("issues") or []
|
|
2229
|
+
if isinstance(item, dict)
|
|
2230
|
+
}
|
|
2231
|
+
digest_only_drift = sensitive_issue_codes == {
|
|
2232
|
+
"SECRET_SCAN_TREE_DIGEST_MISMATCH"
|
|
2233
|
+
}
|
|
2234
|
+
if digest_only_drift and not sensitive_gate.get("unresolvedFailures"):
|
|
2235
|
+
warnings.append(
|
|
2236
|
+
{
|
|
2237
|
+
"code": "SECRET_SCAN_RECEIPT_REFRESH_REQUIRED",
|
|
2238
|
+
"message": (
|
|
2239
|
+
"归档事件或锁文件变化使敏感信息扫描收据过期;"
|
|
2240
|
+
"正式归档会重新扫描并绑定当前字节。"
|
|
2241
|
+
),
|
|
2242
|
+
}
|
|
2243
|
+
)
|
|
2244
|
+
else:
|
|
2245
|
+
blockers.append({
|
|
2246
|
+
"code": str(sensitive_gate.get("reasonCode") or "SECRET_SCAN_GATE_BLOCKED"),
|
|
2247
|
+
"message": str(
|
|
2248
|
+
sensitive_gate.get("nextAction")
|
|
2249
|
+
or sensitive_gate.get("error")
|
|
2250
|
+
or "sensitive evidence publication gate failed"
|
|
2251
|
+
),
|
|
2252
|
+
})
|
|
2213
2253
|
|
|
2214
2254
|
# --- H-4 formal-layer minimum set ---
|
|
2215
2255
|
plans_dir = change_dir / "plans"
|
|
@@ -2238,6 +2278,28 @@ def check_status(
|
|
|
2238
2278
|
|
|
2239
2279
|
ledger = load_ledger(change_dir)
|
|
2240
2280
|
checks["verification_ledger"] = ledger is not None
|
|
2281
|
+
immutable_base = _immutable_change_base_from_snapshot(change_dir)
|
|
2282
|
+
ledger_base = (
|
|
2283
|
+
str(ledger.get("baseCommit") or "").strip()
|
|
2284
|
+
if isinstance(ledger, dict)
|
|
2285
|
+
else ""
|
|
2286
|
+
)
|
|
2287
|
+
checks["change_base"] = immutable_base or None
|
|
2288
|
+
if (
|
|
2289
|
+
immutable_base
|
|
2290
|
+
and ledger_base
|
|
2291
|
+
and immutable_base != ledger_base
|
|
2292
|
+
and not range_adoption.get("present")
|
|
2293
|
+
):
|
|
2294
|
+
blockers.append(
|
|
2295
|
+
{
|
|
2296
|
+
"code": "CHANGE_BASE_IDENTITY_MISMATCH",
|
|
2297
|
+
"message": (
|
|
2298
|
+
"计划阶段保存的变更基线与验证账本不一致;"
|
|
2299
|
+
"请先恢复正确身份,不得在归档阶段自动改写。"
|
|
2300
|
+
),
|
|
2301
|
+
}
|
|
2302
|
+
)
|
|
2241
2303
|
if ledger is None:
|
|
2242
2304
|
target = warnings if closure_disposition != "completed" else blockers
|
|
2243
2305
|
target.append(
|
|
@@ -2296,7 +2358,10 @@ def check_status(
|
|
|
2296
2358
|
hash_source: str | None = None
|
|
2297
2359
|
if ledger is None:
|
|
2298
2360
|
ledger = load_ledger(change_dir)
|
|
2299
|
-
if
|
|
2361
|
+
if range_adoption.get("ok") and range_adoption.get("present"):
|
|
2362
|
+
expected_hash = str(range_adoption.get("tipCommit") or "").strip() or None
|
|
2363
|
+
hash_source = "meta/archive-range-adoption.json"
|
|
2364
|
+
if expected_hash is None and worktree_requested(change_dir) and ledger:
|
|
2300
2365
|
merge_hash = ledger.get("mergeFinalHash") or (
|
|
2301
2366
|
ledger.get("merge") or {}
|
|
2302
2367
|
).get("finalHash")
|
|
@@ -2492,7 +2557,6 @@ def check_status(
|
|
|
2492
2557
|
),
|
|
2493
2558
|
})
|
|
2494
2559
|
|
|
2495
|
-
archivable = len(blockers) == 0
|
|
2496
2560
|
candidate_codes = {
|
|
2497
2561
|
"PRODUCT_CI_NOT_GREEN",
|
|
2498
2562
|
"PRODUCT_CANDIDATE_NOT_VERIFIED",
|
|
@@ -2500,6 +2564,71 @@ def check_status(
|
|
|
2500
2564
|
"PROJECT_RELEASE_POLICY_BLOCKED",
|
|
2501
2565
|
"REMOTE_CI_DOWNGRADE_REFUSED",
|
|
2502
2566
|
}
|
|
2567
|
+
report_adequacy: dict[str, Any]
|
|
2568
|
+
release_summary: dict[str, Any] | None = None
|
|
2569
|
+
try:
|
|
2570
|
+
release_summary = collect_summary_data(
|
|
2571
|
+
change_dir, write=False, for_replay=False
|
|
2572
|
+
)
|
|
2573
|
+
release_summary["archiveIntent"] = archive_intent
|
|
2574
|
+
release_summary["closureDisposition"] = closure_disposition
|
|
2575
|
+
release_summary["closureReason"] = closure_reason
|
|
2576
|
+
report_adequacy = validate_report_adequacy(release_summary)
|
|
2577
|
+
except Exception as exc: # noqa: BLE001 — status must remain read-only
|
|
2578
|
+
report_adequacy = {
|
|
2579
|
+
"ok": False,
|
|
2580
|
+
"code": "REPORT_ADEQUACY_UNAVAILABLE",
|
|
2581
|
+
"message": str(exc),
|
|
2582
|
+
"issues": [
|
|
2583
|
+
{
|
|
2584
|
+
"code": "REPORT_ADEQUACY_UNAVAILABLE",
|
|
2585
|
+
"severity": "error",
|
|
2586
|
+
"message": str(exc),
|
|
2587
|
+
}
|
|
2588
|
+
],
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
adequacy_messages_zh = {
|
|
2592
|
+
"ARCHIVE_BASE_EQUALS_FEATURE_TIP": (
|
|
2593
|
+
"变更基线与产品提交相同,本次没有可归档的产品增量。"
|
|
2594
|
+
"如需封存已有提交,请先使用受控的既有范围认领;"
|
|
2595
|
+
"如变更未完成,请选择废弃或被替代并填写中文原因。"
|
|
2596
|
+
),
|
|
2597
|
+
"IDENTITY_BASE_MISSING": "缺少不可变的变更基线,无法证明归档范围。",
|
|
2598
|
+
"DIFF_ZERO_WITH_NONEMPTY_COMMIT": "提交范围非空,但报告中的文件变更数为 0。",
|
|
2599
|
+
"ARCHIVE_DIFF_SHRUNK_VS_OWNERSHIP": "归档差异小于声明的产品范围。",
|
|
2600
|
+
"ARCHIVE_NOFF_MERGE_DELTA_ONLY": "归档只覆盖了合并提交差异,未覆盖完整变更。",
|
|
2601
|
+
"REPORT_ADEQUACY_UNAVAILABLE": "无法生成归档充分性报告。",
|
|
2602
|
+
}
|
|
2603
|
+
for issue in report_adequacy.get("issues") or []:
|
|
2604
|
+
if not isinstance(issue, dict) or issue.get("severity") != "error":
|
|
2605
|
+
continue
|
|
2606
|
+
code = str(issue.get("code") or "REPORT_ADEQUACY_FAILED")
|
|
2607
|
+
if any(item.get("code") == code for item in blockers):
|
|
2608
|
+
continue
|
|
2609
|
+
blockers.append(
|
|
2610
|
+
{
|
|
2611
|
+
"code": code,
|
|
2612
|
+
"message": adequacy_messages_zh.get(
|
|
2613
|
+
code,
|
|
2614
|
+
str(issue.get("message") or "归档报告不完整"),
|
|
2615
|
+
),
|
|
2616
|
+
}
|
|
2617
|
+
)
|
|
2618
|
+
for issue in report_adequacy.get("issues") or []:
|
|
2619
|
+
if not isinstance(issue, dict) or issue.get("severity") != "warning":
|
|
2620
|
+
continue
|
|
2621
|
+
code = str(issue.get("code") or "REPORT_ADEQUACY_WARNING")
|
|
2622
|
+
if any(item.get("code") == code for item in warnings):
|
|
2623
|
+
continue
|
|
2624
|
+
warnings.append(
|
|
2625
|
+
{
|
|
2626
|
+
"code": code,
|
|
2627
|
+
"message": str(issue.get("message") or "归档报告需要注意"),
|
|
2628
|
+
}
|
|
2629
|
+
)
|
|
2630
|
+
|
|
2631
|
+
archivable = len(blockers) == 0
|
|
2503
2632
|
archive_integrity_ok = not any(
|
|
2504
2633
|
item.get("code") not in candidate_codes for item in blockers
|
|
2505
2634
|
)
|
|
@@ -2511,34 +2640,23 @@ def check_status(
|
|
|
2511
2640
|
else "ARCHIVE_INTEGRITY_FAILED"
|
|
2512
2641
|
),
|
|
2513
2642
|
"message": (
|
|
2514
|
-
"
|
|
2643
|
+
"归档前置条件完整"
|
|
2515
2644
|
if archive_integrity_ok
|
|
2516
|
-
else "
|
|
2645
|
+
else "归档前置条件存在阻断项"
|
|
2517
2646
|
),
|
|
2518
2647
|
}
|
|
2519
|
-
|
|
2520
|
-
release_summary = collect_summary_data(
|
|
2521
|
-
change_dir, write=False, for_replay=False
|
|
2522
|
-
)
|
|
2523
|
-
release_summary["archiveIntent"] = archive_intent
|
|
2524
|
-
release_summary["closureDisposition"] = closure_disposition
|
|
2525
|
-
release_summary["closureReason"] = closure_reason
|
|
2526
|
-
report_adequacy = validate_report_adequacy(release_summary)
|
|
2648
|
+
if release_summary is not None:
|
|
2527
2649
|
release_decision = evaluate_release_eligibility(
|
|
2528
2650
|
change_dir,
|
|
2529
2651
|
release_summary,
|
|
2530
2652
|
archive_integrity=archive_integrity,
|
|
2531
2653
|
report_adequacy=report_adequacy,
|
|
2532
2654
|
)
|
|
2533
|
-
|
|
2655
|
+
else:
|
|
2534
2656
|
release_decision = compose_release_decision(
|
|
2535
2657
|
{
|
|
2536
2658
|
"archiveIntegrity": archive_integrity,
|
|
2537
|
-
"reportAdequacy":
|
|
2538
|
-
"ok": False,
|
|
2539
|
-
"code": "REPORT_ADEQUACY_UNAVAILABLE",
|
|
2540
|
-
"message": str(exc),
|
|
2541
|
-
},
|
|
2659
|
+
"reportAdequacy": report_adequacy,
|
|
2542
2660
|
"candidateVerification": ci_gate,
|
|
2543
2661
|
}
|
|
2544
2662
|
)
|
|
@@ -2572,21 +2690,37 @@ def archive_auto_gate(
|
|
|
2572
2690
|
*,
|
|
2573
2691
|
allow_missing_review: bool = False,
|
|
2574
2692
|
archive_intent: str = "release-candidate",
|
|
2693
|
+
closure_disposition: str = "completed",
|
|
2694
|
+
closure_reason: str = "",
|
|
2575
2695
|
) -> dict[str, Any]:
|
|
2576
2696
|
"""Read-only proof that a post-submit/merge archive may run unattended."""
|
|
2577
2697
|
status = check_status(
|
|
2578
2698
|
change_dir,
|
|
2579
2699
|
allow_missing_review=allow_missing_review,
|
|
2580
2700
|
archive_intent=archive_intent,
|
|
2701
|
+
closure_disposition=closure_disposition,
|
|
2702
|
+
closure_reason=closure_reason,
|
|
2581
2703
|
)
|
|
2582
2704
|
if not status.get("archivable"):
|
|
2705
|
+
report_adequacy = (
|
|
2706
|
+
(status.get("releaseDecision") or {}).get("checks") or {}
|
|
2707
|
+
).get("reportAdequacy") or {}
|
|
2708
|
+
adequacy_failed = report_adequacy.get("ok") is False
|
|
2583
2709
|
return {
|
|
2584
2710
|
"ok": False,
|
|
2585
2711
|
"action": "auto-gate",
|
|
2586
2712
|
"autoArchiveAllowed": False,
|
|
2587
|
-
"reasonCode":
|
|
2713
|
+
"reasonCode": (
|
|
2714
|
+
"ARCHIVE_REPORT_ADEQUACY_FAILED"
|
|
2715
|
+
if adequacy_failed
|
|
2716
|
+
else "ARCHIVE_PRECONDITIONS_UNSATISFIED"
|
|
2717
|
+
),
|
|
2588
2718
|
"status": status,
|
|
2589
|
-
"nextAction":
|
|
2719
|
+
"nextAction": (
|
|
2720
|
+
"归档报告不完整,请按 blockers 中的中文建议处理后重试。"
|
|
2721
|
+
if adequacy_failed
|
|
2722
|
+
else "请先处理归档阻断项,再重新执行归档。"
|
|
2723
|
+
),
|
|
2590
2724
|
}
|
|
2591
2725
|
|
|
2592
2726
|
state_root = resolve_archive_state_root(change_dir)
|
|
@@ -2608,7 +2742,7 @@ def archive_auto_gate(
|
|
|
2608
2742
|
}
|
|
2609
2743
|
|
|
2610
2744
|
git_state = snapshot.get("git")
|
|
2611
|
-
snapshot_base = (
|
|
2745
|
+
snapshot_base = snapshot.get("changeBase") or (
|
|
2612
2746
|
(git_state.get("base") or git_state.get("baseCommit"))
|
|
2613
2747
|
if isinstance(git_state, dict)
|
|
2614
2748
|
else snapshot.get("base") or snapshot.get("baseCommit")
|
|
@@ -2684,10 +2818,131 @@ def archive_auto_gate(
|
|
|
2684
2818
|
"status": status,
|
|
2685
2819
|
"snapshotPath": str(snapshot_path),
|
|
2686
2820
|
"snapshotBase": snapshot_base,
|
|
2687
|
-
"nextAction": "
|
|
2821
|
+
"nextAction": "运行 `harness_archive.py execute`;它会复用本次状态结果,不再重复扫描。",
|
|
2688
2822
|
}
|
|
2689
2823
|
|
|
2690
2824
|
|
|
2825
|
+
def execute_archive(
|
|
2826
|
+
change_dir: Path,
|
|
2827
|
+
archive_root: Path,
|
|
2828
|
+
*,
|
|
2829
|
+
durable_root: Path | None = None,
|
|
2830
|
+
retention_policy: str = "unspecified",
|
|
2831
|
+
skip_ingest: bool = False,
|
|
2832
|
+
allow_missing_review: bool = False,
|
|
2833
|
+
archive_intent: str = "release-candidate",
|
|
2834
|
+
closure_disposition: str = "completed",
|
|
2835
|
+
closure_reason: str = "",
|
|
2836
|
+
) -> tuple[int, dict[str, Any]]:
|
|
2837
|
+
"""Run one status collection, boundary gate, and finalize operation."""
|
|
2838
|
+
change_dir = change_dir.resolve()
|
|
2839
|
+
started = time.perf_counter()
|
|
2840
|
+
try:
|
|
2841
|
+
state_root = resolve_archive_state_root(change_dir)
|
|
2842
|
+
except (OSError, ValueError):
|
|
2843
|
+
state_root = change_dir
|
|
2844
|
+
if state_root.is_dir():
|
|
2845
|
+
try:
|
|
2846
|
+
append_event(
|
|
2847
|
+
state_root,
|
|
2848
|
+
phase="archive",
|
|
2849
|
+
type_="phase.prepare.start",
|
|
2850
|
+
note="开始一次性检查归档条件",
|
|
2851
|
+
)
|
|
2852
|
+
except OSError:
|
|
2853
|
+
pass
|
|
2854
|
+
candidate_certification: dict[str, Any] | None = None
|
|
2855
|
+
if load_product_candidate_ci(change_dir) is None:
|
|
2856
|
+
try:
|
|
2857
|
+
receipt = certify_local_candidate(
|
|
2858
|
+
change_dir,
|
|
2859
|
+
project=find_project_root(change_dir),
|
|
2860
|
+
)
|
|
2861
|
+
subject = (
|
|
2862
|
+
receipt.get("subject")
|
|
2863
|
+
if isinstance(receipt.get("subject"), dict)
|
|
2864
|
+
else {}
|
|
2865
|
+
)
|
|
2866
|
+
candidate_certification = {
|
|
2867
|
+
"ok": True,
|
|
2868
|
+
"code": "LOCAL_CANDIDATE_CERTIFIED",
|
|
2869
|
+
"assurance": receipt.get("assurance"),
|
|
2870
|
+
"subject": subject,
|
|
2871
|
+
"reusedValidations": (
|
|
2872
|
+
receipt.get("verification", {}).get("reusedValidations", [])
|
|
2873
|
+
if isinstance(receipt.get("verification"), dict)
|
|
2874
|
+
else []
|
|
2875
|
+
),
|
|
2876
|
+
}
|
|
2877
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
2878
|
+
candidate_certification = {
|
|
2879
|
+
"ok": False,
|
|
2880
|
+
"code": "LOCAL_CANDIDATE_CERTIFICATION_UNAVAILABLE",
|
|
2881
|
+
"message": str(exc),
|
|
2882
|
+
}
|
|
2883
|
+
gate = archive_auto_gate(
|
|
2884
|
+
change_dir,
|
|
2885
|
+
allow_missing_review=allow_missing_review,
|
|
2886
|
+
archive_intent=archive_intent,
|
|
2887
|
+
closure_disposition=closure_disposition,
|
|
2888
|
+
closure_reason=closure_reason,
|
|
2889
|
+
)
|
|
2890
|
+
elapsed_ms = max(0, int(round((time.perf_counter() - started) * 1000)))
|
|
2891
|
+
if state_root.is_dir():
|
|
2892
|
+
try:
|
|
2893
|
+
append_event(
|
|
2894
|
+
state_root,
|
|
2895
|
+
phase="archive",
|
|
2896
|
+
type_="phase.prepare.end",
|
|
2897
|
+
status="OK" if gate.get("ok") else "BLOCKED",
|
|
2898
|
+
code=str(gate.get("reasonCode") or "ARCHIVE_PREPARE_FINISHED"),
|
|
2899
|
+
message=(
|
|
2900
|
+
"归档条件检查通过"
|
|
2901
|
+
if gate.get("ok")
|
|
2902
|
+
else str(gate.get("nextAction") or "归档条件未满足")
|
|
2903
|
+
),
|
|
2904
|
+
duration_ms=elapsed_ms,
|
|
2905
|
+
note="一次性归档预检完成",
|
|
2906
|
+
)
|
|
2907
|
+
except OSError:
|
|
2908
|
+
pass
|
|
2909
|
+
if not gate.get("ok"):
|
|
2910
|
+
status = gate.get("status") if isinstance(gate.get("status"), dict) else {}
|
|
2911
|
+
blocked_payload = {
|
|
2912
|
+
"ok": False,
|
|
2913
|
+
"action": "execute",
|
|
2914
|
+
"reasonCode": gate.get("reasonCode"),
|
|
2915
|
+
"error": gate.get("nextAction"),
|
|
2916
|
+
"issues": list(status.get("blockers") or []),
|
|
2917
|
+
"preflight": gate,
|
|
2918
|
+
"change_dir": str(change_dir),
|
|
2919
|
+
"archive_dir": str(archive_root.resolve() / f"{today_date()}-{change_dir.name}"),
|
|
2920
|
+
"original_preserved": change_dir.is_dir(),
|
|
2921
|
+
"finalStatus": "BLOCKED",
|
|
2922
|
+
"preparationDurationMs": elapsed_ms,
|
|
2923
|
+
}
|
|
2924
|
+
if candidate_certification is not None:
|
|
2925
|
+
blocked_payload["candidateCertification"] = candidate_certification
|
|
2926
|
+
return 1, blocked_payload
|
|
2927
|
+
code, payload = cmd_finalize(
|
|
2928
|
+
change_dir,
|
|
2929
|
+
archive_root,
|
|
2930
|
+
durable_root=durable_root,
|
|
2931
|
+
retention_policy=retention_policy,
|
|
2932
|
+
skip_ingest=skip_ingest,
|
|
2933
|
+
allow_missing_review=allow_missing_review,
|
|
2934
|
+
archive_intent=archive_intent,
|
|
2935
|
+
closure_disposition=closure_disposition,
|
|
2936
|
+
closure_reason=closure_reason,
|
|
2937
|
+
preflight_status=gate["status"],
|
|
2938
|
+
)
|
|
2939
|
+
payload["preflight"] = gate
|
|
2940
|
+
payload["preparationDurationMs"] = elapsed_ms
|
|
2941
|
+
if candidate_certification is not None:
|
|
2942
|
+
payload["candidateCertification"] = candidate_certification
|
|
2943
|
+
return code, payload
|
|
2944
|
+
|
|
2945
|
+
|
|
2691
2946
|
# ---------------------------------------------------------------------------
|
|
2692
2947
|
# collect
|
|
2693
2948
|
# ---------------------------------------------------------------------------
|
|
@@ -3209,15 +3464,22 @@ def build_verification_projection(
|
|
|
3209
3464
|
gate_policy = None
|
|
3210
3465
|
if isinstance(gate_policy, dict):
|
|
3211
3466
|
capabilities = gate_policy.get("capabilities")
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3467
|
+
required_names: set[str] = set()
|
|
3468
|
+
for field in ("requiredValidations", "requiredValidationsByPhase"):
|
|
3469
|
+
required = gate_policy.get(field)
|
|
3470
|
+
if isinstance(required, list):
|
|
3471
|
+
required_names.update(str(item) for item in required)
|
|
3472
|
+
elif isinstance(required, dict):
|
|
3473
|
+
for phase_items in required.values():
|
|
3474
|
+
if isinstance(phase_items, list):
|
|
3475
|
+
required_names.update(str(item) for item in phase_items)
|
|
3476
|
+
has_database_capability = (
|
|
3477
|
+
isinstance(capabilities, list) and "database" in capabilities
|
|
3478
|
+
)
|
|
3479
|
+
if not has_database_capability and "dbCompatibility" not in required_names:
|
|
3218
3480
|
db_compatibility = {
|
|
3219
3481
|
"status": "NOT_APPLICABLE",
|
|
3220
|
-
"reason": "
|
|
3482
|
+
"reason": "门禁策略未声明数据库能力或数据库兼容性验证要求",
|
|
3221
3483
|
"source": "capability-profile",
|
|
3222
3484
|
}
|
|
3223
3485
|
projection["dbCompatibility"] = db_compatibility["status"]
|
|
@@ -3896,6 +4158,303 @@ def _review_summary(
|
|
|
3896
4158
|
return base
|
|
3897
4159
|
|
|
3898
4160
|
|
|
4161
|
+
def _archive_range_adoption_path(change_dir: Path) -> Path:
|
|
4162
|
+
return change_dir.resolve() / "meta" / "archive-range-adoption.json"
|
|
4163
|
+
|
|
4164
|
+
|
|
4165
|
+
def _archive_range_receipt_id(payload: dict[str, Any]) -> str:
|
|
4166
|
+
canonical = {
|
|
4167
|
+
key: value for key, value in payload.items() if key != "receiptId"
|
|
4168
|
+
}
|
|
4169
|
+
raw = json.dumps(
|
|
4170
|
+
canonical,
|
|
4171
|
+
ensure_ascii=False,
|
|
4172
|
+
sort_keys=True,
|
|
4173
|
+
separators=(",", ":"),
|
|
4174
|
+
).encode("utf-8")
|
|
4175
|
+
return "sha256:" + hashlib.sha256(raw).hexdigest()
|
|
4176
|
+
|
|
4177
|
+
|
|
4178
|
+
def _evaluate_existing_range(
|
|
4179
|
+
change_dir: Path,
|
|
4180
|
+
*,
|
|
4181
|
+
base: str,
|
|
4182
|
+
tip: str,
|
|
4183
|
+
) -> dict[str, Any]:
|
|
4184
|
+
project = find_project_root(change_dir)
|
|
4185
|
+
base_code, resolved_base, base_error = git_run(
|
|
4186
|
+
project, "rev-parse", "--verify", f"{base}^{{commit}}"
|
|
4187
|
+
)
|
|
4188
|
+
tip_code, resolved_tip, tip_error = git_run(
|
|
4189
|
+
project, "rev-parse", "--verify", f"{tip}^{{commit}}"
|
|
4190
|
+
)
|
|
4191
|
+
if base_code != 0 or not resolved_base:
|
|
4192
|
+
return {
|
|
4193
|
+
"ok": False,
|
|
4194
|
+
"reasonCode": "EXISTING_RANGE_BASE_INVALID",
|
|
4195
|
+
"message": f"无法解析既有范围基线:{base_error or base}",
|
|
4196
|
+
}
|
|
4197
|
+
if tip_code != 0 or not resolved_tip:
|
|
4198
|
+
return {
|
|
4199
|
+
"ok": False,
|
|
4200
|
+
"reasonCode": "EXISTING_RANGE_TIP_INVALID",
|
|
4201
|
+
"message": f"无法解析既有范围终点:{tip_error or tip}",
|
|
4202
|
+
}
|
|
4203
|
+
if resolved_base == resolved_tip:
|
|
4204
|
+
return {
|
|
4205
|
+
"ok": False,
|
|
4206
|
+
"reasonCode": "EXISTING_RANGE_EMPTY",
|
|
4207
|
+
"message": "既有提交范围为空,不能认领为已完成产品变更。",
|
|
4208
|
+
}
|
|
4209
|
+
ancestor_code, _, ancestor_error = git_run(
|
|
4210
|
+
project, "merge-base", "--is-ancestor", resolved_base, resolved_tip
|
|
4211
|
+
)
|
|
4212
|
+
if ancestor_code != 0:
|
|
4213
|
+
return {
|
|
4214
|
+
"ok": False,
|
|
4215
|
+
"reasonCode": "EXISTING_RANGE_NOT_ANCESTOR",
|
|
4216
|
+
"message": f"范围基线不是终点的祖先:{ancestor_error or resolved_base}",
|
|
4217
|
+
}
|
|
4218
|
+
identity = resolve_product_archive_identity(change_dir, project=project)
|
|
4219
|
+
expected_tip = str(
|
|
4220
|
+
identity.get("featureTip") or identity.get("productCommit") or ""
|
|
4221
|
+
).strip()
|
|
4222
|
+
if expected_tip:
|
|
4223
|
+
expected_code, resolved_expected, _ = git_run(
|
|
4224
|
+
project, "rev-parse", "--verify", f"{expected_tip}^{{commit}}"
|
|
4225
|
+
)
|
|
4226
|
+
if expected_code == 0 and resolved_expected != resolved_tip:
|
|
4227
|
+
return {
|
|
4228
|
+
"ok": False,
|
|
4229
|
+
"reasonCode": "EXISTING_RANGE_TIP_MISMATCH",
|
|
4230
|
+
"message": (
|
|
4231
|
+
"既有范围终点与当前产品提交不一致:"
|
|
4232
|
+
f"expected={resolved_expected} actual={resolved_tip}"
|
|
4233
|
+
),
|
|
4234
|
+
}
|
|
4235
|
+
try:
|
|
4236
|
+
ownership = hl.compute_ownership_diff(
|
|
4237
|
+
project,
|
|
4238
|
+
base=resolved_base,
|
|
4239
|
+
head=resolved_tip,
|
|
4240
|
+
change_dir=change_dir,
|
|
4241
|
+
)
|
|
4242
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
4243
|
+
return {
|
|
4244
|
+
"ok": False,
|
|
4245
|
+
"reasonCode": "EXISTING_RANGE_OWNERSHIP_UNAVAILABLE",
|
|
4246
|
+
"message": f"无法验证既有范围归属:{exc}",
|
|
4247
|
+
}
|
|
4248
|
+
foreign = list(ownership.get("foreignPaths") or [])
|
|
4249
|
+
owned = list(ownership.get("files") or [])
|
|
4250
|
+
if foreign:
|
|
4251
|
+
return {
|
|
4252
|
+
"ok": False,
|
|
4253
|
+
"reasonCode": "EXISTING_RANGE_FOREIGN_PATHS",
|
|
4254
|
+
"message": "既有范围包含不属于本变更的文件。",
|
|
4255
|
+
"foreignPaths": foreign,
|
|
4256
|
+
}
|
|
4257
|
+
if not owned:
|
|
4258
|
+
return {
|
|
4259
|
+
"ok": False,
|
|
4260
|
+
"reasonCode": "EXISTING_RANGE_NO_PRODUCT_FILES",
|
|
4261
|
+
"message": "既有范围未包含本变更声明的产品文件。",
|
|
4262
|
+
}
|
|
4263
|
+
return {
|
|
4264
|
+
"ok": True,
|
|
4265
|
+
"reasonCode": "EXISTING_RANGE_VALID",
|
|
4266
|
+
"project": project,
|
|
4267
|
+
"baseCommit": resolved_base,
|
|
4268
|
+
"tipCommit": resolved_tip,
|
|
4269
|
+
"ownership": ownership,
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
|
|
4273
|
+
def validate_existing_range_adoption(change_dir: Path) -> dict[str, Any]:
|
|
4274
|
+
path = _archive_range_adoption_path(change_dir)
|
|
4275
|
+
if not path.is_file():
|
|
4276
|
+
return {
|
|
4277
|
+
"ok": True,
|
|
4278
|
+
"present": False,
|
|
4279
|
+
"reasonCode": "EXISTING_RANGE_NOT_ADOPTED",
|
|
4280
|
+
"receiptPath": str(path),
|
|
4281
|
+
}
|
|
4282
|
+
try:
|
|
4283
|
+
receipt = read_json(path)
|
|
4284
|
+
except (OSError, json.JSONDecodeError, TypeError) as exc:
|
|
4285
|
+
return {
|
|
4286
|
+
"ok": False,
|
|
4287
|
+
"present": True,
|
|
4288
|
+
"reasonCode": "EXISTING_RANGE_RECEIPT_INVALID",
|
|
4289
|
+
"message": str(exc),
|
|
4290
|
+
"receiptPath": str(path),
|
|
4291
|
+
}
|
|
4292
|
+
if not isinstance(receipt, dict):
|
|
4293
|
+
return {
|
|
4294
|
+
"ok": False,
|
|
4295
|
+
"present": True,
|
|
4296
|
+
"reasonCode": "EXISTING_RANGE_RECEIPT_INVALID",
|
|
4297
|
+
"message": "既有范围认领收据必须是 JSON 对象。",
|
|
4298
|
+
"receiptPath": str(path),
|
|
4299
|
+
}
|
|
4300
|
+
issues: list[str] = []
|
|
4301
|
+
if receipt.get("schemaVersion") != 1:
|
|
4302
|
+
issues.append("schemaVersion")
|
|
4303
|
+
if receipt.get("action") != "adopt-existing-range":
|
|
4304
|
+
issues.append("action")
|
|
4305
|
+
if receipt.get("changeName") != change_dir.name:
|
|
4306
|
+
issues.append("changeName")
|
|
4307
|
+
if receipt.get("confirmedByUser") is not True:
|
|
4308
|
+
issues.append("confirmedByUser")
|
|
4309
|
+
reason = str(receipt.get("reason") or "").strip()
|
|
4310
|
+
if not reason or re.search(r"[\u4e00-\u9fff]", reason) is None:
|
|
4311
|
+
issues.append("reason")
|
|
4312
|
+
if receipt.get("receiptId") != _archive_range_receipt_id(receipt):
|
|
4313
|
+
issues.append("receiptId")
|
|
4314
|
+
if issues:
|
|
4315
|
+
return {
|
|
4316
|
+
"ok": False,
|
|
4317
|
+
"present": True,
|
|
4318
|
+
"reasonCode": "EXISTING_RANGE_RECEIPT_INVALID",
|
|
4319
|
+
"message": "既有范围认领收据字段无效:" + ", ".join(issues),
|
|
4320
|
+
"receiptPath": str(path),
|
|
4321
|
+
}
|
|
4322
|
+
evaluated = _evaluate_existing_range(
|
|
4323
|
+
change_dir,
|
|
4324
|
+
base=str(receipt.get("baseCommit") or ""),
|
|
4325
|
+
tip=str(receipt.get("tipCommit") or ""),
|
|
4326
|
+
)
|
|
4327
|
+
if not evaluated.get("ok"):
|
|
4328
|
+
return {
|
|
4329
|
+
**evaluated,
|
|
4330
|
+
"present": True,
|
|
4331
|
+
"receiptPath": str(path),
|
|
4332
|
+
}
|
|
4333
|
+
project = evaluated["project"]
|
|
4334
|
+
repository_id = hp.repository_identity(project)
|
|
4335
|
+
ownership = evaluated["ownership"]
|
|
4336
|
+
if receipt.get("repositoryId") != repository_id:
|
|
4337
|
+
issues.append("repositoryId")
|
|
4338
|
+
if receipt.get("diffHash") != ownership.get("diffHash"):
|
|
4339
|
+
issues.append("diffHash")
|
|
4340
|
+
if receipt.get("ownershipHash") != ownership.get("ownershipHash"):
|
|
4341
|
+
issues.append("ownershipHash")
|
|
4342
|
+
if list(receipt.get("files") or []) != list(ownership.get("files") or []):
|
|
4343
|
+
issues.append("files")
|
|
4344
|
+
if issues:
|
|
4345
|
+
return {
|
|
4346
|
+
"ok": False,
|
|
4347
|
+
"present": True,
|
|
4348
|
+
"reasonCode": "EXISTING_RANGE_RECEIPT_STALE",
|
|
4349
|
+
"message": "既有范围认领收据与当前仓库不一致:" + ", ".join(issues),
|
|
4350
|
+
"receiptPath": str(path),
|
|
4351
|
+
}
|
|
4352
|
+
return {
|
|
4353
|
+
"ok": True,
|
|
4354
|
+
"present": True,
|
|
4355
|
+
"reasonCode": "EXISTING_RANGE_ADOPTED",
|
|
4356
|
+
"receiptPath": str(path),
|
|
4357
|
+
"receipt": receipt,
|
|
4358
|
+
"baseCommit": evaluated["baseCommit"],
|
|
4359
|
+
"tipCommit": evaluated["tipCommit"],
|
|
4360
|
+
"files": list(ownership.get("files") or []),
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
|
|
4364
|
+
def adopt_existing_range(
|
|
4365
|
+
change_dir: Path,
|
|
4366
|
+
*,
|
|
4367
|
+
base: str,
|
|
4368
|
+
tip: str,
|
|
4369
|
+
reason: str,
|
|
4370
|
+
confirmed: bool,
|
|
4371
|
+
) -> dict[str, Any]:
|
|
4372
|
+
change_dir = change_dir.resolve()
|
|
4373
|
+
path = _archive_range_adoption_path(change_dir)
|
|
4374
|
+
if not confirmed:
|
|
4375
|
+
return {
|
|
4376
|
+
"ok": False,
|
|
4377
|
+
"action": "adopt-existing-range",
|
|
4378
|
+
"reasonCode": "EXISTING_RANGE_CONFIRMATION_REQUIRED",
|
|
4379
|
+
"message": "认领既有提交范围必须先获得用户明确确认。",
|
|
4380
|
+
"receiptPath": str(path),
|
|
4381
|
+
}
|
|
4382
|
+
reason = reason.strip()
|
|
4383
|
+
if not reason or re.search(r"[\u4e00-\u9fff]", reason) is None:
|
|
4384
|
+
return {
|
|
4385
|
+
"ok": False,
|
|
4386
|
+
"action": "adopt-existing-range",
|
|
4387
|
+
"reasonCode": "EXISTING_RANGE_REASON_REQUIRED",
|
|
4388
|
+
"message": "请提供可读的中文认领原因。",
|
|
4389
|
+
"receiptPath": str(path),
|
|
4390
|
+
}
|
|
4391
|
+
evaluated = _evaluate_existing_range(
|
|
4392
|
+
change_dir,
|
|
4393
|
+
base=base,
|
|
4394
|
+
tip=tip,
|
|
4395
|
+
)
|
|
4396
|
+
if not evaluated.get("ok"):
|
|
4397
|
+
return {
|
|
4398
|
+
**evaluated,
|
|
4399
|
+
"action": "adopt-existing-range",
|
|
4400
|
+
"receiptPath": str(path),
|
|
4401
|
+
}
|
|
4402
|
+
project = evaluated["project"]
|
|
4403
|
+
ownership = evaluated["ownership"]
|
|
4404
|
+
if path.is_file():
|
|
4405
|
+
existing = validate_existing_range_adoption(change_dir)
|
|
4406
|
+
existing_receipt = (
|
|
4407
|
+
existing.get("receipt")
|
|
4408
|
+
if isinstance(existing.get("receipt"), dict)
|
|
4409
|
+
else {}
|
|
4410
|
+
)
|
|
4411
|
+
if (
|
|
4412
|
+
existing.get("ok")
|
|
4413
|
+
and existing_receipt.get("baseCommit") == evaluated["baseCommit"]
|
|
4414
|
+
and existing_receipt.get("tipCommit") == evaluated["tipCommit"]
|
|
4415
|
+
and existing_receipt.get("reason") == reason
|
|
4416
|
+
):
|
|
4417
|
+
return {
|
|
4418
|
+
"ok": True,
|
|
4419
|
+
"action": "adopt-existing-range",
|
|
4420
|
+
"reasonCode": "EXISTING_RANGE_ADOPTED",
|
|
4421
|
+
"idempotent": True,
|
|
4422
|
+
"receiptPath": str(path),
|
|
4423
|
+
"receipt": existing_receipt,
|
|
4424
|
+
}
|
|
4425
|
+
return {
|
|
4426
|
+
"ok": False,
|
|
4427
|
+
"action": "adopt-existing-range",
|
|
4428
|
+
"reasonCode": "EXISTING_RANGE_RECEIPT_CONFLICT",
|
|
4429
|
+
"message": "已有不同的既有范围认领收据,拒绝覆盖。",
|
|
4430
|
+
"receiptPath": str(path),
|
|
4431
|
+
}
|
|
4432
|
+
receipt = {
|
|
4433
|
+
"schemaVersion": 1,
|
|
4434
|
+
"action": "adopt-existing-range",
|
|
4435
|
+
"changeName": change_dir.name,
|
|
4436
|
+
"repositoryId": hp.repository_identity(project),
|
|
4437
|
+
"baseCommit": evaluated["baseCommit"],
|
|
4438
|
+
"tipCommit": evaluated["tipCommit"],
|
|
4439
|
+
"diffHash": ownership["diffHash"],
|
|
4440
|
+
"ownershipHash": ownership["ownershipHash"],
|
|
4441
|
+
"files": list(ownership.get("files") or []),
|
|
4442
|
+
"reason": reason,
|
|
4443
|
+
"confirmedByUser": True,
|
|
4444
|
+
"recordedAt": now_iso(),
|
|
4445
|
+
}
|
|
4446
|
+
receipt["receiptId"] = _archive_range_receipt_id(receipt)
|
|
4447
|
+
write_json(path, receipt)
|
|
4448
|
+
return {
|
|
4449
|
+
"ok": True,
|
|
4450
|
+
"action": "adopt-existing-range",
|
|
4451
|
+
"reasonCode": "EXISTING_RANGE_ADOPTED",
|
|
4452
|
+
"idempotent": False,
|
|
4453
|
+
"receiptPath": str(path),
|
|
4454
|
+
"receipt": receipt,
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
|
|
3899
4458
|
def _base_from_state_snapshot(change_dir: Path) -> str:
|
|
3900
4459
|
"""Read archive-boundary base from meta/state-snapshot.json when present."""
|
|
3901
4460
|
for relative in (
|
|
@@ -3911,6 +4470,9 @@ def _base_from_state_snapshot(change_dir: Path) -> str:
|
|
|
3911
4470
|
continue
|
|
3912
4471
|
if not isinstance(payload, dict):
|
|
3913
4472
|
continue
|
|
4473
|
+
change_base = str(payload.get("changeBase") or "").strip()
|
|
4474
|
+
if change_base and change_base != NOT_AVAILABLE:
|
|
4475
|
+
return change_base
|
|
3914
4476
|
git_info = payload.get("git")
|
|
3915
4477
|
if isinstance(git_info, dict):
|
|
3916
4478
|
base = str(git_info.get("base") or git_info.get("baseCommit") or "").strip()
|
|
@@ -3922,6 +4484,27 @@ def _base_from_state_snapshot(change_dir: Path) -> str:
|
|
|
3922
4484
|
return ""
|
|
3923
4485
|
|
|
3924
4486
|
|
|
4487
|
+
def _immutable_change_base_from_snapshot(change_dir: Path) -> str:
|
|
4488
|
+
for root in archive_read_roots(change_dir):
|
|
4489
|
+
for relative in (
|
|
4490
|
+
Path("meta") / "state-snapshot.json",
|
|
4491
|
+
Path("state-snapshot.json"),
|
|
4492
|
+
):
|
|
4493
|
+
path = root / relative
|
|
4494
|
+
if not path.is_file():
|
|
4495
|
+
continue
|
|
4496
|
+
try:
|
|
4497
|
+
payload = read_json(path)
|
|
4498
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
4499
|
+
continue
|
|
4500
|
+
if not isinstance(payload, dict):
|
|
4501
|
+
continue
|
|
4502
|
+
value = str(payload.get("changeBase") or "").strip()
|
|
4503
|
+
if value and value != NOT_AVAILABLE:
|
|
4504
|
+
return value
|
|
4505
|
+
return ""
|
|
4506
|
+
|
|
4507
|
+
|
|
3925
4508
|
def _resolve_base_commit(
|
|
3926
4509
|
ledger: dict[str, Any] | None,
|
|
3927
4510
|
change_dir: Path,
|
|
@@ -3947,6 +4530,12 @@ def _resolve_base_commit(
|
|
|
3947
4530
|
return ""
|
|
3948
4531
|
return candidate
|
|
3949
4532
|
|
|
4533
|
+
adoption = validate_existing_range_adoption(change_dir)
|
|
4534
|
+
if adoption.get("ok") and adoption.get("present"):
|
|
4535
|
+
adopted_base = stable_base(adoption.get("baseCommit"))
|
|
4536
|
+
if adopted_base:
|
|
4537
|
+
return adopted_base
|
|
4538
|
+
|
|
3950
4539
|
if ledger:
|
|
3951
4540
|
base = stable_base(ledger.get("baseCommit"))
|
|
3952
4541
|
if base and base != NOT_AVAILABLE:
|
|
@@ -5045,6 +5634,10 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5045
5634
|
a lossy projection of itself.
|
|
5046
5635
|
"""
|
|
5047
5636
|
issues: list[dict[str, str]] = []
|
|
5637
|
+
closure_disposition = str(
|
|
5638
|
+
summary.get("closureDisposition") or "completed"
|
|
5639
|
+
).strip()
|
|
5640
|
+
unfinished_closure = closure_disposition in {"abandoned", "superseded"}
|
|
5048
5641
|
|
|
5049
5642
|
# H-12: prefer top-level baseCommit + diffStat; gitFacts is a mirror.
|
|
5050
5643
|
base = str(summary.get("baseCommit") or "").strip()
|
|
@@ -5069,7 +5662,7 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5069
5662
|
final_present = bool(
|
|
5070
5663
|
(final and final != NOT_AVAILABLE) or (merge_final and merge_final != NOT_AVAILABLE)
|
|
5071
5664
|
)
|
|
5072
|
-
if final_present and base_missing:
|
|
5665
|
+
if final_present and base_missing and not unfinished_closure:
|
|
5073
5666
|
issues.append({
|
|
5074
5667
|
"code": "IDENTITY_BASE_MISSING",
|
|
5075
5668
|
"severity": "error",
|
|
@@ -5086,6 +5679,7 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5086
5679
|
and final != NOT_AVAILABLE
|
|
5087
5680
|
and base != final
|
|
5088
5681
|
and files_changed == 0
|
|
5682
|
+
and not unfinished_closure
|
|
5089
5683
|
):
|
|
5090
5684
|
issues.append({
|
|
5091
5685
|
"code": "DIFF_ZERO_WITH_NONEMPTY_COMMIT",
|
|
@@ -5113,13 +5707,14 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5113
5707
|
if not feature_tip:
|
|
5114
5708
|
feature_tip = str(identity_doc_early.get("productCommit") or "").strip()
|
|
5115
5709
|
|
|
5116
|
-
|
|
5710
|
+
collapsed_boundary = (
|
|
5117
5711
|
base
|
|
5118
5712
|
and base != NOT_AVAILABLE
|
|
5119
5713
|
and feature_tip
|
|
5120
5714
|
and feature_tip != NOT_AVAILABLE
|
|
5121
5715
|
and base == feature_tip
|
|
5122
|
-
)
|
|
5716
|
+
)
|
|
5717
|
+
if collapsed_boundary and not unfinished_closure:
|
|
5123
5718
|
issues.append({
|
|
5124
5719
|
"code": "ARCHIVE_BASE_EQUALS_FEATURE_TIP",
|
|
5125
5720
|
"severity": "error",
|
|
@@ -5128,6 +5723,15 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5128
5723
|
"archive boundary collapsed to an empty/merge-only delta"
|
|
5129
5724
|
),
|
|
5130
5725
|
})
|
|
5726
|
+
elif unfinished_closure and files_changed == 0:
|
|
5727
|
+
issues.append({
|
|
5728
|
+
"code": "NO_PRODUCT_DELTA",
|
|
5729
|
+
"severity": "warning",
|
|
5730
|
+
"message": (
|
|
5731
|
+
"未完成变更没有产生产品增量;仅封存过程、原因与已有证据,"
|
|
5732
|
+
"不得作为发布候选。"
|
|
5733
|
+
),
|
|
5734
|
+
})
|
|
5131
5735
|
|
|
5132
5736
|
ownership = summary.get("ownershipDiff") or summary.get("ownership")
|
|
5133
5737
|
ownership = ownership if isinstance(ownership, dict) else {}
|
|
@@ -5140,7 +5744,7 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5140
5744
|
changed_files = summary.get("changedFiles")
|
|
5141
5745
|
changed_count = len(changed_files) if isinstance(changed_files, list) else files_changed
|
|
5142
5746
|
|
|
5143
|
-
if ownership_count > 0 and changed_count == 0:
|
|
5747
|
+
if ownership_count > 0 and changed_count == 0 and not unfinished_closure:
|
|
5144
5748
|
issues.append({
|
|
5145
5749
|
"code": "ARCHIVE_DIFF_SHRUNK_VS_OWNERSHIP",
|
|
5146
5750
|
"severity": "error",
|
|
@@ -5150,7 +5754,12 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5150
5754
|
"but does not represent the full change"
|
|
5151
5755
|
),
|
|
5152
5756
|
})
|
|
5153
|
-
elif
|
|
5757
|
+
elif (
|
|
5758
|
+
ownership_count > 0
|
|
5759
|
+
and changed_count > 0
|
|
5760
|
+
and changed_count * 3 < ownership_count
|
|
5761
|
+
and not unfinished_closure
|
|
5762
|
+
):
|
|
5154
5763
|
issues.append({
|
|
5155
5764
|
"code": "ARCHIVE_DIFF_SHRUNK_VS_OWNERSHIP",
|
|
5156
5765
|
"severity": "error",
|
|
@@ -5161,7 +5770,11 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5161
5770
|
})
|
|
5162
5771
|
|
|
5163
5772
|
merge_parents = summary.get("mergeParents") or summary.get("mergeParentHashes")
|
|
5164
|
-
if
|
|
5773
|
+
if (
|
|
5774
|
+
isinstance(merge_parents, list)
|
|
5775
|
+
and len(merge_parents) >= 2
|
|
5776
|
+
and not unfinished_closure
|
|
5777
|
+
):
|
|
5165
5778
|
first_parent = str(merge_parents[0] or "").strip()
|
|
5166
5779
|
if (
|
|
5167
5780
|
base
|
|
@@ -5384,7 +5997,15 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
5384
5997
|
}
|
|
5385
5998
|
)
|
|
5386
5999
|
|
|
5387
|
-
|
|
6000
|
+
error_count = sum(
|
|
6001
|
+
1 for issue in issues if str(issue.get("severity") or "error") == "error"
|
|
6002
|
+
)
|
|
6003
|
+
return {
|
|
6004
|
+
"ok": error_count == 0,
|
|
6005
|
+
"issues": issues,
|
|
6006
|
+
"error_count": error_count,
|
|
6007
|
+
"warning_count": len(issues) - error_count,
|
|
6008
|
+
}
|
|
5388
6009
|
|
|
5389
6010
|
|
|
5390
6011
|
RELEASE_CHECKS = (
|
|
@@ -6677,6 +7298,7 @@ def cmd_finalize(
|
|
|
6677
7298
|
archive_intent: str = "release-candidate",
|
|
6678
7299
|
closure_disposition: str = "completed",
|
|
6679
7300
|
closure_reason: str = "",
|
|
7301
|
+
preflight_status: dict[str, Any] | None = None,
|
|
6680
7302
|
) -> tuple[int, dict[str, Any]]:
|
|
6681
7303
|
"""Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
|
|
6682
7304
|
if archive_intent not in {"release-candidate", "record-only"}:
|
|
@@ -6747,6 +7369,11 @@ def cmd_finalize(
|
|
|
6747
7369
|
if resolved_state_dir.resolve() != original_change_dir.resolve()
|
|
6748
7370
|
else None
|
|
6749
7371
|
)
|
|
7372
|
+
authoritative_event_dir = (
|
|
7373
|
+
split_state_dir
|
|
7374
|
+
if split_state_dir is not None and split_state_dir.is_dir()
|
|
7375
|
+
else original_change_dir
|
|
7376
|
+
)
|
|
6750
7377
|
operation_id = f"a-{uuid.uuid4().hex[:12]}"
|
|
6751
7378
|
operation_root = project_root / ".harness" / "archive-operations"
|
|
6752
7379
|
operation_temp_dir = operation_root / "staging" / operation_id / change_dir.name
|
|
@@ -6756,13 +7383,22 @@ def cmd_finalize(
|
|
|
6756
7383
|
shutil.rmtree(operation_temp_dir.parent, ignore_errors=True)
|
|
6757
7384
|
payload["original_preserved"] = original_change_dir.is_dir()
|
|
6758
7385
|
payload["finalStatus"] = "FAIL"
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
7386
|
+
if authoritative_event_dir.is_dir():
|
|
7387
|
+
try:
|
|
7388
|
+
append_event(
|
|
7389
|
+
authoritative_event_dir,
|
|
7390
|
+
phase="archive",
|
|
7391
|
+
type_="phase.end",
|
|
7392
|
+
status="FAIL",
|
|
7393
|
+
code=str(
|
|
7394
|
+
payload.get("reasonCode")
|
|
7395
|
+
or "ARCHIVE_FINALIZE_FAILED"
|
|
7396
|
+
),
|
|
7397
|
+
message=str(payload.get("error") or "归档执行失败"),
|
|
7398
|
+
note=f"归档操作 {operation_id} 失败,原变更目录已保留",
|
|
7399
|
+
)
|
|
7400
|
+
except OSError:
|
|
7401
|
+
pass
|
|
6766
7402
|
try:
|
|
6767
7403
|
write_json(
|
|
6768
7404
|
operation_record,
|
|
@@ -6806,6 +7442,65 @@ def cmd_finalize(
|
|
|
6806
7442
|
payload["error"] = f"archive target already exists: {archive_dir}"
|
|
6807
7443
|
return 1, payload
|
|
6808
7444
|
|
|
7445
|
+
status = preflight_status or check_status(
|
|
7446
|
+
original_change_dir,
|
|
7447
|
+
allow_missing_review=allow_missing_review,
|
|
7448
|
+
archive_intent=archive_intent,
|
|
7449
|
+
closure_disposition=closure_disposition,
|
|
7450
|
+
closure_reason=closure_reason,
|
|
7451
|
+
)
|
|
7452
|
+
payload["steps"]["preflight"] = status
|
|
7453
|
+
if not status.get("archivable"):
|
|
7454
|
+
report_adequacy = (
|
|
7455
|
+
(status.get("releaseDecision") or {}).get("checks") or {}
|
|
7456
|
+
).get("reportAdequacy") or {}
|
|
7457
|
+
adequacy_failed = report_adequacy.get("ok") is False
|
|
7458
|
+
payload["reasonCode"] = (
|
|
7459
|
+
"ARCHIVE_REPORT_ADEQUACY_FAILED"
|
|
7460
|
+
if adequacy_failed
|
|
7461
|
+
else "ARCHIVE_PRECONDITIONS_UNSATISFIED"
|
|
7462
|
+
)
|
|
7463
|
+
payload["error"] = (
|
|
7464
|
+
"归档报告不完整,未进入暂存与发布阶段。"
|
|
7465
|
+
if adequacy_failed
|
|
7466
|
+
else "归档前置条件未满足,未进入暂存与发布阶段。"
|
|
7467
|
+
)
|
|
7468
|
+
payload["issues"] = [
|
|
7469
|
+
{
|
|
7470
|
+
"code": str(
|
|
7471
|
+
item.get("code") or "ARCHIVE_PRECONDITION_FAILED"
|
|
7472
|
+
).replace("-", "_").upper(),
|
|
7473
|
+
"severity": "error",
|
|
7474
|
+
"message": str(item.get("message") or payload["error"]),
|
|
7475
|
+
}
|
|
7476
|
+
for item in status.get("blockers") or []
|
|
7477
|
+
if isinstance(item, dict)
|
|
7478
|
+
]
|
|
7479
|
+
payload["original_preserved"] = original_change_dir.is_dir()
|
|
7480
|
+
payload["finalStatus"] = "BLOCKED"
|
|
7481
|
+
_append_finalize_failure_terminal(
|
|
7482
|
+
split_state_dir
|
|
7483
|
+
if split_state_dir is not None and split_state_dir.is_dir()
|
|
7484
|
+
else original_change_dir,
|
|
7485
|
+
payload["error"],
|
|
7486
|
+
operation_id=operation_id,
|
|
7487
|
+
)
|
|
7488
|
+
return 1, payload
|
|
7489
|
+
|
|
7490
|
+
# The formal archive attempt starts after the one-shot preflight succeeds,
|
|
7491
|
+
# before scans and staging begin. Keeping this start in authoritative state
|
|
7492
|
+
# makes the monitor show the real duration and exactly one attempt.
|
|
7493
|
+
if authoritative_event_dir.is_dir():
|
|
7494
|
+
try:
|
|
7495
|
+
append_event(
|
|
7496
|
+
authoritative_event_dir,
|
|
7497
|
+
phase="archive",
|
|
7498
|
+
type_="phase.start",
|
|
7499
|
+
note="归档条件已通过,开始扫描、暂存与发布",
|
|
7500
|
+
)
|
|
7501
|
+
except OSError as exc:
|
|
7502
|
+
warnings.append(f"archive phase start append failed: {exc}")
|
|
7503
|
+
|
|
6809
7504
|
source_sensitive_refresh = hruntime.refresh_sensitive_evidence_scan_receipt(
|
|
6810
7505
|
original_change_dir,
|
|
6811
7506
|
persist=False,
|
|
@@ -6826,8 +7521,7 @@ def cmd_finalize(
|
|
|
6826
7521
|
"message": payload["error"],
|
|
6827
7522
|
}
|
|
6828
7523
|
]
|
|
6829
|
-
|
|
6830
|
-
payload["finalStatus"] = "FAIL"
|
|
7524
|
+
_restore_finalize_failure()
|
|
6831
7525
|
return 1, payload
|
|
6832
7526
|
|
|
6833
7527
|
source_sensitive_gate = validate_sensitive_evidence_publication_gate(
|
|
@@ -6853,8 +7547,7 @@ def cmd_finalize(
|
|
|
6853
7547
|
"message": payload["error"],
|
|
6854
7548
|
}
|
|
6855
7549
|
]
|
|
6856
|
-
|
|
6857
|
-
payload["finalStatus"] = "FAIL"
|
|
7550
|
+
_restore_finalize_failure()
|
|
6858
7551
|
return 1, payload
|
|
6859
7552
|
|
|
6860
7553
|
exact_byte = check_archive_exact_byte_policy(project_root)
|
|
@@ -6885,15 +7578,7 @@ def cmd_finalize(
|
|
|
6885
7578
|
),
|
|
6886
7579
|
}
|
|
6887
7580
|
]
|
|
6888
|
-
|
|
6889
|
-
payload["finalStatus"] = "FAIL"
|
|
6890
|
-
_append_finalize_failure_terminal(
|
|
6891
|
-
split_state_dir
|
|
6892
|
-
if split_state_dir is not None and split_state_dir.is_dir()
|
|
6893
|
-
else original_change_dir,
|
|
6894
|
-
payload["error"],
|
|
6895
|
-
operation_id=operation_id,
|
|
6896
|
-
)
|
|
7581
|
+
_restore_finalize_failure()
|
|
6897
7582
|
return 1, payload
|
|
6898
7583
|
|
|
6899
7584
|
payload["steps"]["service_stop_before_staging"] = run_service_stop(
|
|
@@ -7012,9 +7697,6 @@ def cmd_finalize(
|
|
|
7012
7697
|
except OSError as exc:
|
|
7013
7698
|
warnings.append(f"event append failed: {exc}")
|
|
7014
7699
|
|
|
7015
|
-
# Step 9 starts here: phase.start
|
|
7016
|
-
_safe_append(phase="archive", type_="phase.start", note="finalize start")
|
|
7017
|
-
|
|
7018
7700
|
# --- 0. cleanup transients (before before-manifest) ---
|
|
7019
7701
|
try:
|
|
7020
7702
|
cleanup_result = _cleanup_transients(work_dir)
|
|
@@ -8652,6 +9334,20 @@ def cmd_auto_gate_cli(args: argparse.Namespace) -> int:
|
|
|
8652
9334
|
resolve_path(args.change_dir),
|
|
8653
9335
|
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
8654
9336
|
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
9337
|
+
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
9338
|
+
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
9339
|
+
)
|
|
9340
|
+
emit_json(result)
|
|
9341
|
+
return 0 if result.get("ok") else 1
|
|
9342
|
+
|
|
9343
|
+
|
|
9344
|
+
def cmd_adopt_existing_range_cli(args: argparse.Namespace) -> int:
|
|
9345
|
+
result = adopt_existing_range(
|
|
9346
|
+
resolve_path(args.change_dir),
|
|
9347
|
+
base=str(args.base),
|
|
9348
|
+
tip=str(args.tip),
|
|
9349
|
+
reason=str(args.reason),
|
|
9350
|
+
confirmed=bool(getattr(args, "confirm_existing_range", False)),
|
|
8655
9351
|
)
|
|
8656
9352
|
emit_json(result)
|
|
8657
9353
|
return 0 if result.get("ok") else 1
|
|
@@ -8715,6 +9411,29 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
8715
9411
|
return code
|
|
8716
9412
|
|
|
8717
9413
|
|
|
9414
|
+
def cmd_execute_cli(args: argparse.Namespace) -> int:
|
|
9415
|
+
durable_root = (
|
|
9416
|
+
resolve_path(args.durable_root)
|
|
9417
|
+
if getattr(args, "durable_root", None)
|
|
9418
|
+
else None
|
|
9419
|
+
)
|
|
9420
|
+
code, payload = execute_archive(
|
|
9421
|
+
resolve_path(args.change_dir),
|
|
9422
|
+
resolve_path(args.archive_root),
|
|
9423
|
+
durable_root=durable_root,
|
|
9424
|
+
retention_policy=str(
|
|
9425
|
+
getattr(args, "retention_policy", "unspecified")
|
|
9426
|
+
),
|
|
9427
|
+
skip_ingest=bool(args.skip_ingest),
|
|
9428
|
+
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
9429
|
+
archive_intent=str(getattr(args, "intent", "release-candidate")),
|
|
9430
|
+
closure_disposition=str(getattr(args, "closure", "completed")),
|
|
9431
|
+
closure_reason=str(getattr(args, "closure_reason", "")),
|
|
9432
|
+
)
|
|
9433
|
+
emit_json(payload)
|
|
9434
|
+
return code
|
|
9435
|
+
|
|
9436
|
+
|
|
8718
9437
|
def cmd_restore_durable_cli(args: argparse.Namespace) -> int:
|
|
8719
9438
|
receipt_path = resolve_path(args.receipt)
|
|
8720
9439
|
target_root = resolve_path(args.target_root)
|
|
@@ -8902,8 +9621,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
8902
9621
|
choices=("release-candidate", "record-only"),
|
|
8903
9622
|
default="release-candidate",
|
|
8904
9623
|
)
|
|
9624
|
+
p_auto_gate.add_argument(
|
|
9625
|
+
"--closure",
|
|
9626
|
+
choices=("completed", "abandoned", "superseded"),
|
|
9627
|
+
default="completed",
|
|
9628
|
+
)
|
|
9629
|
+
p_auto_gate.add_argument("--closure-reason", default="")
|
|
8905
9630
|
p_auto_gate.set_defaults(func=cmd_auto_gate_cli)
|
|
8906
9631
|
|
|
9632
|
+
p_adopt = sub.add_parser(
|
|
9633
|
+
"adopt-existing-range",
|
|
9634
|
+
help="explicitly bind an already committed product range to this change",
|
|
9635
|
+
)
|
|
9636
|
+
p_adopt.add_argument("--change-dir", required=True)
|
|
9637
|
+
p_adopt.add_argument("--base", required=True)
|
|
9638
|
+
p_adopt.add_argument("--tip", required=True)
|
|
9639
|
+
p_adopt.add_argument("--reason", required=True)
|
|
9640
|
+
p_adopt.add_argument("--confirm-existing-range", action="store_true")
|
|
9641
|
+
p_adopt.add_argument("--json", action="store_true", default=True)
|
|
9642
|
+
p_adopt.set_defaults(func=cmd_adopt_existing_range_cli)
|
|
9643
|
+
|
|
8907
9644
|
p_cert = sub.add_parser(
|
|
8908
9645
|
"certify-local",
|
|
8909
9646
|
help="create candidate receipt from existing full ledger evidence without rerunning tests",
|
|
@@ -8951,6 +9688,30 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
8951
9688
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
8952
9689
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
8953
9690
|
|
|
9691
|
+
p_execute = sub.add_parser(
|
|
9692
|
+
"execute",
|
|
9693
|
+
help="run one archive preflight, boundary gate, finalize, ZIP, and upload flow",
|
|
9694
|
+
)
|
|
9695
|
+
p_execute.add_argument("--change-dir", required=True)
|
|
9696
|
+
p_execute.add_argument("--archive-root", required=True)
|
|
9697
|
+
p_execute.add_argument("--durable-root", default=None)
|
|
9698
|
+
p_execute.add_argument(
|
|
9699
|
+
"--closure",
|
|
9700
|
+
choices=("completed", "abandoned", "superseded"),
|
|
9701
|
+
default="completed",
|
|
9702
|
+
)
|
|
9703
|
+
p_execute.add_argument("--closure-reason", default="")
|
|
9704
|
+
p_execute.add_argument("--retention-policy", default="unspecified")
|
|
9705
|
+
p_execute.add_argument("--skip-ingest", action="store_true")
|
|
9706
|
+
p_execute.add_argument("--allow-missing-review", action="store_true")
|
|
9707
|
+
p_execute.add_argument(
|
|
9708
|
+
"--intent",
|
|
9709
|
+
choices=("release-candidate", "record-only"),
|
|
9710
|
+
default="release-candidate",
|
|
9711
|
+
)
|
|
9712
|
+
p_execute.add_argument("--json", action="store_true", default=True)
|
|
9713
|
+
p_execute.set_defaults(func=cmd_execute_cli)
|
|
9714
|
+
|
|
8954
9715
|
p_restore = sub.add_parser(
|
|
8955
9716
|
"restore-durable",
|
|
8956
9717
|
help="verify a durable receipt and restore its archive without overwriting",
|