@hunter-harness/workflow-harness 0.2.10 → 0.2.12
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/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- 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-ingest/scripts/harness_knowledge.py +24 -2
- 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-plan/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
- package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
- package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
- package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- 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-ingest/scripts/harness_knowledge.py +24 -2
- 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-plan/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
- package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- 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-ingest/scripts/harness_knowledge.py +24 -2
- 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-plan/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
- package/harness/bundles/general/codex/harness-run/reference.md +6 -4
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
- package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
- package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
- package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
- package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
- package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
- package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- 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-ingest/scripts/harness_knowledge.py +24 -2
- 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-plan/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
- package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
- package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
- package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
- package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
- package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
- package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-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-ingest/scripts/harness_knowledge.py +24 -2
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
- package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
- package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-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-ingest/scripts/harness_knowledge.py +24 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-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-ingest/scripts/harness_knowledge.py +24 -2
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
- package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
- package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
- package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
- package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
- package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
- package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-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-ingest/scripts/harness_knowledge.py +24 -2
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
- package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
- package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
- package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
- package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
- package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
- package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
- package/harness/manifests/general/claude-code.json +43 -27
- package/harness/manifests/general/codebuddy.json +43 -27
- package/harness/manifests/general/codex.json +43 -27
- package/harness/manifests/general/cursor.json +43 -27
- package/harness/manifests/java/claude-code.json +43 -27
- package/harness/manifests/java/codebuddy.json +43 -27
- package/harness/manifests/java/codex.json +43 -27
- package/harness/manifests/java/cursor.json +43 -27
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -318,6 +318,19 @@ def load_ledger(change_dir: Path) -> dict[str, Any] | None:
|
|
|
318
318
|
return None
|
|
319
319
|
|
|
320
320
|
|
|
321
|
+
def load_ci_metrics(change_dir: Path) -> tuple[dict[str, Any] | None, str | None]:
|
|
322
|
+
"""Load schema-versioned runner metrics without parsing human CI logs."""
|
|
323
|
+
for relative in ("evidence/ci-metrics.json", "runtime/ci-metrics.json"):
|
|
324
|
+
path = change_dir / relative
|
|
325
|
+
if not path.is_file():
|
|
326
|
+
continue
|
|
327
|
+
value = read_json(path)
|
|
328
|
+
if value.get("schemaVersion") != 1:
|
|
329
|
+
raise ValueError(f"unsupported ci-metrics schema: {path}")
|
|
330
|
+
return value, relative
|
|
331
|
+
return None, None
|
|
332
|
+
|
|
333
|
+
|
|
321
334
|
def load_execution_log(change_dir: Path) -> str:
|
|
322
335
|
for rel in ("logs/execution-log.md", "execution-log.md"):
|
|
323
336
|
path = change_dir / rel
|
|
@@ -433,6 +446,42 @@ def worktree_requested(change_dir: Path) -> bool:
|
|
|
433
446
|
return False
|
|
434
447
|
|
|
435
448
|
|
|
449
|
+
def check_archive_exact_byte_policy(project_root: Path) -> dict[str, Any]:
|
|
450
|
+
"""Verify frozen archive paths are exempt from Git text conversion."""
|
|
451
|
+
required_rule = ".harness/archive/** -text"
|
|
452
|
+
attributes_path = project_root / ".gitattributes"
|
|
453
|
+
rules: list[str] = []
|
|
454
|
+
if attributes_path.is_file():
|
|
455
|
+
try:
|
|
456
|
+
rules = [
|
|
457
|
+
line.strip()
|
|
458
|
+
for line in attributes_path.read_text(encoding="utf-8-sig").splitlines()
|
|
459
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
460
|
+
]
|
|
461
|
+
except OSError:
|
|
462
|
+
rules = []
|
|
463
|
+
matching = []
|
|
464
|
+
for rule in rules:
|
|
465
|
+
fields = rule.split()
|
|
466
|
+
if not fields or fields[0] != ".harness/archive/**":
|
|
467
|
+
continue
|
|
468
|
+
matching.append(rule)
|
|
469
|
+
if "-text" in fields[1:] or "binary" in fields[1:]:
|
|
470
|
+
return {
|
|
471
|
+
"ok": True,
|
|
472
|
+
"path": str(attributes_path),
|
|
473
|
+
"requiredRule": required_rule,
|
|
474
|
+
"matchedRule": rule,
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
"ok": False,
|
|
478
|
+
"path": str(attributes_path),
|
|
479
|
+
"requiredRule": required_rule,
|
|
480
|
+
"matchedRules": matching,
|
|
481
|
+
"remediation": f"add this exact-byte rule to .gitattributes: {required_rule}",
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
436
485
|
# ---------------------------------------------------------------------------
|
|
437
486
|
# status
|
|
438
487
|
# ---------------------------------------------------------------------------
|
|
@@ -461,6 +510,16 @@ def check_status(
|
|
|
461
510
|
project = find_project_root(change_dir)
|
|
462
511
|
checks["project_root"] = str(project)
|
|
463
512
|
|
|
513
|
+
exact_byte = check_archive_exact_byte_policy(project)
|
|
514
|
+
checks["archive_exact_byte"] = exact_byte
|
|
515
|
+
if not exact_byte["ok"]:
|
|
516
|
+
warnings.append(
|
|
517
|
+
{
|
|
518
|
+
"code": "archive-exact-byte-policy-missing",
|
|
519
|
+
"message": str(exact_byte["remediation"]),
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
|
|
464
523
|
# --- commit pushed ---
|
|
465
524
|
code, out, err = git_run(project, "rev-parse", "--is-inside-work-tree")
|
|
466
525
|
if code != 0:
|
|
@@ -676,25 +735,40 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
676
735
|
status = str(unit.get("status") or "").upper()
|
|
677
736
|
|
|
678
737
|
run = failures = errors = skipped = 0
|
|
738
|
+
passed_count: int | None = None
|
|
679
739
|
pass_rate: Any = None
|
|
680
740
|
source = "committed"
|
|
681
741
|
counted = False
|
|
682
742
|
|
|
683
743
|
metrics = unit.get("metrics")
|
|
684
744
|
if isinstance(metrics, dict) and any(
|
|
685
|
-
k in metrics
|
|
745
|
+
k in metrics
|
|
746
|
+
for k in (
|
|
747
|
+
"run",
|
|
748
|
+
"testsRun",
|
|
749
|
+
"total",
|
|
750
|
+
"passed",
|
|
751
|
+
"failed",
|
|
752
|
+
"failures",
|
|
753
|
+
"errors",
|
|
754
|
+
"skipped",
|
|
755
|
+
)
|
|
686
756
|
):
|
|
687
757
|
if "total" in metrics:
|
|
688
758
|
# ledger v3 typed metrics (UT-005/RET-15): total/passed/failed.
|
|
689
759
|
run = int(metrics.get("total", 0) or 0)
|
|
690
|
-
|
|
691
|
-
errors = int(metrics.get("errors", 0) or 0)
|
|
692
|
-
skipped = int(metrics.get("skipped", 0) or 0)
|
|
693
|
-
else:
|
|
760
|
+
elif "run" in metrics or "testsRun" in metrics:
|
|
694
761
|
run = int(metrics.get("run", metrics.get("testsRun", 0)) or 0)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
762
|
+
else:
|
|
763
|
+
run = sum(
|
|
764
|
+
int(metrics.get(key, 0) or 0)
|
|
765
|
+
for key in ("passed", "failed", "errors", "skipped")
|
|
766
|
+
)
|
|
767
|
+
failures = int(metrics.get("failed", metrics.get("failures", 0)) or 0)
|
|
768
|
+
errors = int(metrics.get("errors", 0) or 0)
|
|
769
|
+
skipped = int(metrics.get("skipped", 0) or 0)
|
|
770
|
+
if "passed" in metrics:
|
|
771
|
+
passed_count = int(metrics.get("passed", 0) or 0)
|
|
698
772
|
pass_rate = metrics.get("passRate")
|
|
699
773
|
source = "committed"
|
|
700
774
|
counted = run > 0 or failures > 0 or errors > 0 or skipped > 0
|
|
@@ -741,7 +815,11 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
741
815
|
source = "committed"
|
|
742
816
|
|
|
743
817
|
if pass_rate is None and run > 0:
|
|
744
|
-
passed =
|
|
818
|
+
passed = (
|
|
819
|
+
passed_count
|
|
820
|
+
if passed_count is not None
|
|
821
|
+
else max(run - failures - errors - skipped, 0)
|
|
822
|
+
)
|
|
745
823
|
pass_rate = f"{passed / run:.0%}"
|
|
746
824
|
elif pass_rate is None:
|
|
747
825
|
pass_rate = NOT_AVAILABLE
|
|
@@ -794,10 +872,10 @@ def _ledger_api_tests(
|
|
|
794
872
|
if isinstance(metrics, dict) and any(
|
|
795
873
|
k in metrics for k in ("total", "passed", "failed", "blocked")
|
|
796
874
|
):
|
|
797
|
-
total = int(metrics.get("total", 0) or 0)
|
|
798
875
|
passed = int(metrics.get("passed", 0) or 0)
|
|
799
876
|
failed = int(metrics.get("failed", 0) or 0)
|
|
800
877
|
blocked = int(metrics.get("blocked", 0) or 0)
|
|
878
|
+
total = int(metrics.get("total", passed + failed + blocked) or 0)
|
|
801
879
|
pass_rate = metrics.get("passRate")
|
|
802
880
|
source = "committed"
|
|
803
881
|
counted = total > 0 or passed > 0 or failed > 0 or blocked > 0
|
|
@@ -1211,7 +1289,8 @@ def _stage_status_from_sources(
|
|
|
1211
1289
|
"submit": "OK",
|
|
1212
1290
|
"archive": "OK",
|
|
1213
1291
|
}
|
|
1214
|
-
|
|
1292
|
+
projected_events = he.apply_event_corrections(events)
|
|
1293
|
+
for event in projected_events:
|
|
1215
1294
|
if event.get("type") != "phase.end":
|
|
1216
1295
|
continue
|
|
1217
1296
|
phase = str(event.get("phase") or "").lower()
|
|
@@ -1226,9 +1305,7 @@ def _stage_status_from_sources(
|
|
|
1226
1305
|
raw = "USER_SKIPPED"
|
|
1227
1306
|
status[phase] = raw
|
|
1228
1307
|
# Issues in events can downgrade
|
|
1229
|
-
for e in events:
|
|
1230
|
-
if e.get("type") != "issue":
|
|
1231
|
-
continue
|
|
1308
|
+
for e in he.current_issues(events):
|
|
1232
1309
|
sev = str(e.get("severity") or "").lower()
|
|
1233
1310
|
issue_text = " ".join(str(e.get(key) or "") for key in ("message", "note", "code")).lower()
|
|
1234
1311
|
if not sev:
|
|
@@ -1258,7 +1335,7 @@ def _stage_status_from_sources(
|
|
|
1258
1335
|
if unit.get("source") == "not-run" and unit.get("run", 0) == 0:
|
|
1259
1336
|
status["test"] = "NOT_RUN"
|
|
1260
1337
|
|
|
1261
|
-
if not review_evidence_present(change_dir,
|
|
1338
|
+
if not review_evidence_present(change_dir, projected_events):
|
|
1262
1339
|
status["review"] = "ADVISORY"
|
|
1263
1340
|
|
|
1264
1341
|
return status
|
|
@@ -1665,6 +1742,17 @@ def collect_summary_data(
|
|
|
1665
1742
|
for_replay: bool = False,
|
|
1666
1743
|
) -> dict[str, Any]:
|
|
1667
1744
|
"""Build schema 2.2 summary-data from events/ledger/log/manifest/reports."""
|
|
1745
|
+
if before_manifest is None:
|
|
1746
|
+
frozen_before = change_dir / "evidence" / "archive-manifest-before.json"
|
|
1747
|
+
if frozen_before.is_file():
|
|
1748
|
+
before_manifest = read_json(frozen_before)
|
|
1749
|
+
if after_manifest is None:
|
|
1750
|
+
frozen_after = change_dir / "evidence" / "archive-manifest-after.json"
|
|
1751
|
+
if frozen_after.is_file():
|
|
1752
|
+
after_manifest = read_json(frozen_after)
|
|
1753
|
+
if compare_result is None and before_manifest is not None and after_manifest is not None:
|
|
1754
|
+
compare_result = compare_manifests(before_manifest, after_manifest)
|
|
1755
|
+
|
|
1668
1756
|
template = load_template()
|
|
1669
1757
|
data = _deepcopy_json(template)
|
|
1670
1758
|
# Clear placeholder strings from template
|
|
@@ -1682,11 +1770,16 @@ def collect_summary_data(
|
|
|
1682
1770
|
sources.append("events.ndjson")
|
|
1683
1771
|
except ValueError:
|
|
1684
1772
|
events = []
|
|
1773
|
+
projected_events = he.apply_event_corrections(events) if events else []
|
|
1685
1774
|
|
|
1686
1775
|
ledger = load_ledger(change_dir)
|
|
1687
1776
|
if ledger:
|
|
1688
1777
|
sources.append("evidence/verification-ledger.json")
|
|
1689
1778
|
|
|
1779
|
+
ci_metrics, ci_metrics_source = load_ci_metrics(change_dir)
|
|
1780
|
+
if ci_metrics_source:
|
|
1781
|
+
sources.append(ci_metrics_source)
|
|
1782
|
+
|
|
1690
1783
|
log_text = load_execution_log(change_dir)
|
|
1691
1784
|
if log_text:
|
|
1692
1785
|
sources.append("logs/execution-log.md")
|
|
@@ -1733,13 +1826,13 @@ def collect_summary_data(
|
|
|
1733
1826
|
if existing and existing.get("businessGoal"):
|
|
1734
1827
|
data["businessGoal"] = existing["businessGoal"]
|
|
1735
1828
|
else:
|
|
1736
|
-
inferred_goal = _business_goal_from_sources(change_dir,
|
|
1829
|
+
inferred_goal = _business_goal_from_sources(change_dir, projected_events)
|
|
1737
1830
|
data["businessGoal"] = inferred_goal or (NOT_AVAILABLE if for_replay else "")
|
|
1738
1831
|
|
|
1739
1832
|
# commits
|
|
1740
1833
|
project = find_project_root(change_dir)
|
|
1741
1834
|
if not data.get("finalCommit") or str(data.get("finalCommit")).startswith("<"):
|
|
1742
|
-
final_commit = _final_commit_from_sources(ledger,
|
|
1835
|
+
final_commit = _final_commit_from_sources(ledger, projected_events, existing, project)
|
|
1743
1836
|
data["finalCommit"] = final_commit or (NOT_AVAILABLE if for_replay else "")
|
|
1744
1837
|
|
|
1745
1838
|
if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
|
|
@@ -1782,6 +1875,8 @@ def collect_summary_data(
|
|
|
1782
1875
|
for typed_key in ("apiContract", "browserE2E"):
|
|
1783
1876
|
if typed_key in projection:
|
|
1784
1877
|
data["verification"][typed_key] = projection[typed_key]
|
|
1878
|
+
if ci_metrics is not None:
|
|
1879
|
+
data["verification"]["ciMetrics"] = _deepcopy_json(ci_metrics)
|
|
1785
1880
|
else:
|
|
1786
1881
|
# Ensure nested keys exist for old schema 2.1
|
|
1787
1882
|
ver = data.setdefault("verification", {})
|
|
@@ -1894,15 +1989,15 @@ def collect_summary_data(
|
|
|
1894
1989
|
if not isinstance(data.get("artifacts"), list):
|
|
1895
1990
|
data["artifacts"] = []
|
|
1896
1991
|
if not for_replay:
|
|
1897
|
-
data["artifacts"] = _artifacts_from_events(
|
|
1992
|
+
data["artifacts"] = _artifacts_from_events(projected_events)
|
|
1898
1993
|
|
|
1899
1994
|
data["reviewSummary"] = _review_summary(
|
|
1900
1995
|
change_dir,
|
|
1901
1996
|
existing if for_replay else None,
|
|
1902
|
-
|
|
1997
|
+
projected_events,
|
|
1903
1998
|
)
|
|
1904
1999
|
if not for_replay:
|
|
1905
|
-
data["timeline"] = _timeline_from_events(event_summary,
|
|
2000
|
+
data["timeline"] = _timeline_from_events(event_summary, projected_events)
|
|
1906
2001
|
else:
|
|
1907
2002
|
data.setdefault("timeline", [])
|
|
1908
2003
|
data.setdefault("uncommittedTestEvidence", [])
|
|
@@ -1911,13 +2006,11 @@ def collect_summary_data(
|
|
|
1911
2006
|
if not for_replay:
|
|
1912
2007
|
maintenance_notes: list[str] = [
|
|
1913
2008
|
str(event.get("note") or event.get("message") or "")
|
|
1914
|
-
for event in
|
|
2009
|
+
for event in projected_events
|
|
1915
2010
|
if event.get("type") == "decision" and (event.get("note") or event.get("message"))
|
|
1916
2011
|
]
|
|
1917
2012
|
known_risks: list[dict[str, Any]] = []
|
|
1918
|
-
for event in events:
|
|
1919
|
-
if event.get("type") != "issue":
|
|
1920
|
-
continue
|
|
2013
|
+
for event in he.current_issues(events):
|
|
1921
2014
|
if event.get("phase") == "archive" and event.get("code") == "missing-command":
|
|
1922
2015
|
continue
|
|
1923
2016
|
sev = str(event.get("severity") or "").strip().lower()
|
|
@@ -1973,12 +2066,12 @@ def collect_summary_data(
|
|
|
1973
2066
|
data["archiveManifest"] = am
|
|
1974
2067
|
|
|
1975
2068
|
# reportPipeline
|
|
1976
|
-
commands = _commands_from_events(
|
|
2069
|
+
commands = _commands_from_events(projected_events)
|
|
1977
2070
|
if not commands and for_replay:
|
|
1978
2071
|
# Cannot invent commands
|
|
1979
2072
|
pass
|
|
1980
|
-
verification_checks = _verification_checks_from_events(
|
|
1981
|
-
pipeline_artifacts = _artifacts_from_events(
|
|
2073
|
+
verification_checks = _verification_checks_from_events(projected_events, ledger)
|
|
2074
|
+
pipeline_artifacts = _artifacts_from_events(projected_events)
|
|
1982
2075
|
if not sources:
|
|
1983
2076
|
sources = [NOT_AVAILABLE]
|
|
1984
2077
|
|
|
@@ -2234,26 +2327,28 @@ def _manifest_self_stats(
|
|
|
2234
2327
|
}
|
|
2235
2328
|
|
|
2236
2329
|
|
|
2237
|
-
def
|
|
2238
|
-
|
|
2239
|
-
code: str,
|
|
2330
|
+
def _append_finalize_failure_terminal(
|
|
2331
|
+
authoritative_change_dir: Path,
|
|
2240
2332
|
message: str,
|
|
2333
|
+
*,
|
|
2334
|
+
operation_id: str,
|
|
2241
2335
|
) -> None:
|
|
2242
|
-
"""
|
|
2243
|
-
|
|
2244
|
-
Never a second phase.end: the frozen cutoff already closed the archive
|
|
2245
|
-
phase exactly once (RET-19/RET-21).
|
|
2246
|
-
"""
|
|
2247
|
-
if not original_change_dir.is_dir():
|
|
2336
|
+
"""Persist one failed finalize attempt on the authoritative change source."""
|
|
2337
|
+
if not authoritative_change_dir.is_dir():
|
|
2248
2338
|
return
|
|
2249
2339
|
try:
|
|
2250
2340
|
append_event(
|
|
2251
|
-
|
|
2341
|
+
authoritative_change_dir,
|
|
2252
2342
|
phase="archive",
|
|
2253
|
-
type_="
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2343
|
+
type_="phase.start",
|
|
2344
|
+
note=f"finalize operation {operation_id} failed before publish",
|
|
2345
|
+
)
|
|
2346
|
+
append_event(
|
|
2347
|
+
authoritative_change_dir,
|
|
2348
|
+
phase="archive",
|
|
2349
|
+
type_="phase.end",
|
|
2350
|
+
status="FAIL",
|
|
2351
|
+
note=f"finalize operation {operation_id} discarded: {message}",
|
|
2257
2352
|
)
|
|
2258
2353
|
except OSError:
|
|
2259
2354
|
pass
|
|
@@ -2849,27 +2944,38 @@ def cmd_finalize(
|
|
|
2849
2944
|
if resolved_state_dir.resolve() != original_change_dir.resolve()
|
|
2850
2945
|
else None
|
|
2851
2946
|
)
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
)
|
|
2947
|
+
operation_id = f"archive-{uuid.uuid4().hex}"
|
|
2948
|
+
operation_root = project_root / ".harness" / "archive-operations"
|
|
2949
|
+
operation_temp_dir = operation_root / "staging" / operation_id / change_name
|
|
2950
|
+
operation_record = operation_root / f"{operation_id}.json"
|
|
2857
2951
|
|
|
2858
2952
|
def _restore_finalize_failure() -> None:
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2953
|
+
shutil.rmtree(operation_temp_dir.parent, ignore_errors=True)
|
|
2954
|
+
payload["original_preserved"] = original_change_dir.is_dir()
|
|
2955
|
+
payload["finalStatus"] = "FAIL"
|
|
2956
|
+
_append_finalize_failure_terminal(
|
|
2957
|
+
split_state_dir
|
|
2958
|
+
if split_state_dir is not None and split_state_dir.is_dir()
|
|
2959
|
+
else original_change_dir,
|
|
2960
|
+
str(payload.get("error") or "archive finalize failed"),
|
|
2961
|
+
operation_id=operation_id,
|
|
2962
|
+
)
|
|
2963
|
+
try:
|
|
2964
|
+
write_json(
|
|
2965
|
+
operation_record,
|
|
2966
|
+
{
|
|
2967
|
+
"schemaVersion": 1,
|
|
2968
|
+
"operationId": operation_id,
|
|
2969
|
+
"changeName": change_name,
|
|
2970
|
+
"sourceDir": str(original_change_dir),
|
|
2971
|
+
"archiveDir": str(archive_dir),
|
|
2972
|
+
"finalStatus": "FAIL",
|
|
2973
|
+
"error": payload.get("error"),
|
|
2974
|
+
"finishedAt": now_iso(),
|
|
2975
|
+
},
|
|
2976
|
+
)
|
|
2977
|
+
except OSError as exc:
|
|
2978
|
+
warnings.append(f"could not persist failed archive operation: {exc}")
|
|
2873
2979
|
|
|
2874
2980
|
payload: dict[str, Any] = {
|
|
2875
2981
|
"ok": False,
|
|
@@ -2877,6 +2983,9 @@ def cmd_finalize(
|
|
|
2877
2983
|
"change_dir": str(original_change_dir),
|
|
2878
2984
|
"archive_dir": str(archive_dir),
|
|
2879
2985
|
"change_name": change_name,
|
|
2986
|
+
"operationId": operation_id,
|
|
2987
|
+
"operationTempDir": str(operation_temp_dir),
|
|
2988
|
+
"operationRecord": str(operation_record),
|
|
2880
2989
|
"warnings": warnings,
|
|
2881
2990
|
"steps": {},
|
|
2882
2991
|
}
|
|
@@ -2889,9 +2998,46 @@ def cmd_finalize(
|
|
|
2889
2998
|
payload["error"] = f"archive target already exists: {archive_dir}"
|
|
2890
2999
|
return 1, payload
|
|
2891
3000
|
|
|
2892
|
-
|
|
3001
|
+
exact_byte = check_archive_exact_byte_policy(project_root)
|
|
3002
|
+
payload["steps"]["archive_exact_byte"] = exact_byte
|
|
3003
|
+
if not exact_byte["ok"]:
|
|
3004
|
+
warnings.append(str(exact_byte["remediation"]))
|
|
3005
|
+
|
|
3006
|
+
try:
|
|
3007
|
+
operation_temp_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
3008
|
+
shutil.copytree(
|
|
3009
|
+
original_change_dir,
|
|
3010
|
+
operation_temp_dir,
|
|
3011
|
+
copy_function=shutil.copy2,
|
|
3012
|
+
)
|
|
3013
|
+
write_json(
|
|
3014
|
+
operation_record,
|
|
3015
|
+
{
|
|
3016
|
+
"schemaVersion": 1,
|
|
3017
|
+
"operationId": operation_id,
|
|
3018
|
+
"changeName": change_name,
|
|
3019
|
+
"sourceDir": str(original_change_dir),
|
|
3020
|
+
"archiveDir": str(archive_dir),
|
|
3021
|
+
"finalStatus": "RUNNING",
|
|
3022
|
+
"startedAt": now_iso(),
|
|
3023
|
+
},
|
|
3024
|
+
)
|
|
3025
|
+
except OSError as exc:
|
|
3026
|
+
shutil.rmtree(operation_temp_dir.parent, ignore_errors=True)
|
|
3027
|
+
payload["error"] = f"operation staging failed: {exc}"
|
|
3028
|
+
_restore_finalize_failure()
|
|
3029
|
+
return 1, payload
|
|
3030
|
+
|
|
3031
|
+
work_dir = operation_temp_dir
|
|
2893
3032
|
before_manifest: dict[str, Any] | None = None
|
|
2894
3033
|
|
|
3034
|
+
if split_state_dir is not None and split_state_dir.is_dir():
|
|
3035
|
+
_merge_runtime_state(split_state_dir, work_dir)
|
|
3036
|
+
payload["steps"]["split_state_merge"] = {
|
|
3037
|
+
"ok": True,
|
|
3038
|
+
"stateDir": str(split_state_dir),
|
|
3039
|
+
}
|
|
3040
|
+
|
|
2895
3041
|
def _safe_append(**kwargs: Any) -> None:
|
|
2896
3042
|
nonlocal work_dir
|
|
2897
3043
|
try:
|
|
@@ -2904,7 +3050,7 @@ def cmd_finalize(
|
|
|
2904
3050
|
|
|
2905
3051
|
# --- 0. cleanup transients (before before-manifest) ---
|
|
2906
3052
|
try:
|
|
2907
|
-
cleanup_result = _cleanup_transients(
|
|
3053
|
+
cleanup_result = _cleanup_transients(work_dir)
|
|
2908
3054
|
payload["steps"]["cleanup"] = cleanup_result
|
|
2909
3055
|
deleted_n = len(cleanup_result.get("deleted") or [])
|
|
2910
3056
|
trunc_n = len(cleanup_result.get("truncated") or [])
|
|
@@ -2919,13 +3065,6 @@ def cmd_finalize(
|
|
|
2919
3065
|
warnings.append(f"cleanup failed: {exc}")
|
|
2920
3066
|
payload["steps"]["cleanup"] = {"ok": False, "error": str(exc)}
|
|
2921
3067
|
|
|
2922
|
-
if split_state_dir is not None and split_state_dir.is_dir():
|
|
2923
|
-
_merge_runtime_state(split_state_dir, work_dir)
|
|
2924
|
-
payload["steps"]["split_state_merge"] = {
|
|
2925
|
-
"ok": True,
|
|
2926
|
-
"stateDir": str(split_state_dir),
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
3068
|
# --- 1. before-manifest ---
|
|
2930
3069
|
before_path = work_dir / "evidence" / "archive-manifest-before.json"
|
|
2931
3070
|
try:
|
|
@@ -2957,41 +3096,7 @@ def cmd_finalize(
|
|
|
2957
3096
|
severity="error",
|
|
2958
3097
|
message=str(exc),
|
|
2959
3098
|
)
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
# Sync events/evidence appended after the first materialization.
|
|
2963
|
-
if split_state_dir is not None and split_state_dir.is_dir():
|
|
2964
|
-
_merge_runtime_state(split_state_dir, work_dir)
|
|
2965
|
-
|
|
2966
|
-
# --- 2. move ---
|
|
2967
|
-
try:
|
|
2968
|
-
archive_root.mkdir(parents=True, exist_ok=True)
|
|
2969
|
-
shutil.move(str(original_change_dir), str(archive_dir))
|
|
2970
|
-
work_dir = archive_dir
|
|
2971
|
-
payload["steps"]["move"] = {"ok": True, "to": str(archive_dir)}
|
|
2972
|
-
_safe_append(
|
|
2973
|
-
phase="archive",
|
|
2974
|
-
type_="command",
|
|
2975
|
-
command=f"move → {archive_dir.name}",
|
|
2976
|
-
exit_code=0,
|
|
2977
|
-
note="moved change dir to archive",
|
|
2978
|
-
)
|
|
2979
|
-
except OSError as exc:
|
|
2980
|
-
# Move failed: original intact, stop immediately
|
|
2981
|
-
payload["error"] = f"move failed: {exc}"
|
|
2982
|
-
payload["steps"]["move"] = {"ok": False, "error": str(exc)}
|
|
2983
|
-
payload["original_preserved"] = original_change_dir.is_dir()
|
|
2984
|
-
try:
|
|
2985
|
-
append_event(
|
|
2986
|
-
original_change_dir,
|
|
2987
|
-
phase="archive",
|
|
2988
|
-
type_="issue",
|
|
2989
|
-
code="move-failed",
|
|
2990
|
-
severity="error",
|
|
2991
|
-
message=str(exc),
|
|
2992
|
-
)
|
|
2993
|
-
except OSError:
|
|
2994
|
-
pass
|
|
3099
|
+
_restore_finalize_failure()
|
|
2995
3100
|
return 1, payload
|
|
2996
3101
|
|
|
2997
3102
|
# --- 2b. user-flag decision (archive fact, must precede the cutoff) ---
|
|
@@ -3002,9 +3107,11 @@ def cmd_finalize(
|
|
|
3002
3107
|
note="review missing on full tier (allowed by user)",
|
|
3003
3108
|
)
|
|
3004
3109
|
|
|
3005
|
-
# --- 3.
|
|
3006
|
-
#
|
|
3007
|
-
#
|
|
3110
|
+
# --- 3. candidate phase.end + freeze cutoff (RET-19 freeze-first) ---
|
|
3111
|
+
# This terminal exists only inside the isolated operation staging tree. It
|
|
3112
|
+
# becomes authoritative atomically at publish; validation failure discards
|
|
3113
|
+
# it and _restore_finalize_failure records the real FAIL attempt instead.
|
|
3114
|
+
# From here on, NO event may be appended to the staged events file.
|
|
3008
3115
|
_safe_append(
|
|
3009
3116
|
phase="archive",
|
|
3010
3117
|
type_="phase.end",
|
|
@@ -3056,9 +3163,8 @@ def cmd_finalize(
|
|
|
3056
3163
|
warnings.append(f"could not write sourceConsistency: {exc}")
|
|
3057
3164
|
if not source_result.get("ok"):
|
|
3058
3165
|
payload["issues"] = source_result.get("issues") or []
|
|
3059
|
-
payload["error"] = "source consistency failed;
|
|
3166
|
+
payload["error"] = "source consistency failed; staged operation discarded"
|
|
3060
3167
|
_restore_finalize_failure()
|
|
3061
|
-
_append_finalize_failure_issue(original_change_dir, "source-consistency-failed", payload["error"])
|
|
3062
3168
|
payload["warnings"] = warnings
|
|
3063
3169
|
payload["ok"] = False
|
|
3064
3170
|
return 1, payload
|
|
@@ -3071,7 +3177,6 @@ def cmd_finalize(
|
|
|
3071
3177
|
msg = str(render_result.get("fallbackReason") or "render failed")
|
|
3072
3178
|
payload["error"] = f"final-summary render failed: {msg}"
|
|
3073
3179
|
_restore_finalize_failure()
|
|
3074
|
-
_append_finalize_failure_issue(original_change_dir, "render-failed", msg)
|
|
3075
3180
|
payload["warnings"] = warnings
|
|
3076
3181
|
payload["ok"] = False
|
|
3077
3182
|
return 1, payload
|
|
@@ -3170,17 +3275,30 @@ def cmd_finalize(
|
|
|
3170
3275
|
}
|
|
3171
3276
|
)
|
|
3172
3277
|
payload["issues"] = issues_out
|
|
3173
|
-
payload["error"] = "validate or manifest check failed;
|
|
3278
|
+
payload["error"] = "validate or manifest check failed; staged operation discarded"
|
|
3174
3279
|
payload["steps"]["delete_original"] = {"ok": False, "deleted": False}
|
|
3175
3280
|
_restore_finalize_failure()
|
|
3176
|
-
_append_finalize_failure_issue(
|
|
3177
|
-
original_change_dir, "closure-failed", payload["error"]
|
|
3178
|
-
)
|
|
3179
3281
|
payload["warnings"] = warnings
|
|
3180
3282
|
payload["ok"] = False
|
|
3181
3283
|
return 1, payload
|
|
3182
3284
|
|
|
3183
|
-
#
|
|
3285
|
+
# Publish only after every validator passes. The archive path is never used
|
|
3286
|
+
# as mutable staging, so a failed attempt cannot poison the next retry.
|
|
3287
|
+
try:
|
|
3288
|
+
archive_root.mkdir(parents=True, exist_ok=True)
|
|
3289
|
+
shutil.move(str(operation_temp_dir), str(archive_dir))
|
|
3290
|
+
shutil.rmtree(operation_temp_dir.parent, ignore_errors=True)
|
|
3291
|
+
work_dir = archive_dir
|
|
3292
|
+
summary_path = work_dir / "reports" / "final" / "summary-data.json"
|
|
3293
|
+
html_path = work_dir / "reports" / "final" / "final-summary.html"
|
|
3294
|
+
payload["steps"]["move"] = {"ok": True, "to": str(archive_dir)}
|
|
3295
|
+
except OSError as exc:
|
|
3296
|
+
payload["error"] = f"publish failed: {exc}"
|
|
3297
|
+
payload["steps"]["move"] = {"ok": False, "error": str(exc)}
|
|
3298
|
+
_restore_finalize_failure()
|
|
3299
|
+
return 1, payload
|
|
3300
|
+
|
|
3301
|
+
# Success path: the validated copy is published; now retire its source.
|
|
3184
3302
|
if original_change_dir.exists():
|
|
3185
3303
|
try:
|
|
3186
3304
|
shutil.rmtree(original_change_dir)
|
|
@@ -3192,7 +3310,7 @@ def cmd_finalize(
|
|
|
3192
3310
|
payload["steps"]["delete_original"] = {
|
|
3193
3311
|
"ok": True,
|
|
3194
3312
|
"deleted": True,
|
|
3195
|
-
"note": "
|
|
3313
|
+
"note": "source already absent",
|
|
3196
3314
|
}
|
|
3197
3315
|
|
|
3198
3316
|
if split_state_dir is not None and split_state_dir.is_dir():
|
|
@@ -3222,7 +3340,28 @@ def cmd_finalize(
|
|
|
3222
3340
|
if service_result.get("warning"):
|
|
3223
3341
|
warnings.append(str(service_result["warning"]))
|
|
3224
3342
|
|
|
3343
|
+
try:
|
|
3344
|
+
write_json(
|
|
3345
|
+
operation_record,
|
|
3346
|
+
{
|
|
3347
|
+
"schemaVersion": 1,
|
|
3348
|
+
"operationId": operation_id,
|
|
3349
|
+
"changeName": change_name,
|
|
3350
|
+
"sourceDir": str(original_change_dir),
|
|
3351
|
+
"archiveDir": str(archive_dir),
|
|
3352
|
+
"finalStatus": "OK",
|
|
3353
|
+
"publishedAt": now_iso(),
|
|
3354
|
+
"summarySha256": sha256_file(summary_path),
|
|
3355
|
+
"manifestSha256": sha256_file(
|
|
3356
|
+
archive_dir / "evidence" / "archive-manifest-after.json"
|
|
3357
|
+
),
|
|
3358
|
+
},
|
|
3359
|
+
)
|
|
3360
|
+
except OSError as exc:
|
|
3361
|
+
warnings.append(f"could not persist completed archive operation: {exc}")
|
|
3362
|
+
|
|
3225
3363
|
payload["ok"] = True
|
|
3364
|
+
payload["finalStatus"] = "OK"
|
|
3226
3365
|
payload["warnings"] = warnings
|
|
3227
3366
|
payload["summary_data"] = str(summary_path)
|
|
3228
3367
|
payload["final_summary"] = str(html_path) if html_path.is_file() else None
|
|
@@ -3422,6 +3561,21 @@ def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
|
3422
3561
|
if not summary_path.is_file():
|
|
3423
3562
|
payload["error"] = "summary-data.json missing; cannot repair"
|
|
3424
3563
|
return 1, payload
|
|
3564
|
+
frozen_manifest_paths = {
|
|
3565
|
+
name: archive_dir / "evidence" / name
|
|
3566
|
+
for name in (
|
|
3567
|
+
"archive-manifest-before.json",
|
|
3568
|
+
"archive-manifest-after.json",
|
|
3569
|
+
)
|
|
3570
|
+
}
|
|
3571
|
+
missing_manifests = [
|
|
3572
|
+
name for name, path in frozen_manifest_paths.items() if not path.is_file()
|
|
3573
|
+
]
|
|
3574
|
+
if missing_manifests:
|
|
3575
|
+
payload["error"] = (
|
|
3576
|
+
"frozen manifests missing; cannot repair: " + ", ".join(missing_manifests)
|
|
3577
|
+
)
|
|
3578
|
+
return 1, payload
|
|
3425
3579
|
|
|
3426
3580
|
# 1. candidate: fresh collect from frozen sources (read-only on archive).
|
|
3427
3581
|
try:
|
|
@@ -3462,22 +3616,30 @@ def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
|
3462
3616
|
]
|
|
3463
3617
|
version = f"v{(max(existing) + 1) if existing else 1}"
|
|
3464
3618
|
version_dir = derived / version
|
|
3465
|
-
|
|
3466
|
-
|
|
3619
|
+
staged_version_dir = staging / version
|
|
3620
|
+
staged_version_dir.mkdir()
|
|
3621
|
+
final_summary = staged_version_dir / "summary-data.json"
|
|
3467
3622
|
write_json(final_summary, candidate)
|
|
3623
|
+
frozen_manifest_hashes: dict[str, str] = {}
|
|
3624
|
+
for manifest_name, source_manifest in frozen_manifest_paths.items():
|
|
3625
|
+
target_manifest = staged_version_dir / manifest_name
|
|
3626
|
+
shutil.copy2(source_manifest, target_manifest)
|
|
3627
|
+
frozen_manifest_hashes[manifest_name] = "sha256:" + sha256_file(target_manifest)
|
|
3468
3628
|
if staged_html.is_file():
|
|
3469
|
-
shutil.copy2(staged_html,
|
|
3629
|
+
shutil.copy2(staged_html, staged_version_dir / "final-summary.html")
|
|
3470
3630
|
record = {
|
|
3471
3631
|
"version": version,
|
|
3472
3632
|
"createdAt": now_iso(),
|
|
3473
3633
|
"summarySha256": "sha256:" + hashlib.sha256(final_summary.read_bytes()).hexdigest(),
|
|
3474
3634
|
"baseSummarySha256": "sha256:" + hashlib.sha256(summary_path.read_bytes()).hexdigest(),
|
|
3635
|
+
"frozenManifestHashes": frozen_manifest_hashes,
|
|
3475
3636
|
"validators": {
|
|
3476
3637
|
"source": {"ok": bool(source_result.get("ok")), "issues": source_result.get("issues") or []},
|
|
3477
3638
|
"renderer": {"ok": bool(renderer_result.get("ok")), "issues": renderer_result.get("issues") or []},
|
|
3478
3639
|
},
|
|
3479
3640
|
}
|
|
3480
|
-
write_json(
|
|
3641
|
+
write_json(staged_version_dir / "repair-record.json", record)
|
|
3642
|
+
shutil.move(str(staged_version_dir), str(version_dir))
|
|
3481
3643
|
|
|
3482
3644
|
# 4. authoritative pointer — only after both validators passed.
|
|
3483
3645
|
write_json(
|