@hunter-harness/workflow-harness 0.2.8 → 0.2.9
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/workflow-policy.json +3 -3
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-archive/reference.md +18 -10
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
- 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 +5 -0
- package/harness/bundles/general/claude-code/harness-run/reference.md +31 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/checklist.md +9 -0
- package/harness/bundles/general/claude-code/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +36 -0
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/claude-code/scripts/harness_events.py +70 -19
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +18 -10
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
- 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 +5 -0
- package/harness/bundles/general/codebuddy/harness-run/reference.md +31 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +9 -0
- package/harness/bundles/general/codebuddy/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +36 -0
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +70 -19
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-archive/reference.md +18 -10
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
- 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 +5 -0
- package/harness/bundles/general/codex/harness-run/reference.md +31 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/checklist.md +9 -0
- package/harness/bundles/general/codex/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/codex/harness-test/reference.md +36 -0
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/codex/scripts/harness_events.py +70 -19
- package/harness/bundles/general/codex/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/codex/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-archive/reference.md +18 -10
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
- 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 +5 -0
- package/harness/bundles/general/cursor/harness-run/reference.md +31 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/checklist.md +9 -0
- package/harness/bundles/general/cursor/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +36 -0
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/cursor/scripts/harness_events.py +70 -19
- package/harness/bundles/general/cursor/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-archive/reference.md +18 -10
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
- 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/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/claude-code/scripts/harness_events.py +70 -19
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +18 -10
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
- 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/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +70 -19
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-archive/reference.md +18 -10
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
- 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/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/codex/scripts/harness_events.py +70 -19
- package/harness/bundles/java/codex/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/codex/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-archive/reference.md +18 -10
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +5 -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-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
- 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/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/cursor/scripts/harness_events.py +70 -19
- package/harness/bundles/java/cursor/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +17 -0
- package/harness/manifests/general/claude-code.json +32 -28
- package/harness/manifests/general/codebuddy.json +32 -28
- package/harness/manifests/general/codex.json +32 -28
- package/harness/manifests/general/cursor.json +32 -28
- package/harness/manifests/java/claude-code.json +26 -26
- package/harness/manifests/java/codebuddy.json +26 -26
- package/harness/manifests/java/codex.json +26 -26
- package/harness/manifests/java/cursor.json +26 -26
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -57,6 +57,12 @@ MANIFEST_COMPARE_EXCLUDE = frozenset(
|
|
|
57
57
|
SCHEMA_VERSION = "2.2"
|
|
58
58
|
NOT_AVAILABLE = "not_available"
|
|
59
59
|
|
|
60
|
+
# Compiled once for evidence-text count fallbacks in _ledger_unit_tests / _ledger_api_tests.
|
|
61
|
+
_RE_UNIT_COUNTS = re.compile(
|
|
62
|
+
r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)"
|
|
63
|
+
)
|
|
64
|
+
_RE_API_PASSED = re.compile(r"(\d+)/(\d+)\s*passed", re.I)
|
|
65
|
+
|
|
60
66
|
# Ensure sibling harness_events is importable.
|
|
61
67
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
62
68
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
@@ -429,7 +435,11 @@ def worktree_requested(change_dir: Path) -> bool:
|
|
|
429
435
|
# ---------------------------------------------------------------------------
|
|
430
436
|
|
|
431
437
|
|
|
432
|
-
def check_status(
|
|
438
|
+
def check_status(
|
|
439
|
+
change_dir: Path,
|
|
440
|
+
*,
|
|
441
|
+
allow_missing_review: bool = False,
|
|
442
|
+
) -> dict[str, Any]:
|
|
433
443
|
"""Read-only archive preconditions. Never mutates."""
|
|
434
444
|
blockers: list[dict[str, str]] = []
|
|
435
445
|
warnings: list[dict[str, str]] = []
|
|
@@ -574,6 +584,10 @@ def check_status(change_dir: Path) -> dict[str, Any]:
|
|
|
574
584
|
}
|
|
575
585
|
)
|
|
576
586
|
|
|
587
|
+
gate_policy = load_gate_policy(change_dir)
|
|
588
|
+
risk_tier = str((gate_policy or {}).get("tier") or "unknown")
|
|
589
|
+
checks["riskTier"] = risk_tier
|
|
590
|
+
|
|
577
591
|
if review_reports:
|
|
578
592
|
checks["review_report_status"] = "present"
|
|
579
593
|
elif review_phase_completed(events):
|
|
@@ -589,12 +603,23 @@ def check_status(change_dir: Path) -> dict[str, Any]:
|
|
|
589
603
|
)
|
|
590
604
|
else:
|
|
591
605
|
checks["review_report_status"] = "not-run"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
606
|
+
review_msg = {
|
|
607
|
+
"code": "review-not-run",
|
|
608
|
+
"message": "no review report; mark reviewSummary as ADVISORY_NOT_RUN",
|
|
609
|
+
}
|
|
610
|
+
warnings.append(review_msg)
|
|
611
|
+
if risk_tier == "full" and not review_ran:
|
|
612
|
+
tier_issue = {
|
|
613
|
+
"code": "review-required-on-full-tier",
|
|
614
|
+
"message": (
|
|
615
|
+
"gate-policy tier=full requires review evidence; "
|
|
616
|
+
"pass --allow-missing-review to override"
|
|
617
|
+
),
|
|
596
618
|
}
|
|
597
|
-
|
|
619
|
+
if allow_missing_review:
|
|
620
|
+
warnings.append(tier_issue)
|
|
621
|
+
else:
|
|
622
|
+
blockers.append(tier_issue)
|
|
598
623
|
|
|
599
624
|
archivable = len(blockers) == 0
|
|
600
625
|
return {
|
|
@@ -637,33 +662,80 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
637
662
|
if not ledger:
|
|
638
663
|
return empty
|
|
639
664
|
validations = ledger.get("validations") or ledger.get("verification") or {}
|
|
640
|
-
unit =
|
|
665
|
+
unit = (
|
|
666
|
+
validations.get("unitTestFull")
|
|
667
|
+
or validations.get("unitTest")
|
|
668
|
+
or validations.get("unitTests")
|
|
669
|
+
or {}
|
|
670
|
+
)
|
|
641
671
|
if not isinstance(unit, dict):
|
|
642
672
|
return empty
|
|
643
673
|
status = str(unit.get("status") or "").upper()
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
674
|
+
|
|
675
|
+
run = failures = errors = skipped = 0
|
|
676
|
+
pass_rate: Any = None
|
|
677
|
+
source = "committed"
|
|
678
|
+
counted = False
|
|
679
|
+
|
|
680
|
+
metrics = unit.get("metrics")
|
|
681
|
+
if isinstance(metrics, dict) and any(
|
|
682
|
+
k in metrics for k in ("run", "testsRun", "failures", "errors", "skipped")
|
|
683
|
+
):
|
|
684
|
+
run = int(metrics.get("run", metrics.get("testsRun", 0)) or 0)
|
|
685
|
+
failures = int(metrics.get("failures", 0) or 0)
|
|
686
|
+
errors = int(metrics.get("errors", 0) or 0)
|
|
687
|
+
skipped = int(metrics.get("skipped", 0) or 0)
|
|
688
|
+
pass_rate = metrics.get("passRate")
|
|
689
|
+
source = "committed"
|
|
690
|
+
counted = run > 0 or failures > 0 or errors > 0 or skipped > 0
|
|
691
|
+
|
|
692
|
+
evidence = unit.get("evidence")
|
|
693
|
+
if not counted and isinstance(evidence, dict):
|
|
694
|
+
run = int(
|
|
695
|
+
evidence.get(
|
|
696
|
+
"run",
|
|
697
|
+
evidence.get("testsRun", unit.get("run", unit.get("testsRun", 0))),
|
|
698
|
+
)
|
|
699
|
+
or 0
|
|
649
700
|
)
|
|
650
|
-
failures = evidence.get("failures", unit.get("failures", 0))
|
|
651
|
-
errors = evidence.get("errors", unit.get("errors", 0))
|
|
652
|
-
skipped = evidence.get("skipped", unit.get("skipped", 0))
|
|
701
|
+
failures = int(evidence.get("failures", unit.get("failures", 0)) or 0)
|
|
702
|
+
errors = int(evidence.get("errors", unit.get("errors", 0)) or 0)
|
|
703
|
+
skipped = int(evidence.get("skipped", unit.get("skipped", 0)) or 0)
|
|
653
704
|
pass_rate = evidence.get("passRate") or unit.get("passRate")
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
705
|
+
source = "committed"
|
|
706
|
+
counted = run > 0 or failures > 0 or errors > 0 or skipped > 0
|
|
707
|
+
|
|
708
|
+
evidence_text = evidence if isinstance(evidence, str) else ""
|
|
709
|
+
if not counted and evidence_text:
|
|
710
|
+
matches = list(_RE_UNIT_COUNTS.finditer(evidence_text))
|
|
711
|
+
if matches:
|
|
712
|
+
m = matches[-1]
|
|
713
|
+
run = int(m.group(1))
|
|
714
|
+
failures = int(m.group(2))
|
|
715
|
+
errors = int(m.group(3))
|
|
716
|
+
skipped = int(m.group(4))
|
|
717
|
+
source = "evidence-text"
|
|
718
|
+
counted = True
|
|
719
|
+
|
|
720
|
+
if not counted:
|
|
721
|
+
run = int(unit.get("run", unit.get("testsRun", 0)) or 0)
|
|
722
|
+
failures = int(unit.get("failures", 0) or 0)
|
|
723
|
+
errors = int(unit.get("errors", 0) or 0)
|
|
724
|
+
skipped = int(unit.get("skipped", 0) or 0)
|
|
659
725
|
pass_rate = unit.get("passRate")
|
|
726
|
+
source = "committed"
|
|
660
727
|
|
|
661
|
-
source = "committed"
|
|
662
728
|
if status in {"NOT_RUN", "SKIPPED", "USER_SKIPPED"}:
|
|
663
729
|
source = "not-run"
|
|
664
730
|
elif str(unit.get("reused") or "").lower() in {"true", "1"} or "REUSED" in status:
|
|
665
731
|
source = "committed"
|
|
666
732
|
|
|
733
|
+
if pass_rate is None and run > 0:
|
|
734
|
+
passed = max(run - failures - errors, 0)
|
|
735
|
+
pass_rate = f"{passed / run:.0%}"
|
|
736
|
+
elif pass_rate is None:
|
|
737
|
+
pass_rate = NOT_AVAILABLE
|
|
738
|
+
|
|
667
739
|
result = {
|
|
668
740
|
"run": int(run or 0),
|
|
669
741
|
"failures": int(failures or 0),
|
|
@@ -677,7 +749,11 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
677
749
|
return result
|
|
678
750
|
|
|
679
751
|
|
|
680
|
-
def _ledger_api_tests(
|
|
752
|
+
def _ledger_api_tests(
|
|
753
|
+
ledger: dict[str, Any] | None,
|
|
754
|
+
*,
|
|
755
|
+
change_dir: Path | None = None,
|
|
756
|
+
) -> dict[str, Any]:
|
|
681
757
|
empty = {
|
|
682
758
|
"status": "NOT_RUN",
|
|
683
759
|
"total": 0,
|
|
@@ -685,6 +761,7 @@ def _ledger_api_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
685
761
|
"failed": 0,
|
|
686
762
|
"blocked": 0,
|
|
687
763
|
"passRate": NOT_AVAILABLE,
|
|
764
|
+
"source": "not-run",
|
|
688
765
|
}
|
|
689
766
|
if not ledger:
|
|
690
767
|
return empty
|
|
@@ -697,12 +774,76 @@ def _ledger_api_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
697
774
|
status = "OK"
|
|
698
775
|
elif status in {"SKIP", "SKIPPED"}:
|
|
699
776
|
status = "USER_SKIPPED"
|
|
700
|
-
|
|
701
|
-
total =
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
777
|
+
|
|
778
|
+
total = passed = failed = blocked = 0
|
|
779
|
+
pass_rate: Any = None
|
|
780
|
+
source = "committed"
|
|
781
|
+
counted = False
|
|
782
|
+
|
|
783
|
+
metrics = api.get("metrics")
|
|
784
|
+
if isinstance(metrics, dict) and any(
|
|
785
|
+
k in metrics for k in ("total", "passed", "failed", "blocked")
|
|
786
|
+
):
|
|
787
|
+
total = int(metrics.get("total", 0) or 0)
|
|
788
|
+
passed = int(metrics.get("passed", 0) or 0)
|
|
789
|
+
failed = int(metrics.get("failed", 0) or 0)
|
|
790
|
+
blocked = int(metrics.get("blocked", 0) or 0)
|
|
791
|
+
pass_rate = metrics.get("passRate")
|
|
792
|
+
source = "committed"
|
|
793
|
+
counted = total > 0 or passed > 0 or failed > 0 or blocked > 0
|
|
794
|
+
|
|
795
|
+
evidence_raw = api.get("evidence")
|
|
796
|
+
if not counted and isinstance(evidence_raw, dict):
|
|
797
|
+
total = int(evidence_raw.get("total", api.get("total", 0)) or 0)
|
|
798
|
+
passed = int(evidence_raw.get("passed", api.get("passed", 0)) or 0)
|
|
799
|
+
failed = int(evidence_raw.get("failed", api.get("failed", 0)) or 0)
|
|
800
|
+
blocked = int(evidence_raw.get("blocked", api.get("blocked", 0)) or 0)
|
|
801
|
+
pass_rate = evidence_raw.get("passRate") or api.get("passRate")
|
|
802
|
+
source = "committed"
|
|
803
|
+
counted = total > 0 or passed > 0 or failed > 0 or blocked > 0
|
|
804
|
+
|
|
805
|
+
evidence_text = evidence_raw if isinstance(evidence_raw, str) else ""
|
|
806
|
+
if not counted and evidence_text:
|
|
807
|
+
matches = list(_RE_API_PASSED.finditer(evidence_text))
|
|
808
|
+
if matches:
|
|
809
|
+
m = matches[-1]
|
|
810
|
+
passed = int(m.group(1))
|
|
811
|
+
total = int(m.group(2))
|
|
812
|
+
failed = max(total - passed, 0)
|
|
813
|
+
blocked = 0
|
|
814
|
+
source = "evidence-text"
|
|
815
|
+
counted = True
|
|
816
|
+
|
|
817
|
+
if not counted and change_dir is not None:
|
|
818
|
+
results_path = change_dir / "runtime" / "api-test-results.json"
|
|
819
|
+
if results_path.is_file():
|
|
820
|
+
try:
|
|
821
|
+
raw = read_json(results_path)
|
|
822
|
+
except (OSError, json.JSONDecodeError):
|
|
823
|
+
raw = None
|
|
824
|
+
if isinstance(raw, dict) and all(
|
|
825
|
+
isinstance(raw.get(k), int) for k in ("total", "passed", "failed", "blocked")
|
|
826
|
+
):
|
|
827
|
+
total = int(raw["total"])
|
|
828
|
+
passed = int(raw["passed"])
|
|
829
|
+
failed = int(raw["failed"])
|
|
830
|
+
blocked = int(raw["blocked"])
|
|
831
|
+
source = "api-test-results"
|
|
832
|
+
counted = True
|
|
833
|
+
|
|
834
|
+
if not counted:
|
|
835
|
+
total = int(api.get("total", 0) or 0)
|
|
836
|
+
passed = int(api.get("passed", 0) or 0)
|
|
837
|
+
failed = int(api.get("failed", 0) or 0)
|
|
838
|
+
blocked = int(api.get("blocked", 0) or 0)
|
|
839
|
+
pass_rate = api.get("passRate")
|
|
840
|
+
source = "committed"
|
|
841
|
+
|
|
842
|
+
if pass_rate is None and total > 0:
|
|
843
|
+
pass_rate = f"{passed / total:.0%}"
|
|
844
|
+
elif pass_rate is None:
|
|
845
|
+
pass_rate = NOT_AVAILABLE
|
|
846
|
+
|
|
706
847
|
return {
|
|
707
848
|
"status": status,
|
|
708
849
|
"total": int(total or 0),
|
|
@@ -710,6 +851,7 @@ def _ledger_api_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
|
710
851
|
"failed": int(failed or 0),
|
|
711
852
|
"blocked": int(blocked or 0),
|
|
712
853
|
"passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
|
|
854
|
+
"source": source,
|
|
713
855
|
}
|
|
714
856
|
|
|
715
857
|
|
|
@@ -977,7 +1119,7 @@ def _stage_status_from_sources(
|
|
|
977
1119
|
elif phase in status and sev in {"warn", "warning"} and status[phase] == "OK":
|
|
978
1120
|
status[phase] = "WARN"
|
|
979
1121
|
|
|
980
|
-
api = _ledger_api_tests(ledger)
|
|
1122
|
+
api = _ledger_api_tests(ledger, change_dir=change_dir)
|
|
981
1123
|
db = _ledger_db_compat(ledger)
|
|
982
1124
|
api_status = str(api.get("status") or "").upper()
|
|
983
1125
|
if api_status in {"FAIL", "FAILED", "ERROR"} or int(api.get("failed") or 0) > 0:
|
|
@@ -1002,28 +1144,266 @@ def _stage_status_from_sources(
|
|
|
1002
1144
|
def _compute_final_status(
|
|
1003
1145
|
stage_status: dict[str, str],
|
|
1004
1146
|
verification: dict[str, Any],
|
|
1005
|
-
) -> str:
|
|
1147
|
+
) -> tuple[str, list[str]]:
|
|
1006
1148
|
api = verification.get("apiTests") or {}
|
|
1007
1149
|
db = str(verification.get("dbCompatibility") or "")
|
|
1008
1150
|
api_status = str(api.get("status") or "")
|
|
1009
|
-
|
|
1151
|
+
reasons: list[str] = []
|
|
1152
|
+
for phase, v in stage_status.items():
|
|
1010
1153
|
if v == "FAIL":
|
|
1011
|
-
return "FAIL"
|
|
1154
|
+
return "FAIL", [f"stage {phase}=FAIL"]
|
|
1012
1155
|
unit = verification.get("unitTests") or {}
|
|
1013
|
-
if int(unit.get("failures") or 0) > 0
|
|
1014
|
-
return "FAIL"
|
|
1156
|
+
if int(unit.get("failures") or 0) > 0:
|
|
1157
|
+
return "FAIL", [f"unitTests.failures={unit.get('failures')}"]
|
|
1158
|
+
if int(unit.get("errors") or 0) > 0:
|
|
1159
|
+
return "FAIL", [f"unitTests.errors={unit.get('errors')}"]
|
|
1015
1160
|
if int(api.get("failed") or 0) > 0:
|
|
1016
|
-
return "FAIL"
|
|
1161
|
+
return "FAIL", [f"apiTests.failed={api.get('failed')}"]
|
|
1017
1162
|
conditional = {
|
|
1018
1163
|
"USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
|
|
1019
1164
|
"NOT_RUN", "PARTIAL",
|
|
1020
1165
|
}
|
|
1021
|
-
if api_status in conditional
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1166
|
+
if api_status in conditional:
|
|
1167
|
+
reasons.append(f"apiTests.status={api_status}")
|
|
1168
|
+
if db in conditional:
|
|
1169
|
+
reasons.append(f"dbCompatibility={db}")
|
|
1170
|
+
if reasons:
|
|
1171
|
+
return "CONDITIONAL_OK", reasons
|
|
1172
|
+
for phase, v in stage_status.items():
|
|
1173
|
+
if v == "WARN":
|
|
1174
|
+
return "WARN", [f"stage {phase}=WARN"]
|
|
1175
|
+
if v in conditional:
|
|
1176
|
+
return "CONDITIONAL_OK", [f"stage {phase}={v}"]
|
|
1177
|
+
return "OK", []
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def load_gate_policy(change_dir: Path) -> dict[str, Any] | None:
|
|
1181
|
+
path = change_dir / "meta" / "gate-policy.json"
|
|
1182
|
+
if not path.is_file():
|
|
1183
|
+
return None
|
|
1184
|
+
try:
|
|
1185
|
+
data = read_json(path)
|
|
1186
|
+
except (OSError, json.JSONDecodeError):
|
|
1187
|
+
return None
|
|
1188
|
+
if not isinstance(data, dict) or data.get("schemaVersion") != 1:
|
|
1189
|
+
return None
|
|
1190
|
+
return data
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
_RISK_SEVERITIES = frozenset({"warning", "error", "critical"})
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _cleanup_transients(work_dir: Path) -> dict[str, Any]:
|
|
1197
|
+
"""Delete lock/pid/launcher/credential files; truncate oversized logs."""
|
|
1198
|
+
deleted: list[str] = []
|
|
1199
|
+
truncated: list[dict[str, Any]] = []
|
|
1200
|
+
|
|
1201
|
+
def _rel(path: Path) -> str:
|
|
1202
|
+
try:
|
|
1203
|
+
return path.relative_to(work_dir).as_posix()
|
|
1204
|
+
except ValueError:
|
|
1205
|
+
return str(path)
|
|
1206
|
+
|
|
1207
|
+
lock = work_dir / "events.ndjson.lock"
|
|
1208
|
+
if lock.is_file():
|
|
1209
|
+
try:
|
|
1210
|
+
lock.unlink()
|
|
1211
|
+
deleted.append(_rel(lock))
|
|
1212
|
+
except OSError:
|
|
1213
|
+
pass
|
|
1214
|
+
|
|
1215
|
+
runtime = work_dir / "runtime"
|
|
1216
|
+
if runtime.is_dir():
|
|
1217
|
+
for path in sorted(runtime.iterdir()):
|
|
1218
|
+
if not path.is_file():
|
|
1219
|
+
continue
|
|
1220
|
+
name = path.name
|
|
1221
|
+
drop = False
|
|
1222
|
+
if name.endswith(".pid"):
|
|
1223
|
+
drop = True
|
|
1224
|
+
elif name in {
|
|
1225
|
+
"_harness_service_launcher.py",
|
|
1226
|
+
"_harness_service.command.txt",
|
|
1227
|
+
}:
|
|
1228
|
+
drop = True
|
|
1229
|
+
elif re.search(r"credential|token|secret", name, re.I):
|
|
1230
|
+
drop = True
|
|
1231
|
+
if drop:
|
|
1232
|
+
try:
|
|
1233
|
+
path.unlink()
|
|
1234
|
+
deleted.append(_rel(path))
|
|
1235
|
+
except OSError:
|
|
1236
|
+
pass
|
|
1237
|
+
|
|
1238
|
+
logs_root = work_dir / "logs"
|
|
1239
|
+
if logs_root.is_dir():
|
|
1240
|
+
for path in sorted(logs_root.rglob("*.log")):
|
|
1241
|
+
if not path.is_file():
|
|
1242
|
+
continue
|
|
1243
|
+
try:
|
|
1244
|
+
size = path.stat().st_size
|
|
1245
|
+
except OSError:
|
|
1246
|
+
continue
|
|
1247
|
+
if size <= 65536:
|
|
1248
|
+
continue
|
|
1249
|
+
try:
|
|
1250
|
+
with path.open("rb") as handle:
|
|
1251
|
+
handle.seek(-65536, 2)
|
|
1252
|
+
tail = handle.read()
|
|
1253
|
+
header = (
|
|
1254
|
+
f"# [truncated by harness-archive finalize: original {size} bytes]\n"
|
|
1255
|
+
).encode("utf-8")
|
|
1256
|
+
path.write_bytes(header + tail)
|
|
1257
|
+
truncated.append({"path": _rel(path), "originalBytes": size})
|
|
1258
|
+
except OSError:
|
|
1259
|
+
pass
|
|
1260
|
+
|
|
1261
|
+
return {"deleted": deleted, "truncated": truncated}
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def write_archive_meta(work_dir: Path, summary: dict[str, Any]) -> Path:
|
|
1265
|
+
"""Generate meta/archive-meta.md from summary-data (single ownership)."""
|
|
1266
|
+
archive_id = work_dir.name
|
|
1267
|
+
change_name = str(summary.get("changeName") or work_dir.name)
|
|
1268
|
+
archived_at = dt.datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
1269
|
+
final_status = str(summary.get("finalStatus") or "UNKNOWN")
|
|
1270
|
+
lines = [
|
|
1271
|
+
"---",
|
|
1272
|
+
f"archive-id: {archive_id}",
|
|
1273
|
+
f"change-name: {change_name}",
|
|
1274
|
+
f"archived-at: {archived_at}",
|
|
1275
|
+
f"final-commit: {summary.get('finalCommit') or ''}",
|
|
1276
|
+
f"base-commit: {summary.get('baseCommit') or ''}",
|
|
1277
|
+
f"final-status: {final_status}",
|
|
1278
|
+
"source: harness-archive",
|
|
1279
|
+
"---",
|
|
1280
|
+
f"# 归档元数据 — {change_name}",
|
|
1281
|
+
"",
|
|
1282
|
+
"## 阶段状态",
|
|
1283
|
+
"",
|
|
1284
|
+
"| 阶段 | 状态 |",
|
|
1285
|
+
"|---|---|",
|
|
1286
|
+
]
|
|
1287
|
+
for stage, status in (summary.get("stageStatus") or {}).items():
|
|
1288
|
+
lines.append(f"| {stage} | {status} |")
|
|
1289
|
+
lines.extend(["", "## 变更文件", "", "| 路径 | + | - |", "|---|---|---|"])
|
|
1290
|
+
changed = summary.get("changedFiles") or []
|
|
1291
|
+
if changed:
|
|
1292
|
+
for item in changed:
|
|
1293
|
+
lines.append(
|
|
1294
|
+
f"| {item.get('path') or ''} | "
|
|
1295
|
+
f"{item.get('insertions', 0)} | {item.get('deletions', 0)} |"
|
|
1296
|
+
)
|
|
1297
|
+
else:
|
|
1298
|
+
lines.append("| (无) | | |")
|
|
1299
|
+
lines.extend(["", "## 已知风险", ""])
|
|
1300
|
+
risks = summary.get("knownRisks") or []
|
|
1301
|
+
if risks:
|
|
1302
|
+
for risk in risks:
|
|
1303
|
+
if isinstance(risk, dict):
|
|
1304
|
+
lines.append(
|
|
1305
|
+
f"- [{risk.get('severity') or 'unknown'}] "
|
|
1306
|
+
f"{risk.get('message') or risk}"
|
|
1307
|
+
)
|
|
1308
|
+
else:
|
|
1309
|
+
lines.append(f"- {risk}")
|
|
1310
|
+
else:
|
|
1311
|
+
lines.append("无")
|
|
1312
|
+
lines.append("")
|
|
1313
|
+
out = work_dir / "meta" / "archive-meta.md"
|
|
1314
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
1315
|
+
out.write_text("\n".join(lines), encoding="utf-8", newline="\n")
|
|
1316
|
+
return out
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _patch_archive_stage(
|
|
1320
|
+
summary_path: Path,
|
|
1321
|
+
work_dir: Path,
|
|
1322
|
+
*,
|
|
1323
|
+
render: bool = True,
|
|
1324
|
+
) -> None:
|
|
1325
|
+
"""Backfill archive stage duration/status from events (optionally re-render).
|
|
1326
|
+
|
|
1327
|
+
When called from finalize step 9, pass ``render=False`` so the caller can
|
|
1328
|
+
write archive-meta + remanifest and invoke ``render_final_summary`` once.
|
|
1329
|
+
"""
|
|
1330
|
+
events_file = he.events_path(work_dir)
|
|
1331
|
+
events = he.load_events(events_file) if events_file.is_file() else []
|
|
1332
|
+
event_summary = he.build_summary(work_dir, events)
|
|
1333
|
+
archive_info = (event_summary.get("phases") or {}).get("archive") or {}
|
|
1334
|
+
duration_ms = archive_info.get("duration_ms")
|
|
1335
|
+
status = str(archive_info.get("status") or "OK").upper()
|
|
1336
|
+
if status in {"PASS", "PASSED", "SUCCESS"}:
|
|
1337
|
+
status = "OK"
|
|
1338
|
+
elif status in {"FAILED", "ERROR"}:
|
|
1339
|
+
status = "FAIL"
|
|
1340
|
+
|
|
1341
|
+
summary = read_json(summary_path)
|
|
1342
|
+
stage_status = summary.setdefault("stageStatus", {})
|
|
1343
|
+
if isinstance(stage_status, dict):
|
|
1344
|
+
stage_status["archive"] = status
|
|
1345
|
+
|
|
1346
|
+
durations = summary.setdefault("durations", {})
|
|
1347
|
+
stages = list(durations.get("stages") or [])
|
|
1348
|
+
minutes = round((duration_ms or 0) / 60000, 2) if duration_ms is not None else 0
|
|
1349
|
+
archive_stage = {
|
|
1350
|
+
"stage": "archive",
|
|
1351
|
+
"skill": "harness-archive",
|
|
1352
|
+
"startedAt": archive_info.get("started_at") or NOT_AVAILABLE,
|
|
1353
|
+
"endedAt": archive_info.get("ended_at") or NOT_AVAILABLE,
|
|
1354
|
+
"minutes": minutes,
|
|
1355
|
+
"result": status,
|
|
1356
|
+
"attempts": archive_info.get("attempts") if isinstance(archive_info.get("attempts"), list) else [],
|
|
1357
|
+
}
|
|
1358
|
+
replaced = False
|
|
1359
|
+
for idx, stage in enumerate(stages):
|
|
1360
|
+
if str(stage.get("stage") or "").lower() == "archive":
|
|
1361
|
+
stages[idx] = archive_stage
|
|
1362
|
+
replaced = True
|
|
1363
|
+
break
|
|
1364
|
+
if not replaced:
|
|
1365
|
+
stages.append(archive_stage)
|
|
1366
|
+
durations["stages"] = stages
|
|
1367
|
+
total_min = round(sum(float(s.get("minutes") or 0) for s in stages), 2)
|
|
1368
|
+
durations["totalMinutes"] = total_min
|
|
1369
|
+
durations["totalLabel"] = f"约 {int(round(total_min))} 分"
|
|
1370
|
+
|
|
1371
|
+
timeline = list(summary.get("timeline") or [])
|
|
1372
|
+
for item in timeline:
|
|
1373
|
+
if item.get("phase") == "archive" and item.get("type") not in {"decision", "issue"}:
|
|
1374
|
+
item["durationMs"] = duration_ms
|
|
1375
|
+
item["status"] = status
|
|
1376
|
+
item["endedAt"] = archive_info.get("ended_at")
|
|
1377
|
+
item["startedAt"] = archive_info.get("started_at")
|
|
1378
|
+
# Ensure at least one archive timeline entry with duration
|
|
1379
|
+
if duration_ms is not None and not any(
|
|
1380
|
+
t.get("phase") == "archive" and t.get("durationMs") is not None for t in timeline
|
|
1381
|
+
):
|
|
1382
|
+
timeline.append(
|
|
1383
|
+
{
|
|
1384
|
+
"phase": "archive",
|
|
1385
|
+
"attempt": 1,
|
|
1386
|
+
"startedAt": archive_info.get("started_at"),
|
|
1387
|
+
"endedAt": archive_info.get("ended_at"),
|
|
1388
|
+
"durationMs": duration_ms,
|
|
1389
|
+
"status": status,
|
|
1390
|
+
}
|
|
1391
|
+
)
|
|
1392
|
+
summary["timeline"] = timeline
|
|
1393
|
+
|
|
1394
|
+
skill_calls = list(summary.get("skillCalls") or [])
|
|
1395
|
+
found_skill = False
|
|
1396
|
+
for call in skill_calls:
|
|
1397
|
+
if str(call.get("skill") or "") in {"harness-archive", "archive"}:
|
|
1398
|
+
call["result"] = status
|
|
1399
|
+
found_skill = True
|
|
1400
|
+
if not found_skill:
|
|
1401
|
+
skill_calls.append({"skill": "harness-archive", "count": 1, "result": status})
|
|
1402
|
+
summary["skillCalls"] = skill_calls
|
|
1403
|
+
|
|
1404
|
+
write_json(summary_path, summary)
|
|
1405
|
+
if render:
|
|
1406
|
+
render_final_summary(work_dir, summary_path)
|
|
1027
1407
|
|
|
1028
1408
|
|
|
1029
1409
|
def _changed_files_from_git(
|
|
@@ -1298,7 +1678,7 @@ def collect_summary_data(
|
|
|
1298
1678
|
# verification
|
|
1299
1679
|
if not for_replay or not isinstance(data.get("verification"), dict):
|
|
1300
1680
|
unit = _ledger_unit_tests(ledger)
|
|
1301
|
-
api = _ledger_api_tests(ledger)
|
|
1681
|
+
api = _ledger_api_tests(ledger, change_dir=change_dir)
|
|
1302
1682
|
db = _ledger_db_compat(ledger)
|
|
1303
1683
|
if not find_test_reports(change_dir) and unit.get("run", 0) == 0:
|
|
1304
1684
|
if "status" not in unit:
|
|
@@ -1326,10 +1706,17 @@ def collect_summary_data(
|
|
|
1326
1706
|
if not for_replay or not isinstance(data.get("stageStatus"), dict):
|
|
1327
1707
|
data["stageStatus"] = _stage_status_from_sources(events, ledger, change_dir)
|
|
1328
1708
|
if not for_replay or not data.get("finalStatus") or str(data.get("finalStatus")).startswith("OK |"):
|
|
1329
|
-
|
|
1709
|
+
final_status, final_reasons = _compute_final_status(
|
|
1330
1710
|
data.get("stageStatus") or {},
|
|
1331
1711
|
data.get("verification") or {},
|
|
1332
1712
|
)
|
|
1713
|
+
data["finalStatus"] = final_status
|
|
1714
|
+
data["finalStatusReasons"] = list(final_reasons)
|
|
1715
|
+
else:
|
|
1716
|
+
data.setdefault("finalStatusReasons", [])
|
|
1717
|
+
|
|
1718
|
+
gate_policy = load_gate_policy(change_dir)
|
|
1719
|
+
data["riskTier"] = str((gate_policy or {}).get("tier") or "unknown")
|
|
1333
1720
|
|
|
1334
1721
|
# durations / skillCalls
|
|
1335
1722
|
if events:
|
|
@@ -1397,24 +1784,33 @@ def collect_summary_data(
|
|
|
1397
1784
|
|
|
1398
1785
|
# Derive risks/actions from evidence. These fields are facts, not model prose.
|
|
1399
1786
|
if not for_replay:
|
|
1400
|
-
|
|
1787
|
+
maintenance_notes: list[str] = [
|
|
1401
1788
|
str(event.get("note") or event.get("message") or "")
|
|
1402
1789
|
for event in events
|
|
1403
1790
|
if event.get("type") == "decision" and (event.get("note") or event.get("message"))
|
|
1404
1791
|
]
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1792
|
+
known_risks: list[dict[str, Any]] = []
|
|
1793
|
+
for event in events:
|
|
1794
|
+
if event.get("type") != "issue":
|
|
1795
|
+
continue
|
|
1796
|
+
if event.get("phase") == "archive" and event.get("code") == "missing-command":
|
|
1797
|
+
continue
|
|
1798
|
+
sev = str(event.get("severity") or "").strip().lower()
|
|
1799
|
+
message = event.get("message") or event.get("note") or event.get("code") or ""
|
|
1800
|
+
if sev in _RISK_SEVERITIES:
|
|
1801
|
+
known_risks.append(
|
|
1802
|
+
{
|
|
1803
|
+
"phase": event.get("phase"),
|
|
1804
|
+
"severity": sev,
|
|
1805
|
+
"message": message,
|
|
1806
|
+
}
|
|
1807
|
+
)
|
|
1808
|
+
else:
|
|
1809
|
+
note = str(message).strip()
|
|
1810
|
+
if note:
|
|
1811
|
+
maintenance_notes.append(note)
|
|
1812
|
+
data["maintenanceNotes"] = maintenance_notes
|
|
1813
|
+
data["knownRisks"] = known_risks
|
|
1418
1814
|
data["manualActions"] = []
|
|
1419
1815
|
for name, value in (data.get("stageStatus") or {}).items():
|
|
1420
1816
|
if value in {"BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA", "NOT_RUN", "USER_SKIPPED"}:
|
|
@@ -1427,6 +1823,8 @@ def collect_summary_data(
|
|
|
1427
1823
|
data.setdefault("maintenanceNotes", [])
|
|
1428
1824
|
data.setdefault("knownRisks", [])
|
|
1429
1825
|
data.setdefault("manualActions", [])
|
|
1826
|
+
data.setdefault("finalStatusReasons", [])
|
|
1827
|
+
data.setdefault("riskTier", "unknown")
|
|
1430
1828
|
|
|
1431
1829
|
# archiveManifest
|
|
1432
1830
|
am = {
|
|
@@ -1521,7 +1919,13 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
1521
1919
|
f'<h2 id="changeName">{esc(summary.get("changeName"))}</h2>',
|
|
1522
1920
|
f'<p><strong>finalStatus</strong>: '
|
|
1523
1921
|
f'<span id="finalStatus">{esc(summary.get("finalStatus"))}</span></p>',
|
|
1922
|
+
f'<div id="finalStatusReasons"><strong>finalStatusReasons</strong><ul>',
|
|
1524
1923
|
]
|
|
1924
|
+
for reason in summary.get("finalStatusReasons") or []:
|
|
1925
|
+
parts.append(f"<li>{esc(reason)}</li>")
|
|
1926
|
+
parts.append("</ul></div>")
|
|
1927
|
+
if summary.get("riskTier"):
|
|
1928
|
+
parts.append(f"<p><strong>riskTier</strong>: {esc(summary.get('riskTier'))}</p>")
|
|
1525
1929
|
|
|
1526
1930
|
pipeline = summary.get("reportPipeline") or {}
|
|
1527
1931
|
cmds = pipeline.get("commands") or []
|
|
@@ -2006,6 +2410,7 @@ def cmd_finalize(
|
|
|
2006
2410
|
archive_root: Path,
|
|
2007
2411
|
*,
|
|
2008
2412
|
skip_ingest: bool = False,
|
|
2413
|
+
allow_missing_review: bool = False,
|
|
2009
2414
|
) -> tuple[int, dict[str, Any]]:
|
|
2010
2415
|
"""Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
|
|
2011
2416
|
warnings: list[str] = []
|
|
@@ -2035,6 +2440,7 @@ def cmd_finalize(
|
|
|
2035
2440
|
return 1, payload
|
|
2036
2441
|
|
|
2037
2442
|
work_dir = original_change_dir
|
|
2443
|
+
before_manifest: dict[str, Any] | None = None
|
|
2038
2444
|
|
|
2039
2445
|
def _safe_append(**kwargs: Any) -> None:
|
|
2040
2446
|
nonlocal work_dir
|
|
@@ -2046,6 +2452,23 @@ def cmd_finalize(
|
|
|
2046
2452
|
# Step 9 starts here: phase.start
|
|
2047
2453
|
_safe_append(phase="archive", type_="phase.start", note="finalize start")
|
|
2048
2454
|
|
|
2455
|
+
# --- 0. cleanup transients (before before-manifest) ---
|
|
2456
|
+
try:
|
|
2457
|
+
cleanup_result = _cleanup_transients(work_dir)
|
|
2458
|
+
payload["steps"]["cleanup"] = cleanup_result
|
|
2459
|
+
deleted_n = len(cleanup_result.get("deleted") or [])
|
|
2460
|
+
trunc_n = len(cleanup_result.get("truncated") or [])
|
|
2461
|
+
_safe_append(
|
|
2462
|
+
phase="archive",
|
|
2463
|
+
type_="command",
|
|
2464
|
+
command="cleanup-transients",
|
|
2465
|
+
exit_code=0,
|
|
2466
|
+
note=f"deleted={deleted_n} truncated={trunc_n}",
|
|
2467
|
+
)
|
|
2468
|
+
except OSError as exc:
|
|
2469
|
+
warnings.append(f"cleanup failed: {exc}")
|
|
2470
|
+
payload["steps"]["cleanup"] = {"ok": False, "error": str(exc)}
|
|
2471
|
+
|
|
2049
2472
|
# --- 1. before-manifest ---
|
|
2050
2473
|
before_path = work_dir / "evidence" / "archive-manifest-before.json"
|
|
2051
2474
|
try:
|
|
@@ -2119,6 +2542,18 @@ def cmd_finalize(
|
|
|
2119
2542
|
for_replay=False,
|
|
2120
2543
|
)
|
|
2121
2544
|
summary_path = work_dir / "reports" / "final" / "summary-data.json"
|
|
2545
|
+
if allow_missing_review:
|
|
2546
|
+
reasons = list(summary.get("finalStatusReasons") or [])
|
|
2547
|
+
reason = "review missing on full tier (allowed by user)"
|
|
2548
|
+
if reason not in reasons:
|
|
2549
|
+
reasons.append(reason)
|
|
2550
|
+
summary["finalStatusReasons"] = reasons
|
|
2551
|
+
write_json(summary_path, summary)
|
|
2552
|
+
_safe_append(
|
|
2553
|
+
phase="archive",
|
|
2554
|
+
type_="decision",
|
|
2555
|
+
note=reason,
|
|
2556
|
+
)
|
|
2122
2557
|
payload["steps"]["collect"] = {"ok": True, "path": str(summary_path)}
|
|
2123
2558
|
_safe_append(
|
|
2124
2559
|
phase="archive",
|
|
@@ -2346,6 +2781,23 @@ def cmd_finalize(
|
|
|
2346
2781
|
if service_result.get("warning"):
|
|
2347
2782
|
warnings.append(str(service_result["warning"]))
|
|
2348
2783
|
|
|
2784
|
+
# --- 9a. archive-meta + artifact event BEFORE phase.end ---
|
|
2785
|
+
# phase.end must remain the last archive event; patch (9b) still runs after
|
|
2786
|
+
# phase.end so it can read duration/status from events.
|
|
2787
|
+
try:
|
|
2788
|
+
summary = read_json(summary_path)
|
|
2789
|
+
meta_path = write_archive_meta(work_dir, summary)
|
|
2790
|
+
payload["steps"]["archive_meta"] = {"ok": True, "path": str(meta_path)}
|
|
2791
|
+
_safe_append(
|
|
2792
|
+
phase="archive",
|
|
2793
|
+
type_="artifact",
|
|
2794
|
+
path="meta/archive-meta.md",
|
|
2795
|
+
kind="archive-meta",
|
|
2796
|
+
)
|
|
2797
|
+
except Exception as exc: # noqa: BLE001 — meta soft-fail
|
|
2798
|
+
warnings.append(f"archive-meta write failed: {exc}")
|
|
2799
|
+
payload["steps"]["archive_meta"] = {"ok": False, "error": str(exc)}
|
|
2800
|
+
|
|
2349
2801
|
_safe_append(
|
|
2350
2802
|
phase="archive",
|
|
2351
2803
|
type_="phase.end",
|
|
@@ -2353,6 +2805,50 @@ def cmd_finalize(
|
|
|
2353
2805
|
note="finalize complete",
|
|
2354
2806
|
)
|
|
2355
2807
|
|
|
2808
|
+
# --- 9b. patch archive stage + remanifest + single render (no more events) ---
|
|
2809
|
+
try:
|
|
2810
|
+
_patch_archive_stage(summary_path, work_dir, render=False)
|
|
2811
|
+
payload["steps"]["patch_archive"] = {"ok": True}
|
|
2812
|
+
except Exception as exc: # noqa: BLE001 — patch must not roll back archive
|
|
2813
|
+
warnings.append(f"archive stage patch failed: {exc}")
|
|
2814
|
+
payload["steps"]["patch_archive"] = {"ok": False, "error": str(exc)}
|
|
2815
|
+
|
|
2816
|
+
try:
|
|
2817
|
+
summary = read_json(summary_path)
|
|
2818
|
+
# Refresh archive-meta with patched stageStatus/durations (no new event).
|
|
2819
|
+
meta_path = write_archive_meta(work_dir, summary)
|
|
2820
|
+
payload["steps"]["archive_meta"] = {"ok": True, "path": str(meta_path)}
|
|
2821
|
+
after_manifest = generate_manifest(work_dir, after_path)
|
|
2822
|
+
before_in_archive = work_dir / "evidence" / "archive-manifest-before.json"
|
|
2823
|
+
if before_in_archive.is_file():
|
|
2824
|
+
before_manifest = read_json(before_in_archive)
|
|
2825
|
+
if before_manifest is None:
|
|
2826
|
+
raise ValueError(
|
|
2827
|
+
"before-manifest unavailable for remanifest compare "
|
|
2828
|
+
f"(missing {before_in_archive.as_posix()})"
|
|
2829
|
+
)
|
|
2830
|
+
compare_result = compare_manifests(before_manifest, after_manifest)
|
|
2831
|
+
summary["archiveManifest"] = {
|
|
2832
|
+
"movedFiles": compare_result.get("movedFiles", 0),
|
|
2833
|
+
"generatedFiles": compare_result.get("generatedFiles", 0),
|
|
2834
|
+
"totalArchiveFiles": compare_result.get("totalArchiveFiles", 0),
|
|
2835
|
+
"checksumStatus": compare_result.get("checksumStatus", "FAIL"),
|
|
2836
|
+
}
|
|
2837
|
+
write_json(summary_path, summary)
|
|
2838
|
+
render_final_summary(work_dir, summary_path)
|
|
2839
|
+
payload["steps"]["after_manifest"] = {
|
|
2840
|
+
"ok": True,
|
|
2841
|
+
"path": str(after_path),
|
|
2842
|
+
"compare": compare_result,
|
|
2843
|
+
"includesArchiveMeta": True,
|
|
2844
|
+
}
|
|
2845
|
+
except Exception as exc: # noqa: BLE001 — meta/remanifest soft-fail
|
|
2846
|
+
warnings.append(f"archive-meta/remanifest failed: {exc}")
|
|
2847
|
+
payload["steps"]["archive_meta"] = {
|
|
2848
|
+
"ok": False,
|
|
2849
|
+
"error": str(exc),
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2356
2852
|
payload["ok"] = True
|
|
2357
2853
|
payload["warnings"] = warnings
|
|
2358
2854
|
payload["summary_data"] = str(summary_path)
|
|
@@ -2455,7 +2951,10 @@ def cmd_replay(
|
|
|
2455
2951
|
|
|
2456
2952
|
def cmd_status_cli(args: argparse.Namespace) -> int:
|
|
2457
2953
|
change_dir = resolve_path(args.change_dir)
|
|
2458
|
-
result = check_status(
|
|
2954
|
+
result = check_status(
|
|
2955
|
+
change_dir,
|
|
2956
|
+
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
2957
|
+
)
|
|
2459
2958
|
emit_json(result)
|
|
2460
2959
|
# Checks completed → exit 0; archivable flag conveys the gate result.
|
|
2461
2960
|
return 0 if result.get("ok") else 1
|
|
@@ -2468,6 +2967,7 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
|
2468
2967
|
change_dir,
|
|
2469
2968
|
archive_root,
|
|
2470
2969
|
skip_ingest=bool(args.skip_ingest),
|
|
2970
|
+
allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
|
|
2471
2971
|
)
|
|
2472
2972
|
emit_json(payload)
|
|
2473
2973
|
return code
|
|
@@ -2491,12 +2991,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2491
2991
|
p_status = sub.add_parser("status", help="pre-archive gate checks (read-only)")
|
|
2492
2992
|
p_status.add_argument("--change-dir", required=True)
|
|
2493
2993
|
p_status.add_argument("--json", action="store_true", default=True)
|
|
2994
|
+
p_status.add_argument(
|
|
2995
|
+
"--allow-missing-review",
|
|
2996
|
+
action="store_true",
|
|
2997
|
+
help="downgrade full-tier missing review from blocker to warning",
|
|
2998
|
+
)
|
|
2494
2999
|
p_status.set_defaults(func=cmd_status_cli)
|
|
2495
3000
|
|
|
2496
3001
|
p_fin = sub.add_parser("finalize", help="single-process archive finalize")
|
|
2497
3002
|
p_fin.add_argument("--change-dir", required=True)
|
|
2498
3003
|
p_fin.add_argument("--archive-root", required=True)
|
|
2499
3004
|
p_fin.add_argument("--skip-ingest", action="store_true")
|
|
3005
|
+
p_fin.add_argument(
|
|
3006
|
+
"--allow-missing-review",
|
|
3007
|
+
action="store_true",
|
|
3008
|
+
help="record override when full-tier review evidence is missing",
|
|
3009
|
+
)
|
|
2500
3010
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
2501
3011
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
2502
3012
|
|