@hunter-harness/workflow-harness 0.2.59 → 0.2.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/README.md +11 -13
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +13 -16
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +5 -8
- package/harness/bundles/general/claude-code/harness-archive/reference.md +7 -16
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-run/checklist.md +10 -9
- package/harness/bundles/general/claude-code/harness-run/protocols.md +2 -0
- package/harness/bundles/general/claude-code/harness-run/reference.md +19 -16
- 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 +2 -2
- package/harness/bundles/general/claude-code/harness-test/pitfalls.md +3 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/general/claude-code/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +161 -987
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +11 -13
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +13 -16
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +5 -8
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +7 -16
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +10 -9
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +2 -0
- package/harness/bundles/general/codebuddy/harness-run/reference.md +19 -16
- 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 +2 -2
- package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +3 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/general/codebuddy/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +161 -987
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +11 -13
- package/harness/bundles/general/codex/harness-archive/SKILL.md +13 -16
- package/harness/bundles/general/codex/harness-archive/checklist.md +5 -8
- package/harness/bundles/general/codex/harness-archive/reference.md +7 -16
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-run/checklist.md +10 -9
- package/harness/bundles/general/codex/harness-run/protocols.md +2 -0
- package/harness/bundles/general/codex/harness-run/reference.md +19 -16
- 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 +2 -2
- package/harness/bundles/general/codex/harness-test/pitfalls.md +3 -3
- package/harness/bundles/general/codex/harness-test/reference.md +2 -2
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/general/codex/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/general/codex/scripts/harness_archive.py +161 -987
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +11 -13
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +13 -16
- package/harness/bundles/general/cursor/harness-archive/checklist.md +5 -8
- package/harness/bundles/general/cursor/harness-archive/reference.md +7 -16
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-run/checklist.md +10 -9
- package/harness/bundles/general/cursor/harness-run/protocols.md +2 -0
- package/harness/bundles/general/cursor/harness-run/reference.md +19 -16
- 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 +2 -2
- package/harness/bundles/general/cursor/harness-test/pitfalls.md +3 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/general/cursor/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/general/cursor/scripts/harness_archive.py +161 -987
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +11 -13
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +13 -16
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +5 -8
- package/harness/bundles/java/claude-code/harness-archive/reference.md +7 -16
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-run/protocols.md +2 -0
- 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/harness-test/checklist.md +2 -2
- package/harness/bundles/java/claude-code/harness-test/pitfalls.md +3 -3
- package/harness/bundles/java/claude-code/harness-test/reference.md +2 -2
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/java/claude-code/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +161 -987
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +11 -13
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +13 -16
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +5 -8
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +7 -16
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +2 -0
- 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/harness-test/checklist.md +2 -2
- package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +3 -3
- package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -2
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/java/codebuddy/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +161 -987
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +11 -13
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +13 -16
- package/harness/bundles/java/codex/harness-archive/checklist.md +5 -8
- package/harness/bundles/java/codex/harness-archive/reference.md +7 -16
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-run/protocols.md +2 -0
- 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/harness-test/checklist.md +2 -2
- package/harness/bundles/java/codex/harness-test/pitfalls.md +3 -3
- package/harness/bundles/java/codex/harness-test/reference.md +2 -2
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/java/codex/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/java/codex/scripts/harness_archive.py +161 -987
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +11 -13
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +13 -16
- package/harness/bundles/java/cursor/harness-archive/checklist.md +5 -8
- package/harness/bundles/java/cursor/harness-archive/reference.md +7 -16
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +1 -7
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-run/protocols.md +2 -0
- 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/harness-test/checklist.md +2 -2
- package/harness/bundles/java/cursor/harness-test/pitfalls.md +3 -3
- package/harness/bundles/java/cursor/harness-test/reference.md +2 -2
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +16 -27
- package/harness/bundles/java/cursor/protocols/evidence-based-reporting-protocol.md +5 -5
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +6 -6
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +5 -6
- package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +3 -3
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +2 -4
- package/harness/bundles/java/cursor/scripts/harness_archive.py +161 -987
- package/harness/manifests/general/claude-code.json +30 -34
- package/harness/manifests/general/codebuddy.json +30 -34
- package/harness/manifests/general/codex.json +30 -34
- package/harness/manifests/general/cursor.json +30 -34
- package/harness/manifests/java/claude-code.json +30 -34
- package/harness/manifests/java/codebuddy.json +30 -34
- package/harness/manifests/java/codex.json +30 -34
- package/harness/manifests/java/cursor.json +30 -34
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +0 -299
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +0 -299
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
Subcommands:
|
|
5
5
|
status — pre-archive gate checks (read-only)
|
|
6
|
-
finalize — single-process archive: manifest → move → collect →
|
|
7
|
-
|
|
6
|
+
finalize — single-process archive: manifest → move → collect → validate →
|
|
7
|
+
after-manifest → publish → ZIP upload/service
|
|
8
8
|
replay — read-only re-collect + validate for historical archives
|
|
9
9
|
|
|
10
10
|
Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
|
|
@@ -43,7 +43,6 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
43
43
|
|
|
44
44
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
45
45
|
SKILLS_ROOT = SCRIPTS_DIR.parent
|
|
46
|
-
RENDER_SCRIPT = SKILLS_ROOT / "harness-archive" / "templates" / "render-summary.mjs"
|
|
47
46
|
SUMMARY_TEMPLATE = (
|
|
48
47
|
SKILLS_ROOT / "harness-archive" / "templates" / "summary-data-template.json"
|
|
49
48
|
)
|
|
@@ -3198,7 +3197,11 @@ def build_verification_projection(
|
|
|
3198
3197
|
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
3199
3198
|
}
|
|
3200
3199
|
db_compatibility = _ledger_db_compat(effective_ledger)
|
|
3201
|
-
if db_compatibility["status"]
|
|
3200
|
+
if db_compatibility["status"] in {
|
|
3201
|
+
"NOT_RUN",
|
|
3202
|
+
"UNKNOWN",
|
|
3203
|
+
"EVIDENCE_MISSING",
|
|
3204
|
+
} and change_dir is not None:
|
|
3202
3205
|
gate_policy_path = change_dir / "meta" / "gate-policy.json"
|
|
3203
3206
|
try:
|
|
3204
3207
|
gate_policy = read_json(gate_policy_path) if gate_policy_path.is_file() else None
|
|
@@ -3936,8 +3939,16 @@ def _resolve_base_commit(
|
|
|
3936
3939
|
|
|
3937
3940
|
Latest merge phase context must not override the full change boundary.
|
|
3938
3941
|
"""
|
|
3942
|
+
def stable_base(value: Any) -> str:
|
|
3943
|
+
candidate = str(value or "").strip()
|
|
3944
|
+
if candidate.upper() in {"HEAD", "FETCH_HEAD", "ORIG_HEAD"}:
|
|
3945
|
+
return ""
|
|
3946
|
+
if candidate.startswith(("refs/", "@{")):
|
|
3947
|
+
return ""
|
|
3948
|
+
return candidate
|
|
3949
|
+
|
|
3939
3950
|
if ledger:
|
|
3940
|
-
base =
|
|
3951
|
+
base = stable_base(ledger.get("baseCommit"))
|
|
3941
3952
|
if base and base != NOT_AVAILABLE:
|
|
3942
3953
|
return base
|
|
3943
3954
|
|
|
@@ -3959,7 +3970,7 @@ def _resolve_base_commit(
|
|
|
3959
3970
|
continue
|
|
3960
3971
|
if not isinstance(payload, dict):
|
|
3961
3972
|
continue
|
|
3962
|
-
base =
|
|
3973
|
+
base = stable_base(payload.get("baseCommit"))
|
|
3963
3974
|
if base and base != NOT_AVAILABLE:
|
|
3964
3975
|
return base
|
|
3965
3976
|
|
|
@@ -3996,9 +4007,18 @@ def _final_commit_from_sources(
|
|
|
3996
4007
|
if str(event.get("phase") or "").lower() not in {"submit", "merge", "archive"}:
|
|
3997
4008
|
continue
|
|
3998
4009
|
text = " ".join(str(event.get(key) or "") for key in ("note", "message", "command"))
|
|
3999
|
-
match
|
|
4000
|
-
|
|
4001
|
-
|
|
4010
|
+
for match in commit_pattern.finditer(text):
|
|
4011
|
+
candidate = match.group(0)
|
|
4012
|
+
code, resolved, _ = git_run(
|
|
4013
|
+
project,
|
|
4014
|
+
"rev-parse",
|
|
4015
|
+
"--verify",
|
|
4016
|
+
f"{candidate}^{{commit}}",
|
|
4017
|
+
)
|
|
4018
|
+
resolved_lines = str(resolved or "").splitlines()
|
|
4019
|
+
resolved_hash = resolved_lines[0].strip() if resolved_lines else ""
|
|
4020
|
+
if code == 0 and re.fullmatch(r"[0-9a-f]{40}", resolved_hash, re.IGNORECASE):
|
|
4021
|
+
return resolved_hash
|
|
4002
4022
|
if existing and existing.get("finalCommit"):
|
|
4003
4023
|
return str(existing["finalCommit"])
|
|
4004
4024
|
code, head, _ = git_run(project, "rev-parse", "HEAD")
|
|
@@ -4669,618 +4689,6 @@ def collect_summary_data(
|
|
|
4669
4689
|
|
|
4670
4690
|
|
|
4671
4691
|
# ---------------------------------------------------------------------------
|
|
4672
|
-
# render
|
|
4673
|
-
# ---------------------------------------------------------------------------
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
def resolve_node_path(project_root: Path) -> str | None:
|
|
4677
|
-
profile = project_root / ".harness" / "config" / "build-profile.json"
|
|
4678
|
-
if profile.is_file():
|
|
4679
|
-
try:
|
|
4680
|
-
data = read_json(profile)
|
|
4681
|
-
node = (data.get("toolPaths") or {}).get("node")
|
|
4682
|
-
if node and Path(str(node)).exists():
|
|
4683
|
-
return str(node)
|
|
4684
|
-
except (OSError, json.JSONDecodeError, TypeError):
|
|
4685
|
-
pass
|
|
4686
|
-
return shutil.which("node")
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
def _render_legacy_fallback_html(summary: dict[str, Any]) -> str:
|
|
4690
|
-
"""Render escaped, deterministic HTML with every validate-required fact.
|
|
4691
|
-
|
|
4692
|
-
Used when the Node renderer is unavailable or fails. No timestamps / random
|
|
4693
|
-
data; all dynamic values are HTML-escaped via _html_escape.
|
|
4694
|
-
"""
|
|
4695
|
-
def esc(v: Any) -> str:
|
|
4696
|
-
return _html_escape("" if v is None else str(v))
|
|
4697
|
-
|
|
4698
|
-
parts: list[str] = [
|
|
4699
|
-
"<!DOCTYPE html>",
|
|
4700
|
-
'<html lang="zh-CN"><head><meta charset="utf-8">',
|
|
4701
|
-
"<title>harness final-summary (python fallback)</title>",
|
|
4702
|
-
"</head><body>",
|
|
4703
|
-
"<h1>变更最终报告(Python fallback 渲染)</h1>",
|
|
4704
|
-
f'<h2 id="changeName">{esc(summary.get("changeName"))}</h2>',
|
|
4705
|
-
f'<p><strong>finalStatus</strong>: '
|
|
4706
|
-
f'<span id="finalStatus">{esc(summary.get("finalStatus"))}</span></p>',
|
|
4707
|
-
f'<div id="finalStatusReasons"><strong>finalStatusReasons</strong><ul>',
|
|
4708
|
-
]
|
|
4709
|
-
for reason in summary.get("finalStatusReasons") or []:
|
|
4710
|
-
parts.append(f"<li>{esc(reason)}</li>")
|
|
4711
|
-
parts.append("</ul></div>")
|
|
4712
|
-
if summary.get("riskTier"):
|
|
4713
|
-
parts.append(f"<p><strong>riskTier</strong>: {esc(summary.get('riskTier'))}</p>")
|
|
4714
|
-
parts.append("<h3>Current Outcome</h3>")
|
|
4715
|
-
parts.append(
|
|
4716
|
-
f"<p>finalStatus={esc(summary.get('finalStatus'))}</p>"
|
|
4717
|
-
)
|
|
4718
|
-
|
|
4719
|
-
candidate = (
|
|
4720
|
-
summary.get("candidateVerification")
|
|
4721
|
-
if isinstance(summary.get("candidateVerification"), dict)
|
|
4722
|
-
else {}
|
|
4723
|
-
)
|
|
4724
|
-
parts.append("<h3>Candidate Claim / Attestation</h3>")
|
|
4725
|
-
parts.append(
|
|
4726
|
-
f"<p>assurance={esc(candidate.get('assurance'))} "
|
|
4727
|
-
f"code={esc(candidate.get('code'))} "
|
|
4728
|
-
f"provider={esc(candidate.get('provider'))}</p>"
|
|
4729
|
-
)
|
|
4730
|
-
|
|
4731
|
-
integrity = (
|
|
4732
|
-
summary.get("archiveIntegrity")
|
|
4733
|
-
if isinstance(summary.get("archiveIntegrity"), dict)
|
|
4734
|
-
else {}
|
|
4735
|
-
)
|
|
4736
|
-
parts.append("<h3>Archive Integrity</h3>")
|
|
4737
|
-
parts.append(
|
|
4738
|
-
f"<p>ok={esc(integrity.get('ok'))} "
|
|
4739
|
-
f"code={esc(integrity.get('code'))} "
|
|
4740
|
-
f"checksumStatus={esc(integrity.get('checksumStatus'))}</p>"
|
|
4741
|
-
)
|
|
4742
|
-
|
|
4743
|
-
decision = (
|
|
4744
|
-
summary.get("releaseDecision")
|
|
4745
|
-
if isinstance(summary.get("releaseDecision"), dict)
|
|
4746
|
-
else {}
|
|
4747
|
-
)
|
|
4748
|
-
parts.append("<h3>Release Eligibility</h3>")
|
|
4749
|
-
parts.append(
|
|
4750
|
-
f"<p>releaseEligible={esc(decision.get('releaseEligible'))} "
|
|
4751
|
-
f"code={esc(decision.get('code'))}</p><ul>"
|
|
4752
|
-
)
|
|
4753
|
-
checks = decision.get("checks")
|
|
4754
|
-
checks = checks if isinstance(checks, dict) else {}
|
|
4755
|
-
for name, check in checks.items():
|
|
4756
|
-
check = check if isinstance(check, dict) else {}
|
|
4757
|
-
parts.append(
|
|
4758
|
-
f"<li>{esc(name)}: ok={esc(check.get('ok'))} "
|
|
4759
|
-
f"code={esc(check.get('code'))}</li>"
|
|
4760
|
-
)
|
|
4761
|
-
parts.append("</ul>")
|
|
4762
|
-
|
|
4763
|
-
timing = summary.get("timing") if isinstance(summary.get("timing"), dict) else {}
|
|
4764
|
-
if timing:
|
|
4765
|
-
parts.append("<h3>Timing</h3>")
|
|
4766
|
-
parts.append(
|
|
4767
|
-
"<p id=\"timingColumns\">"
|
|
4768
|
-
f"stageActiveExecution={esc(timing.get('stageActiveExecutionMs'))} · "
|
|
4769
|
-
f"stageWallClockSpan={esc(timing.get('stageWallClockSpanMs'))} · "
|
|
4770
|
-
f"workflowWallClock={esc(timing.get('workflowWallClockMs'))}"
|
|
4771
|
-
"</p>"
|
|
4772
|
-
)
|
|
4773
|
-
parts.append(
|
|
4774
|
-
"<p id=\"timingConservation\">"
|
|
4775
|
-
f"attributedStageUnionMs={esc(timing.get('attributedStageUnionMs'))} · "
|
|
4776
|
-
f"externalWaitMs={esc(timing.get('externalWaitMs'))} · "
|
|
4777
|
-
f"pausedMs={esc(timing.get('pausedMs'))} · "
|
|
4778
|
-
"agentOrToolUnattributedMs="
|
|
4779
|
-
f"{esc(timing.get('agentOrToolUnattributedMs'))} · "
|
|
4780
|
-
f"conservationDeltaMs={esc(timing.get('conservationDeltaMs'))}"
|
|
4781
|
-
"</p>"
|
|
4782
|
-
)
|
|
4783
|
-
parts.append(
|
|
4784
|
-
f"<p><strong>reportCutoffAt</strong>: "
|
|
4785
|
-
f"<span id=\"reportCutoffAt\">{esc(timing.get('reportCutoffAt'))}</span></p>"
|
|
4786
|
-
)
|
|
4787
|
-
parts.append(
|
|
4788
|
-
"<p><small>durations.totalMinutes is active-only; "
|
|
4789
|
-
"do not treat it as workflow wall clock.</small></p>"
|
|
4790
|
-
)
|
|
4791
|
-
parts.append("<h3>History Quality</h3><ul>")
|
|
4792
|
-
attempts = timing.get("attempts")
|
|
4793
|
-
attempts = attempts if isinstance(attempts, list) else []
|
|
4794
|
-
for attempt in attempts:
|
|
4795
|
-
attempt = attempt if isinstance(attempt, dict) else {}
|
|
4796
|
-
parts.append(
|
|
4797
|
-
f"<li>{esc(attempt.get('phase'))}: "
|
|
4798
|
-
f"{esc(attempt.get('terminalStatus') or attempt.get('status'))} "
|
|
4799
|
-
f"durationMs={esc(attempt.get('durationMs'))}</li>"
|
|
4800
|
-
)
|
|
4801
|
-
if not attempts:
|
|
4802
|
-
parts.append("<li>no typed attempts recorded</li>")
|
|
4803
|
-
parts.append("</ul>")
|
|
4804
|
-
|
|
4805
|
-
remote_cost = (
|
|
4806
|
-
summary.get("remoteCost")
|
|
4807
|
-
if isinstance(summary.get("remoteCost"), dict)
|
|
4808
|
-
else {}
|
|
4809
|
-
)
|
|
4810
|
-
remote_totals = remote_cost.get("totals")
|
|
4811
|
-
remote_totals = remote_totals if isinstance(remote_totals, dict) else {}
|
|
4812
|
-
parts.append("<h3>Remote Cost</h3>")
|
|
4813
|
-
parts.append(
|
|
4814
|
-
f"<p>runnerMinutes={esc(remote_totals.get('runnerMinutes'))} "
|
|
4815
|
-
f"queueWaitMs={esc(remote_totals.get('queueWaitMs'))} "
|
|
4816
|
-
f"artifactBytes={esc(remote_totals.get('artifactBytes'))} "
|
|
4817
|
-
f"duplicateRunCount={esc(remote_totals.get('duplicateRunCount'))}</p>"
|
|
4818
|
-
)
|
|
4819
|
-
|
|
4820
|
-
storage = (
|
|
4821
|
-
summary.get("artifactStorage")
|
|
4822
|
-
if isinstance(summary.get("artifactStorage"), dict)
|
|
4823
|
-
else {}
|
|
4824
|
-
)
|
|
4825
|
-
parts.append("<h3>Artifact Storage</h3>")
|
|
4826
|
-
parts.append(
|
|
4827
|
-
f"<p>artifactCount={esc(storage.get('artifactCount'))} "
|
|
4828
|
-
f"bytesAdded={esc(storage.get('bytesAdded'))} "
|
|
4829
|
-
f"bytesReused={esc(storage.get('bytesReused'))} "
|
|
4830
|
-
f"bytesPruned={esc(storage.get('bytesPruned'))}</p>"
|
|
4831
|
-
)
|
|
4832
|
-
|
|
4833
|
-
projection = (
|
|
4834
|
-
summary.get("projection")
|
|
4835
|
-
if isinstance(summary.get("projection"), dict)
|
|
4836
|
-
else {}
|
|
4837
|
-
)
|
|
4838
|
-
parts.append("<h3>Projection / Fallback</h3>")
|
|
4839
|
-
parts.append(
|
|
4840
|
-
f"<p>mode={esc(projection.get('mode'))} "
|
|
4841
|
-
f"code={esc(projection.get('code'))} "
|
|
4842
|
-
f"remediation={esc(projection.get('remediation'))}</p>"
|
|
4843
|
-
)
|
|
4844
|
-
|
|
4845
|
-
if summary.get("productCommit") or summary.get("productTreeHash"):
|
|
4846
|
-
parts.append("<h3>Identity</h3>")
|
|
4847
|
-
parts.append(
|
|
4848
|
-
f"<p>productCommit={esc(summary.get('productCommit'))} "
|
|
4849
|
-
f"productTreeHash={esc(summary.get('productTreeHash'))} "
|
|
4850
|
-
f"archiveCommit={esc(summary.get('archiveCommit'))}</p>"
|
|
4851
|
-
)
|
|
4852
|
-
|
|
4853
|
-
pipeline = summary.get("reportPipeline") or {}
|
|
4854
|
-
cmds = pipeline.get("commands") or []
|
|
4855
|
-
parts.append("<h3>Commands</h3><ul>")
|
|
4856
|
-
for c in cmds:
|
|
4857
|
-
parts.append(
|
|
4858
|
-
f"<li><code>{esc(c.get('command'))}</code> "
|
|
4859
|
-
f"exitCode={esc(c.get('exit_code'))}</li>"
|
|
4860
|
-
)
|
|
4861
|
-
parts.append("</ul>")
|
|
4862
|
-
|
|
4863
|
-
ver = summary.get("verification") or {}
|
|
4864
|
-
unit = ver.get("unitTests") or {}
|
|
4865
|
-
api = ver.get("apiTests") or {}
|
|
4866
|
-
browser = ver.get("browserE2E") or {}
|
|
4867
|
-
parts.append("<h3>Verification</h3>")
|
|
4868
|
-
parts.append(
|
|
4869
|
-
"<p>unitTests: run={run} failures={failures} errors={errors} "
|
|
4870
|
-
"skipped={skipped} passRate={passRate} status={status}</p>".format(
|
|
4871
|
-
run=esc(unit.get("run")),
|
|
4872
|
-
failures=esc(unit.get("failures")),
|
|
4873
|
-
errors=esc(unit.get("errors")),
|
|
4874
|
-
skipped=esc(unit.get("skipped")),
|
|
4875
|
-
passRate=esc(unit.get("passRate")),
|
|
4876
|
-
status=esc(unit.get("status")),
|
|
4877
|
-
)
|
|
4878
|
-
)
|
|
4879
|
-
parts.append(
|
|
4880
|
-
"<p>apiTests: status={status} total={total} passed={passed} "
|
|
4881
|
-
"failed={failed} blocked={blocked}</p>".format(
|
|
4882
|
-
status=esc(api.get("status")),
|
|
4883
|
-
total=esc(api.get("total")),
|
|
4884
|
-
passed=esc(api.get("passed")),
|
|
4885
|
-
failed=esc(api.get("failed")),
|
|
4886
|
-
blocked=esc(api.get("blocked")),
|
|
4887
|
-
)
|
|
4888
|
-
)
|
|
4889
|
-
parts.append(
|
|
4890
|
-
"<p>browserE2E: status={status} total={total} passed={passed} "
|
|
4891
|
-
"failed={failed} skipped={skipped} retries={retries}</p>".format(
|
|
4892
|
-
status=esc(browser.get("status")),
|
|
4893
|
-
total=esc(browser.get("total")),
|
|
4894
|
-
passed=esc(browser.get("passed")),
|
|
4895
|
-
failed=esc(browser.get("failed")),
|
|
4896
|
-
skipped=esc(browser.get("skipped")),
|
|
4897
|
-
retries=esc(browser.get("retries")),
|
|
4898
|
-
)
|
|
4899
|
-
)
|
|
4900
|
-
parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
|
|
4901
|
-
|
|
4902
|
-
parts.append("<h3>Changed Files</h3><ul>")
|
|
4903
|
-
for f in summary.get("changedFiles") or []:
|
|
4904
|
-
parts.append(
|
|
4905
|
-
f"<li>{esc(f.get('path'))} +{esc(f.get('insertions'))} "
|
|
4906
|
-
f"-{esc(f.get('deletions'))}</li>"
|
|
4907
|
-
)
|
|
4908
|
-
parts.append("</ul>")
|
|
4909
|
-
|
|
4910
|
-
am = summary.get("archiveManifest") or {}
|
|
4911
|
-
parts.append("<h3>Archive Manifest</h3>")
|
|
4912
|
-
parts.append(
|
|
4913
|
-
"<p>movedFiles={moved} generatedFiles={gen} totalArchiveFiles={total} "
|
|
4914
|
-
"checksumStatus={cs}</p>".format(
|
|
4915
|
-
moved=esc(am.get("movedFiles")),
|
|
4916
|
-
gen=esc(am.get("generatedFiles")),
|
|
4917
|
-
total=esc(am.get("totalArchiveFiles")),
|
|
4918
|
-
cs=esc(am.get("checksumStatus")),
|
|
4919
|
-
)
|
|
4920
|
-
)
|
|
4921
|
-
|
|
4922
|
-
def _list_section(title: str, items: Any) -> None:
|
|
4923
|
-
parts.append(f"<h3>{esc(title)}</h3><ul>")
|
|
4924
|
-
for it in items or []:
|
|
4925
|
-
parts.append(f"<li>{esc(it)}</li>")
|
|
4926
|
-
parts.append("</ul>")
|
|
4927
|
-
|
|
4928
|
-
_list_section("Known Risks", summary.get("knownRisks"))
|
|
4929
|
-
_list_section("Manual Actions", summary.get("manualActions"))
|
|
4930
|
-
_list_section("Maintenance Notes", summary.get("maintenanceNotes"))
|
|
4931
|
-
|
|
4932
|
-
parts.append("</body></html>")
|
|
4933
|
-
return "\n".join(parts) + "\n"
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
4937
|
-
"""Render the same executive information architecture without Node."""
|
|
4938
|
-
|
|
4939
|
-
def esc(value: Any) -> str:
|
|
4940
|
-
return _html_escape("" if value is None else str(value))
|
|
4941
|
-
|
|
4942
|
-
def rec(value: Any) -> dict[str, Any]:
|
|
4943
|
-
return value if isinstance(value, dict) else {}
|
|
4944
|
-
|
|
4945
|
-
def seq(value: Any) -> list[Any]:
|
|
4946
|
-
return value if isinstance(value, list) else []
|
|
4947
|
-
|
|
4948
|
-
normalized = rec(summary.get("normalizedReport"))
|
|
4949
|
-
outcomes = rec(normalized.get("outcomes"))
|
|
4950
|
-
current = rec(outcomes.get("current")) or {
|
|
4951
|
-
"status": summary.get("finalStatus"),
|
|
4952
|
-
"reasons": seq(summary.get("finalStatusReasons")),
|
|
4953
|
-
"stages": rec(summary.get("stageStatus")),
|
|
4954
|
-
"knownRisks": seq(summary.get("knownRisks")),
|
|
4955
|
-
}
|
|
4956
|
-
release = rec(outcomes.get("release")) or {
|
|
4957
|
-
"intent": summary.get("archiveIntent"),
|
|
4958
|
-
"decision": "NOT_REQUESTED"
|
|
4959
|
-
if summary.get("archiveIntent") == "record-only"
|
|
4960
|
-
else rec(summary.get("releaseDecision")).get("code"),
|
|
4961
|
-
"eligible": rec(summary.get("releaseDecision")).get("releaseEligible"),
|
|
4962
|
-
}
|
|
4963
|
-
identity = rec(normalized.get("identity")) or rec(summary.get("changeIdentity"))
|
|
4964
|
-
verification = rec(normalized.get("verification")) or rec(
|
|
4965
|
-
summary.get("verification")
|
|
4966
|
-
)
|
|
4967
|
-
timing = rec(normalized.get("timing")) or rec(summary.get("timing"))
|
|
4968
|
-
measurements = rec(normalized.get("measurements"))
|
|
4969
|
-
record_only = (
|
|
4970
|
-
release.get("intent") == "record-only"
|
|
4971
|
-
or summary.get("archiveIntent") == "record-only"
|
|
4972
|
-
)
|
|
4973
|
-
product_commit = (
|
|
4974
|
-
identity.get("productCommit")
|
|
4975
|
-
or summary.get("productCommit")
|
|
4976
|
-
or summary.get("finalCommit")
|
|
4977
|
-
or "N/A"
|
|
4978
|
-
)
|
|
4979
|
-
labels = [
|
|
4980
|
-
("后端", ("unitTests", "dbCompatibility")),
|
|
4981
|
-
("Geo", ("geo",)),
|
|
4982
|
-
("前端", ("frontend",)),
|
|
4983
|
-
("浏览器", ("browserE2E",)),
|
|
4984
|
-
("API", ("apiTests",)),
|
|
4985
|
-
]
|
|
4986
|
-
status_labels = {
|
|
4987
|
-
"OK": "通过",
|
|
4988
|
-
"PASS": "通过",
|
|
4989
|
-
"PASSED": "通过",
|
|
4990
|
-
"CONDITIONAL_OK": "有条件通过",
|
|
4991
|
-
"WARN": "警告",
|
|
4992
|
-
"ADVISORY": "建议",
|
|
4993
|
-
"FAIL": "失败",
|
|
4994
|
-
"FAILED": "失败",
|
|
4995
|
-
"ERROR": "错误",
|
|
4996
|
-
"BLOCKED": "阻塞",
|
|
4997
|
-
"NOT_RUN": "未运行",
|
|
4998
|
-
"SKIPPED": "已跳过",
|
|
4999
|
-
"NOT_APPLICABLE": "不适用",
|
|
5000
|
-
"UNKNOWN": "未知",
|
|
5001
|
-
}
|
|
5002
|
-
|
|
5003
|
-
def status_label(value: Any) -> str:
|
|
5004
|
-
status = str(value or "UNKNOWN").upper()
|
|
5005
|
-
return status_labels.get(status, status)
|
|
5006
|
-
|
|
5007
|
-
def verification_status(value: Any) -> str:
|
|
5008
|
-
if isinstance(value, str):
|
|
5009
|
-
return value
|
|
5010
|
-
item = rec(value)
|
|
5011
|
-
if item.get("status"):
|
|
5012
|
-
return str(item["status"])
|
|
5013
|
-
if int(item.get("failures") or 0) + int(item.get("errors") or 0):
|
|
5014
|
-
return "FAIL"
|
|
5015
|
-
if int(item.get("run") or item.get("total") or 0):
|
|
5016
|
-
return "OK"
|
|
5017
|
-
return "NOT_RUN"
|
|
5018
|
-
|
|
5019
|
-
def describe(value: Any) -> str:
|
|
5020
|
-
if isinstance(value, str):
|
|
5021
|
-
return value
|
|
5022
|
-
item = rec(value)
|
|
5023
|
-
return str(
|
|
5024
|
-
item.get("title")
|
|
5025
|
-
or item.get("message")
|
|
5026
|
-
or item.get("summary")
|
|
5027
|
-
or item.get("action")
|
|
5028
|
-
or item.get("remediation")
|
|
5029
|
-
or item
|
|
5030
|
-
)
|
|
5031
|
-
|
|
5032
|
-
def measurement_label(value: Any) -> str:
|
|
5033
|
-
item = rec(value)
|
|
5034
|
-
if item.get("state") in {"unknown", "not_applicable"}:
|
|
5035
|
-
return "N/A"
|
|
5036
|
-
if item.get("state") == "zero":
|
|
5037
|
-
return "0"
|
|
5038
|
-
if item.get("state") == "known":
|
|
5039
|
-
return str(item.get("value"))
|
|
5040
|
-
return "N/A" if value is None or value == "" else str(value)
|
|
5041
|
-
|
|
5042
|
-
risks = (
|
|
5043
|
-
seq(current.get("findings"))
|
|
5044
|
-
or seq(current.get("knownRisks"))
|
|
5045
|
-
or seq(summary.get("knownRisks"))
|
|
5046
|
-
)
|
|
5047
|
-
actions = seq(summary.get("manualActions"))
|
|
5048
|
-
groups: list[str] = []
|
|
5049
|
-
passed = 0
|
|
5050
|
-
for label, keys in labels:
|
|
5051
|
-
statuses = [
|
|
5052
|
-
verification_status(verification[key])
|
|
5053
|
-
for key in keys
|
|
5054
|
-
if key in verification
|
|
5055
|
-
]
|
|
5056
|
-
status = (
|
|
5057
|
-
"FAIL"
|
|
5058
|
-
if any(value in {"FAIL", "ERROR", "BLOCKED"} for value in statuses)
|
|
5059
|
-
else "WARN"
|
|
5060
|
-
if any("WARN" in value or "CONDITIONAL" in value for value in statuses)
|
|
5061
|
-
else "NOT_APPLICABLE"
|
|
5062
|
-
if statuses and all(value == "NOT_APPLICABLE" for value in statuses)
|
|
5063
|
-
else "OK"
|
|
5064
|
-
if statuses
|
|
5065
|
-
and all(
|
|
5066
|
-
value in {"OK", "PASS", "PASSED", "NOT_APPLICABLE"}
|
|
5067
|
-
for value in statuses
|
|
5068
|
-
)
|
|
5069
|
-
else "NOT_RUN"
|
|
5070
|
-
)
|
|
5071
|
-
if status == "OK":
|
|
5072
|
-
passed += 1
|
|
5073
|
-
details = " · ".join(
|
|
5074
|
-
str(rec(verification.get(key)).get("passRate") or status)
|
|
5075
|
-
for key in keys
|
|
5076
|
-
if key in verification
|
|
5077
|
-
) or "未配置该组验证"
|
|
5078
|
-
raw_details = " · ".join(
|
|
5079
|
-
f"{key} · status={verification_status(verification[key])}"
|
|
5080
|
-
+ (
|
|
5081
|
-
f" · failed={rec(verification[key]).get('failed')}"
|
|
5082
|
-
if rec(verification[key]).get("failed") is not None
|
|
5083
|
-
else ""
|
|
5084
|
-
)
|
|
5085
|
-
for key in keys
|
|
5086
|
-
if key in verification
|
|
5087
|
-
)
|
|
5088
|
-
groups.append(
|
|
5089
|
-
f'<article class="verify" title="{esc(raw_details)}"><strong>{esc(label)}</strong>'
|
|
5090
|
-
f'<span title="{esc(status)}">{esc(status_label(status))}</span><small>{esc(details)}</small></article>'
|
|
5091
|
-
)
|
|
5092
|
-
|
|
5093
|
-
commands = seq(rec(summary.get("reportPipeline")).get("commands"))
|
|
5094
|
-
command_rows = "".join(
|
|
5095
|
-
f"<tr><td>{esc(item.get('phase') or '-')}</td>"
|
|
5096
|
-
f"<td><code>{esc(item.get('command'))}</code></td>"
|
|
5097
|
-
f"<td>{esc('OK' if int(item.get('exit_code', item.get('exitCode', 1))) == 0 else 'FAIL')}</td></tr>"
|
|
5098
|
-
for item in commands
|
|
5099
|
-
if isinstance(item, dict)
|
|
5100
|
-
) or '<tr><td colspan="3">没有命令证据</td></tr>'
|
|
5101
|
-
files = seq(summary.get("changedFiles"))
|
|
5102
|
-
file_rows = "".join(
|
|
5103
|
-
f"<tr><td><code>{esc(item.get('path') or item.get('file'))}</code></td>"
|
|
5104
|
-
f"<td>+{esc(item.get('insertions') or 0)}</td>"
|
|
5105
|
-
f"<td>-{esc(item.get('deletions') or 0)}</td></tr>"
|
|
5106
|
-
for item in files
|
|
5107
|
-
if isinstance(item, dict)
|
|
5108
|
-
) or '<tr><td colspan="3">没有变更文件证据</td></tr>'
|
|
5109
|
-
risk_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in risks) or "<li>当前没有未处置风险</li>"
|
|
5110
|
-
action_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in actions) or "<li>无需人工后续动作</li>"
|
|
5111
|
-
release_card = (
|
|
5112
|
-
""
|
|
5113
|
-
if record_only
|
|
5114
|
-
else "<article class=\"card\"><h2>发布与候选</h2>"
|
|
5115
|
-
f"<p>候选证明 / 发布资格:{esc(release.get('decision') or 'NOT_EVALUATED')}</p></article>"
|
|
5116
|
-
)
|
|
5117
|
-
remote = rec(measurements.get("remoteCost"))
|
|
5118
|
-
storage = rec(measurements.get("artifactStorage"))
|
|
5119
|
-
remote_totals = rec(remote.get("totals"))
|
|
5120
|
-
projection = rec(summary.get("projection"))
|
|
5121
|
-
archive_integrity = rec(summary.get("archiveIntegrity"))
|
|
5122
|
-
durability = rec(summary.get("archiveDurability"))
|
|
5123
|
-
durability_status = str(
|
|
5124
|
-
durability.get("status") or "ARCHIVED_LOCAL_ONLY"
|
|
5125
|
-
)
|
|
5126
|
-
scenario_coverage = rec(summary.get("scenarioCoverage"))
|
|
5127
|
-
unexecuted_scenarios = [
|
|
5128
|
-
str(item) for item in seq(scenario_coverage.get("unexecuted"))
|
|
5129
|
-
]
|
|
5130
|
-
scenario_status = str(
|
|
5131
|
-
scenario_coverage.get("code")
|
|
5132
|
-
or ("SCENARIO_COVERAGE_OK" if scenario_coverage.get("ok") else "NOT_RUN")
|
|
5133
|
-
)
|
|
5134
|
-
total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
|
|
5135
|
-
total_files_text = "" if total_files is None else str(total_files)
|
|
5136
|
-
|
|
5137
|
-
return f"""<!doctype html>
|
|
5138
|
-
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5139
|
-
<title>Harness 最终报告 · {esc(summary.get("changeName"))}</title>
|
|
5140
|
-
<style>
|
|
5141
|
-
:root{{--bg:#f4f6f8;--card:#fff;--text:#172033;--muted:#667085;--line:#dfe5ec}}
|
|
5142
|
-
@media(prefers-color-scheme:dark){{:root{{--bg:#0b1119;--card:#121a26;--text:#eef3f9;--muted:#9aa8ba;--line:#2a384a}}}}
|
|
5143
|
-
*{{box-sizing:border-box}}html,body{{max-width:100%;overflow-x:hidden}}body{{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 "Segoe UI","Microsoft YaHei",sans-serif}}main{{width:min(1120px,calc(100% - 32px));margin:24px auto}}.hero,.card,.metric,.verify,details{{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px}}.hero h1{{margin:4px 0}}.metrics,.groups{{display:grid;grid-template-columns:repeat(5,1fr);gap:9px;margin:10px 0}}.metric strong,.verify strong{{display:block;font-size:17px}}.verify small{{display:block;color:var(--muted)}}.risk{{display:grid;grid-template-columns:1fr 1fr;gap:10px}}.card{{margin:10px 0}}details{{margin:9px 0}}summary{{cursor:pointer;font-weight:700}}details>div{{overflow:auto}}table{{width:100%;border-collapse:collapse}}td,th{{padding:8px;border-bottom:1px solid var(--line);text-align:left}}code{{overflow-wrap:anywhere}}@media(max-width:600px){{main{{width:calc(100% - 18px);margin:9px auto}}.metrics,.groups,.risk{{grid-template-columns:1fr}}}}
|
|
5144
|
-
</style></head><body><main>
|
|
5145
|
-
<section class="hero"><small>HARNESS · 管理结论</small><h1>{esc(summary.get("changeName"))}</h1>
|
|
5146
|
-
<p>{esc(summary.get("businessGoal") or "未记录业务目标")}</p>
|
|
5147
|
-
<strong title="{esc(current.get("status"))}">{esc(status_label(current.get("status")))}</strong>
|
|
5148
|
-
<p id="finalStatusReasons">{esc(" · ".join(str(item) for item in seq(current.get("reasons"))))}</p>
|
|
5149
|
-
{"<p>归档意图:仅记录 · 未请求发布</p>" if record_only else ""}</section>
|
|
5150
|
-
<section class="metrics"><article class="metric"><small>产品提交</small><strong><code>{esc(str(product_commit)[:10])}</code></strong></article>
|
|
5151
|
-
<article class="metric"><small>验证概览</small><strong>{passed}/5 组通过</strong></article>
|
|
5152
|
-
<article class="metric"><small>风险与动作</small><strong>{len(risks)} / {len(actions)}</strong></article>
|
|
5153
|
-
<article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
|
|
5154
|
-
<article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
|
|
5155
|
-
<article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
|
|
5156
|
-
<article class="card"><h2>场景执行闭环</h2>
|
|
5157
|
-
<p>结构化运行回执:{esc(scenario_status)}</p>
|
|
5158
|
-
<p>未执行 / 未通过:{esc(", ".join(unexecuted_scenarios) or "无")}</p></article>
|
|
5159
|
-
<article class="card"><h2>风险与动作</h2><div class="risk"><section><h3>当前风险</h3><ul>{risk_rows}</ul></section><section><h3>人工动作</h3><ul>{action_rows}</ul></section></div></article>
|
|
5160
|
-
{release_card}
|
|
5161
|
-
<details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
|
|
5162
|
-
<p>workflowWallClock={esc(timing.get("workflowWallClockMs") if "workflowWallClockMs" in timing else "N/A")}</p>
|
|
5163
|
-
<p>stageActiveExecution={esc(timing.get("stageActiveExecutionMs") if "stageActiveExecutionMs" in timing else "N/A")}</p>
|
|
5164
|
-
<p>stageWallClockSpan={esc(timing.get("stageWallClockSpanMs") if "stageWallClockSpanMs" in timing else "N/A")}</p>
|
|
5165
|
-
<p>reportCutoffAt={esc(timing.get("reportCutoffAt") or "N/A")}</p>
|
|
5166
|
-
<p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
|
|
5167
|
-
<p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
|
|
5168
|
-
<details><summary>技术证据 · 发布与归档治理</summary><div>
|
|
5169
|
-
<p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
|
|
5170
|
-
<p>归档持久性:{esc(durability_status)} · {esc(durability.get("risk") or "已通过持久介质回读校验")}</p>
|
|
5171
|
-
<p>保留策略:{esc(durability.get("retentionPolicy") or "N/A")}</p>
|
|
5172
|
-
<p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
|
|
5173
|
-
<p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
|
|
5174
|
-
<details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
|
|
5175
|
-
<details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
|
|
5176
|
-
<details><summary>技术元数据</summary><div><p>productCommit=<code>{esc(product_commit)}</code></p>
|
|
5177
|
-
<p>schemaVersion={esc(summary.get("schemaVersion"))}</p></div></details>
|
|
5178
|
-
</main></body></html>
|
|
5179
|
-
"""
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
# P1 slim-files: final-summary.html is a regenerable presentation layer over
|
|
5184
|
-
# summary-data.json. `finalize --no-html` skips local HTML rendering entirely
|
|
5185
|
-
# (a remote platform can render from summary-data.json); render failures are
|
|
5186
|
-
# downgraded to warnings instead of rolling back the archive.
|
|
5187
|
-
RENDER_HTML_ENABLED = True
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
def render_final_summary(
|
|
5191
|
-
change_dir: Path,
|
|
5192
|
-
summary_path: Path,
|
|
5193
|
-
) -> dict[str, Any]:
|
|
5194
|
-
"""Render final-summary.html via Node; fall back to a Python renderer.
|
|
5195
|
-
|
|
5196
|
-
Returns ``{ok, renderer, fallbackReason, out_path}``:
|
|
5197
|
-
- Node success -> renderer="node".
|
|
5198
|
-
- Node unavailable/timeout/non-zero/no-file -> Python fallback; success ->
|
|
5199
|
-
renderer="python-fallback" (fallbackReason carries the node failure cause).
|
|
5200
|
-
- Both fail or produce no file -> ok=False (caller downgrades to warning).
|
|
5201
|
-
- Rendering disabled (--no-html) -> ok=True with renderer="skipped".
|
|
5202
|
-
"""
|
|
5203
|
-
out_path = change_dir / "reports" / "final" / "final-summary.html"
|
|
5204
|
-
if not RENDER_HTML_ENABLED:
|
|
5205
|
-
return {
|
|
5206
|
-
"ok": True,
|
|
5207
|
-
"renderer": "skipped",
|
|
5208
|
-
"fallbackReason": "",
|
|
5209
|
-
"out_path": str(out_path),
|
|
5210
|
-
"skipped": True,
|
|
5211
|
-
}
|
|
5212
|
-
project = find_project_root(change_dir)
|
|
5213
|
-
node = resolve_node_path(project)
|
|
5214
|
-
fallback_reason = ""
|
|
5215
|
-
|
|
5216
|
-
if node and RENDER_SCRIPT.is_file():
|
|
5217
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
5218
|
-
try:
|
|
5219
|
-
proc = subprocess.run(
|
|
5220
|
-
[
|
|
5221
|
-
node,
|
|
5222
|
-
str(RENDER_SCRIPT),
|
|
5223
|
-
"--summary",
|
|
5224
|
-
str(summary_path),
|
|
5225
|
-
"--out",
|
|
5226
|
-
str(out_path),
|
|
5227
|
-
],
|
|
5228
|
-
capture_output=True,
|
|
5229
|
-
text=True,
|
|
5230
|
-
encoding="utf-8",
|
|
5231
|
-
errors="replace",
|
|
5232
|
-
timeout=60,
|
|
5233
|
-
check=False,
|
|
5234
|
-
)
|
|
5235
|
-
if proc.returncode == 0 and out_path.is_file():
|
|
5236
|
-
return {
|
|
5237
|
-
"ok": True,
|
|
5238
|
-
"renderer": "node",
|
|
5239
|
-
"fallbackReason": "",
|
|
5240
|
-
"out_path": str(out_path),
|
|
5241
|
-
}
|
|
5242
|
-
fallback_reason = (
|
|
5243
|
-
f"node render exit {proc.returncode}: "
|
|
5244
|
-
f"{(proc.stderr or proc.stdout or '')[:200]}"
|
|
5245
|
-
)
|
|
5246
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
5247
|
-
fallback_reason = f"node render failed: {exc}"
|
|
5248
|
-
else:
|
|
5249
|
-
fallback_reason = (
|
|
5250
|
-
"node unavailable" if not node else f"renderer missing: {RENDER_SCRIPT}"
|
|
5251
|
-
)
|
|
5252
|
-
|
|
5253
|
-
# Python fallback
|
|
5254
|
-
try:
|
|
5255
|
-
summary = read_json(summary_path)
|
|
5256
|
-
html = render_fallback_html(summary)
|
|
5257
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
5258
|
-
out_path.write_text(html, encoding="utf-8", newline="\n")
|
|
5259
|
-
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
5260
|
-
return {
|
|
5261
|
-
"ok": False,
|
|
5262
|
-
"renderer": "none",
|
|
5263
|
-
"fallbackReason": f"{fallback_reason}; fallback failed: {exc}",
|
|
5264
|
-
"out_path": str(out_path),
|
|
5265
|
-
}
|
|
5266
|
-
if out_path.is_file():
|
|
5267
|
-
return {
|
|
5268
|
-
"ok": True,
|
|
5269
|
-
"renderer": "python-fallback",
|
|
5270
|
-
"fallbackReason": fallback_reason,
|
|
5271
|
-
"out_path": str(out_path),
|
|
5272
|
-
}
|
|
5273
|
-
return {
|
|
5274
|
-
"ok": False,
|
|
5275
|
-
"renderer": "none",
|
|
5276
|
-
"fallbackReason": f"{fallback_reason}; fallback produced no file",
|
|
5277
|
-
"out_path": str(out_path),
|
|
5278
|
-
}
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
# ---------------------------------------------------------------------------
|
|
5282
|
-
# validate
|
|
5283
|
-
# ---------------------------------------------------------------------------
|
|
5284
4692
|
|
|
5285
4693
|
|
|
5286
4694
|
def _manifest_self_stats(
|
|
@@ -5312,22 +4720,24 @@ def _append_finalize_failure_terminal(
|
|
|
5312
4720
|
*,
|
|
5313
4721
|
operation_id: str,
|
|
5314
4722
|
) -> None:
|
|
5315
|
-
"""
|
|
4723
|
+
"""Record a blocked preparation without manufacturing an archive attempt."""
|
|
5316
4724
|
if not authoritative_change_dir.is_dir():
|
|
5317
4725
|
return
|
|
5318
4726
|
try:
|
|
5319
4727
|
append_event(
|
|
5320
4728
|
authoritative_change_dir,
|
|
5321
4729
|
phase="archive",
|
|
5322
|
-
type_="phase.start",
|
|
5323
|
-
note=
|
|
4730
|
+
type_="phase.prepare.start",
|
|
4731
|
+
note="开始检查归档条件",
|
|
5324
4732
|
)
|
|
5325
4733
|
append_event(
|
|
5326
4734
|
authoritative_change_dir,
|
|
5327
4735
|
phase="archive",
|
|
5328
|
-
type_="phase.end",
|
|
5329
|
-
status="
|
|
5330
|
-
|
|
4736
|
+
type_="phase.prepare.end",
|
|
4737
|
+
status="BLOCKED",
|
|
4738
|
+
code="ARCHIVE_PREPARE_BLOCKED",
|
|
4739
|
+
message=f"归档条件未满足:{message}",
|
|
4740
|
+
note=f"归档操作 {operation_id} 未进入正式执行",
|
|
5331
4741
|
)
|
|
5332
4742
|
except OSError:
|
|
5333
4743
|
pass
|
|
@@ -5570,256 +4980,57 @@ def validate_source_consistency(
|
|
|
5570
4980
|
}
|
|
5571
4981
|
|
|
5572
4982
|
|
|
5573
|
-
def
|
|
5574
|
-
summary
|
|
5575
|
-
html_path: Path | None,
|
|
5576
|
-
*,
|
|
5577
|
-
render_skipped: bool = False,
|
|
5578
|
-
) -> dict[str, Any]:
|
|
5579
|
-
"""Validate final-summary covers summary-data key facts (in-process)."""
|
|
4983
|
+
def validate_summary_data(summary: dict[str, Any]) -> dict[str, Any]:
|
|
4984
|
+
"""Validate canonical summary data without creating an HTML projection."""
|
|
5580
4985
|
issues: list[dict[str, str]] = []
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
return True
|
|
5611
|
-
return needle in html
|
|
5612
|
-
|
|
5613
|
-
# change id
|
|
5614
|
-
if html and change_id and not has_text(change_id):
|
|
4986
|
+
verification = summary.get("verification") or {}
|
|
4987
|
+
verification = verification if isinstance(verification, dict) else {}
|
|
4988
|
+
unit = verification.get("unitTests") or {}
|
|
4989
|
+
api = verification.get("apiTests") or {}
|
|
4990
|
+
browser = verification.get("browserE2E") or {}
|
|
4991
|
+
unit = unit if isinstance(unit, dict) else {}
|
|
4992
|
+
api = api if isinstance(api, dict) else {}
|
|
4993
|
+
browser = browser if isinstance(browser, dict) else {}
|
|
4994
|
+
api_status = str(api.get("status") or "")
|
|
4995
|
+
browser_status = str(browser.get("status") or "")
|
|
4996
|
+
final_status = str(summary.get("finalStatus") or "")
|
|
4997
|
+
has_skip = api_status == "USER_SKIPPED" or browser_status in {
|
|
4998
|
+
"USER_SKIPPED",
|
|
4999
|
+
"BLOCKED",
|
|
5000
|
+
"BLOCKED_BY_ENV",
|
|
5001
|
+
"NOT_RUN",
|
|
5002
|
+
"PARTIAL",
|
|
5003
|
+
} or str(verification.get("dbCompatibility") or "") == "BLOCKED_BY_DBA"
|
|
5004
|
+
has_failed_verification = (
|
|
5005
|
+
int(unit.get("failures") or 0) > 0
|
|
5006
|
+
or int(unit.get("errors") or 0) > 0
|
|
5007
|
+
or int(api.get("failed") or 0) > 0
|
|
5008
|
+
or int(browser.get("failed") or 0) > 0
|
|
5009
|
+
or browser_status == "FAIL"
|
|
5010
|
+
)
|
|
5011
|
+
stage = summary.get("stageStatus") or {}
|
|
5012
|
+
stage = stage if isinstance(stage, dict) else {}
|
|
5013
|
+
has_failed_stage = any(str(value).upper() == "FAIL" for value in stage.values())
|
|
5014
|
+
if (has_skip or has_failed_verification or has_failed_stage) and final_status == "OK":
|
|
5615
5015
|
issues.append(
|
|
5616
5016
|
{
|
|
5617
|
-
"code": "
|
|
5017
|
+
"code": "status-contradiction",
|
|
5618
5018
|
"severity": "error",
|
|
5619
|
-
"message":
|
|
5019
|
+
"message": (
|
|
5020
|
+
"finalStatus is pure OK but skipped, blocked, or failed "
|
|
5021
|
+
"verification is present"
|
|
5022
|
+
),
|
|
5620
5023
|
}
|
|
5621
5024
|
)
|
|
5622
|
-
|
|
5623
|
-
# key commands — stock render-summary.mjs does not embed reportPipeline.commands.
|
|
5624
|
-
# Require commands to be present in summary-data; HTML absence is warning only.
|
|
5625
|
-
commands = (summary.get("reportPipeline") or {}).get("commands") or []
|
|
5626
|
-
if html:
|
|
5627
|
-
for cmd in commands[:8]:
|
|
5628
|
-
c = str(cmd.get("command") or "").strip()
|
|
5629
|
-
if not c:
|
|
5630
|
-
continue
|
|
5631
|
-
fragment = c if len(c) <= 60 else c[:60]
|
|
5632
|
-
token = c.split()[-1] if c.split() else c
|
|
5633
|
-
in_html = (
|
|
5634
|
-
fragment in html
|
|
5635
|
-
or _html_escape(fragment) in html
|
|
5636
|
-
or (len(token) >= 4 and (token in html or _html_escape(token) in html))
|
|
5637
|
-
)
|
|
5638
|
-
if not in_html:
|
|
5639
|
-
issues.append(
|
|
5640
|
-
{
|
|
5641
|
-
"code": "missing-command",
|
|
5642
|
-
"severity": "warning",
|
|
5643
|
-
"message": (
|
|
5644
|
-
f"final-summary HTML omits command (renderer may not "
|
|
5645
|
-
f"embed commands): {c}"
|
|
5646
|
-
),
|
|
5647
|
-
}
|
|
5648
|
-
)
|
|
5649
|
-
|
|
5650
|
-
# verification
|
|
5651
|
-
ver = summary.get("verification") or {}
|
|
5652
|
-
unit = ver.get("unitTests") or {}
|
|
5653
|
-
api = ver.get("apiTests") or {}
|
|
5654
|
-
browser = ver.get("browserE2E") or {}
|
|
5655
|
-
pass_rate = unit.get("passRate")
|
|
5656
|
-
if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
|
|
5657
|
-
issues.append(
|
|
5658
|
-
{
|
|
5659
|
-
"code": "missing-verification",
|
|
5660
|
-
"severity": "warning",
|
|
5661
|
-
"message": f"unitTests.passRate '{pass_rate}' not in final-summary",
|
|
5662
|
-
}
|
|
5663
|
-
)
|
|
5664
|
-
api_status = str(api.get("status") or "")
|
|
5665
|
-
if api_status and api_status not in {"", NOT_AVAILABLE} and api_status not in html:
|
|
5666
|
-
# Renderer shows api status; soft warning
|
|
5667
|
-
if api_status in {"USER_SKIPPED", "BLOCKED_BY_DBA", "NOT_RUN", "PARTIAL"}:
|
|
5668
|
-
issues.append(
|
|
5669
|
-
{
|
|
5670
|
-
"code": "missing-verification",
|
|
5671
|
-
"severity": "warning",
|
|
5672
|
-
"message": f"apiTests.status '{api_status}' not visible in final-summary",
|
|
5673
|
-
}
|
|
5674
|
-
)
|
|
5675
|
-
browser_status = str(browser.get("status") or "")
|
|
5676
|
-
if (
|
|
5677
|
-
browser_status
|
|
5678
|
-
and browser_status not in {"", NOT_AVAILABLE}
|
|
5679
|
-
and browser_status not in html
|
|
5680
|
-
):
|
|
5681
|
-
issues.append(
|
|
5682
|
-
{
|
|
5683
|
-
"code": "missing-verification",
|
|
5684
|
-
"severity": "warning",
|
|
5685
|
-
"message": (
|
|
5686
|
-
f"browserE2E.status '{browser_status}' not visible "
|
|
5687
|
-
"in final-summary"
|
|
5688
|
-
),
|
|
5689
|
-
}
|
|
5690
|
-
)
|
|
5691
|
-
|
|
5692
|
-
# artifacts / summary-data path hints
|
|
5693
|
-
am = summary.get("archiveManifest") or {}
|
|
5694
|
-
total = am.get("totalArchiveFiles")
|
|
5695
|
-
if total is not None and str(total) not in html:
|
|
5696
|
-
issues.append(
|
|
5697
|
-
{
|
|
5698
|
-
"code": "missing-artifact",
|
|
5699
|
-
"severity": "warning",
|
|
5700
|
-
"message": "archiveManifest.totalArchiveFiles not reflected in final-summary",
|
|
5701
|
-
}
|
|
5702
|
-
)
|
|
5703
|
-
|
|
5704
|
-
# risks / manual actions — empty arrays are OK (placeholder)
|
|
5705
|
-
for risk in summary.get("knownRisks") or []:
|
|
5706
|
-
# UT-016/RET-28: canonical projection — structured risk objects
|
|
5707
|
-
# project to their message; str(dict) never matches rendered HTML.
|
|
5708
|
-
if isinstance(risk, dict):
|
|
5709
|
-
text = str(risk.get("message") or risk.get("summary") or "").strip()
|
|
5710
|
-
else:
|
|
5711
|
-
text = str(risk)
|
|
5712
|
-
if text and text not in html and _html_escape(text) not in html:
|
|
5713
|
-
issues.append(
|
|
5714
|
-
{
|
|
5715
|
-
"code": "missing-risk",
|
|
5716
|
-
"severity": "warning",
|
|
5717
|
-
"message": f"knownRisk not in final-summary: {text[:80]}",
|
|
5718
|
-
}
|
|
5719
|
-
)
|
|
5720
|
-
|
|
5721
|
-
# status contradiction
|
|
5722
|
-
final_status = str(summary.get("finalStatus") or "")
|
|
5723
|
-
browser_conditional = {
|
|
5724
|
-
"USER_SKIPPED",
|
|
5725
|
-
"BLOCKED",
|
|
5726
|
-
"BLOCKED_BY_ENV",
|
|
5727
|
-
"NOT_RUN",
|
|
5728
|
-
"PARTIAL",
|
|
5729
|
-
}
|
|
5730
|
-
has_skip = api_status == "USER_SKIPPED" or browser_status in browser_conditional or str(
|
|
5731
|
-
ver.get("dbCompatibility") or ""
|
|
5732
|
-
) == "BLOCKED_BY_DBA"
|
|
5733
|
-
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
5734
|
-
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5735
|
-
has_fail_ver = (
|
|
5736
|
-
has_fail_ver
|
|
5737
|
-
or int(browser.get("failed") or 0) > 0
|
|
5738
|
-
or browser_status == "FAIL"
|
|
5739
|
-
)
|
|
5740
|
-
stage = summary.get("stageStatus") or {}
|
|
5741
|
-
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
5742
|
-
|
|
5743
|
-
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
5744
|
-
issues.append(
|
|
5745
|
-
{
|
|
5746
|
-
"code": "status-contradiction",
|
|
5747
|
-
"severity": "error",
|
|
5748
|
-
"message": (
|
|
5749
|
-
"finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
|
|
5750
|
-
"failed verification present"
|
|
5751
|
-
),
|
|
5752
|
-
}
|
|
5753
|
-
)
|
|
5754
|
-
if has_skip and html:
|
|
5755
|
-
if re.search(r">\s*OK\s*<", html) and "CONDITIONAL" not in html.upper():
|
|
5756
|
-
if final_status != "CONDITIONAL_OK":
|
|
5757
|
-
issues.append(
|
|
5758
|
-
{
|
|
5759
|
-
"code": "status-contradiction",
|
|
5760
|
-
"severity": "error",
|
|
5761
|
-
"message": "final-summary shows pure OK despite USER_SKIPPED/BLOCKED",
|
|
5762
|
-
}
|
|
5763
|
-
)
|
|
5764
|
-
else:
|
|
5765
|
-
# No HTML (and not render_skipped handled above): still check data-level status rules
|
|
5766
|
-
ver = summary.get("verification") or {}
|
|
5767
|
-
unit = ver.get("unitTests") or {}
|
|
5768
|
-
api = ver.get("apiTests") or {}
|
|
5769
|
-
browser = ver.get("browserE2E") or {}
|
|
5770
|
-
api_status = str(api.get("status") or "")
|
|
5771
|
-
browser_status = str(browser.get("status") or "")
|
|
5772
|
-
final_status = str(summary.get("finalStatus") or "")
|
|
5773
|
-
has_skip = api_status == "USER_SKIPPED" or browser_status in {
|
|
5774
|
-
"USER_SKIPPED",
|
|
5775
|
-
"BLOCKED",
|
|
5776
|
-
"BLOCKED_BY_ENV",
|
|
5777
|
-
"NOT_RUN",
|
|
5778
|
-
"PARTIAL",
|
|
5779
|
-
} or str(
|
|
5780
|
-
ver.get("dbCompatibility") or ""
|
|
5781
|
-
) == "BLOCKED_BY_DBA"
|
|
5782
|
-
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
5783
|
-
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5784
|
-
has_fail_ver = (
|
|
5785
|
-
has_fail_ver
|
|
5786
|
-
or int(browser.get("failed") or 0) > 0
|
|
5787
|
-
or browser_status == "FAIL"
|
|
5788
|
-
)
|
|
5789
|
-
stage = summary.get("stageStatus") or {}
|
|
5790
|
-
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
5791
|
-
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
5792
|
-
issues.append(
|
|
5793
|
-
{
|
|
5794
|
-
"code": "status-contradiction",
|
|
5795
|
-
"severity": "error",
|
|
5796
|
-
"message": (
|
|
5797
|
-
"finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
|
|
5798
|
-
"failed verification present"
|
|
5799
|
-
),
|
|
5800
|
-
}
|
|
5801
|
-
)
|
|
5802
|
-
|
|
5803
|
-
errors = [i for i in issues if i.get("severity") == "error"]
|
|
5804
|
-
warnings = [i for i in issues if i.get("severity") != "error"]
|
|
5025
|
+
errors = [item for item in issues if item.get("severity") == "error"]
|
|
5805
5026
|
return {
|
|
5806
|
-
"ok":
|
|
5027
|
+
"ok": not errors,
|
|
5807
5028
|
"issues": issues,
|
|
5808
5029
|
"error_count": len(errors),
|
|
5809
|
-
"warning_count": len(
|
|
5030
|
+
"warning_count": len(issues) - len(errors),
|
|
5810
5031
|
}
|
|
5811
5032
|
|
|
5812
5033
|
|
|
5813
|
-
def _html_escape(text: str) -> str:
|
|
5814
|
-
return (
|
|
5815
|
-
text.replace("&", "&")
|
|
5816
|
-
.replace("<", "<")
|
|
5817
|
-
.replace(">", ">")
|
|
5818
|
-
.replace('"', """)
|
|
5819
|
-
.replace("'", "'")
|
|
5820
|
-
)
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
5034
|
# ---------------------------------------------------------------------------
|
|
5824
5035
|
# knowledge / service post-steps
|
|
5825
5036
|
# ---------------------------------------------------------------------------
|
|
@@ -8125,32 +7336,12 @@ def cmd_finalize(
|
|
|
8125
7336
|
payload["ok"] = False
|
|
8126
7337
|
return 1, payload
|
|
8127
7338
|
|
|
8128
|
-
# --- 6.
|
|
8129
|
-
render_result = render_final_summary(work_dir, summary_path)
|
|
8130
|
-
payload["steps"]["render"] = render_result
|
|
8131
|
-
if not render_result.get("ok"):
|
|
8132
|
-
# final-summary.html 是 summary-data.json 的可再生展示层:渲染失败
|
|
8133
|
-
# 降级为 warning,归档继续(平台端可按 summary-data 重新渲染)。
|
|
8134
|
-
msg = str(render_result.get("fallbackReason") or "render failed")
|
|
8135
|
-
warnings.append(f"final-summary render failed (archive continues): {msg}")
|
|
8136
|
-
renderer = render_result.get("renderer")
|
|
8137
|
-
if renderer == "python-fallback" and render_result.get("fallbackReason"):
|
|
8138
|
-
warnings.append(
|
|
8139
|
-
f"node render unavailable; used python-fallback: "
|
|
8140
|
-
f"{render_result.get('fallbackReason')}"
|
|
8141
|
-
)
|
|
8142
|
-
|
|
8143
|
-
html_path = work_dir / "reports" / "final" / "final-summary.html"
|
|
8144
|
-
|
|
8145
|
-
# --- 7. renderer consistency (layer 2) ---
|
|
7339
|
+
# --- 6. canonical summary validation ---
|
|
8146
7340
|
try:
|
|
8147
7341
|
summary = read_json(summary_path)
|
|
8148
7342
|
except (OSError, json.JSONDecodeError):
|
|
8149
7343
|
pass
|
|
8150
|
-
validate_result =
|
|
8151
|
-
summary,
|
|
8152
|
-
html_path if html_path.is_file() else None,
|
|
8153
|
-
)
|
|
7344
|
+
validate_result = validate_summary_data(summary)
|
|
8154
7345
|
payload["steps"]["validate"] = validate_result
|
|
8155
7346
|
summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get(
|
|
8156
7347
|
"issues"
|
|
@@ -8169,10 +7360,10 @@ def cmd_finalize(
|
|
|
8169
7360
|
warnings.append(f"archive-meta write failed: {exc}")
|
|
8170
7361
|
payload["steps"]["archive_meta"] = {"ok": False, "error": str(exc)}
|
|
8171
7362
|
|
|
8172
|
-
# --- 9/10. final summary stats
|
|
8173
|
-
# Post-manifest rewrites of covered bytes are forbidden. We update
|
|
8174
|
-
#
|
|
8175
|
-
# If summary
|
|
7363
|
+
# --- 9/10. final summary stats, then LAST manifest (IA-7) ---
|
|
7364
|
+
# Post-manifest rewrites of covered bytes are forbidden. We update the
|
|
7365
|
+
# summary first, regenerate after-manifest last, then verify on-disk hashes.
|
|
7366
|
+
# If summary must still change after that, it is excluded with a reason.
|
|
8176
7367
|
summary = read_json(summary_path)
|
|
8177
7368
|
summary["archiveManifest"] = {
|
|
8178
7369
|
"movedFiles": 0,
|
|
@@ -8181,14 +7372,8 @@ def cmd_finalize(
|
|
|
8181
7372
|
"checksumStatus": "PENDING",
|
|
8182
7373
|
}
|
|
8183
7374
|
write_json(summary_path, summary)
|
|
8184
|
-
render_result = render_final_summary(work_dir, summary_path)
|
|
8185
|
-
if not render_result.get("ok"):
|
|
8186
|
-
warnings.append(
|
|
8187
|
-
"final summary re-render failed (archive continues): "
|
|
8188
|
-
f"{render_result.get('fallbackReason') or render_result.get('error')}"
|
|
8189
|
-
)
|
|
8190
7375
|
summary = read_json(summary_path)
|
|
8191
|
-
validate_result =
|
|
7376
|
+
validate_result = validate_summary_data(summary)
|
|
8192
7377
|
payload["steps"]["validate"] = validate_result
|
|
8193
7378
|
summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get(
|
|
8194
7379
|
"issues"
|
|
@@ -8214,25 +7399,15 @@ def cmd_finalize(
|
|
|
8214
7399
|
"reports/final/summary-data.json": (
|
|
8215
7400
|
"archiveManifest stats written after coverage snapshot"
|
|
8216
7401
|
),
|
|
8217
|
-
"reports/final/final-summary.html": (
|
|
8218
|
-
"re-rendered after coverage snapshot to display manifest stats"
|
|
8219
|
-
),
|
|
8220
7402
|
},
|
|
8221
7403
|
**_manifest_self_stats(work_dir, after_manifest, after_path),
|
|
8222
7404
|
}
|
|
8223
7405
|
write_json(summary_path, summary)
|
|
8224
|
-
render_result = render_final_summary(work_dir, summary_path)
|
|
8225
|
-
if not render_result.get("ok"):
|
|
8226
|
-
warnings.append(
|
|
8227
|
-
"post-manifest summary render failed (archive continues): "
|
|
8228
|
-
f"{render_result.get('fallbackReason') or render_result.get('error')}"
|
|
8229
|
-
)
|
|
8230
7406
|
coverage = verify_manifest_byte_coverage(
|
|
8231
7407
|
work_dir,
|
|
8232
7408
|
after_manifest,
|
|
8233
7409
|
exclude_paths=[
|
|
8234
7410
|
"reports/final/summary-data.json",
|
|
8235
|
-
"reports/final/final-summary.html",
|
|
8236
7411
|
],
|
|
8237
7412
|
exclusion_reasons=summary["archiveManifest"]["exclusionReasons"],
|
|
8238
7413
|
)
|
|
@@ -8288,12 +7463,8 @@ def cmd_finalize(
|
|
|
8288
7463
|
]
|
|
8289
7464
|
payload["releaseEligible"] = summary["releaseEligible"]
|
|
8290
7465
|
write_json(summary_path, summary)
|
|
8291
|
-
# Re-render once more for checksumStatus display; remain excluded.
|
|
8292
|
-
render_final_summary(work_dir, summary_path)
|
|
8293
7466
|
summary = read_json(summary_path)
|
|
8294
|
-
validate_result =
|
|
8295
|
-
summary, html_path if html_path.is_file() else None
|
|
8296
|
-
)
|
|
7467
|
+
validate_result = validate_summary_data(summary)
|
|
8297
7468
|
payload["steps"]["validate"] = validate_result
|
|
8298
7469
|
summary.setdefault("reportPipeline", {})["validationIssues"] = (
|
|
8299
7470
|
validate_result.get("issues") or []
|
|
@@ -8407,7 +7578,6 @@ def cmd_finalize(
|
|
|
8407
7578
|
shutil.rmtree(operation_temp_dir.parent, ignore_errors=True)
|
|
8408
7579
|
work_dir = archive_dir
|
|
8409
7580
|
summary_path = work_dir / "reports" / "final" / "summary-data.json"
|
|
8410
|
-
html_path = work_dir / "reports" / "final" / "final-summary.html"
|
|
8411
7581
|
payload["steps"]["move"] = {"ok": True, "to": str(archive_dir)}
|
|
8412
7582
|
except OSError as exc:
|
|
8413
7583
|
payload["error"] = f"publish failed: {exc}"
|
|
@@ -8508,7 +7678,6 @@ def cmd_finalize(
|
|
|
8508
7678
|
payload["finalStatus"] = "OK"
|
|
8509
7679
|
payload["warnings"] = warnings
|
|
8510
7680
|
payload["summary_data"] = str(summary_path)
|
|
8511
|
-
payload["final_summary"] = str(html_path) if html_path.is_file() else None
|
|
8512
7681
|
return 0, payload
|
|
8513
7682
|
|
|
8514
7683
|
|
|
@@ -8845,6 +8014,59 @@ def _deterministic_zip_info(path: str) -> zipfile.ZipInfo:
|
|
|
8845
8014
|
return info
|
|
8846
8015
|
|
|
8847
8016
|
|
|
8017
|
+
_WINDOWS_LOCAL_PATH_RE = re.compile(
|
|
8018
|
+
r"\b[A-Za-z]:\\(?:[^\s<>:\"|?*]+\\)*[^\s<>:\"|?*]*"
|
|
8019
|
+
)
|
|
8020
|
+
|
|
8021
|
+
|
|
8022
|
+
def _archive_remote_json_value(value: Any) -> Any:
|
|
8023
|
+
"""Remove machine-local absolute paths from the remote JSON projection."""
|
|
8024
|
+
if isinstance(value, dict):
|
|
8025
|
+
return {
|
|
8026
|
+
key: _archive_remote_json_value(item)
|
|
8027
|
+
for key, item in value.items()
|
|
8028
|
+
}
|
|
8029
|
+
if isinstance(value, list):
|
|
8030
|
+
return [_archive_remote_json_value(item) for item in value]
|
|
8031
|
+
if not isinstance(value, str):
|
|
8032
|
+
return value
|
|
8033
|
+
if re.match(r"^[A-Za-z]:[\\/]", value) or value.startswith("\\\\"):
|
|
8034
|
+
return "<local-path>"
|
|
8035
|
+
return _WINDOWS_LOCAL_PATH_RE.sub("<local-path>", value)
|
|
8036
|
+
|
|
8037
|
+
|
|
8038
|
+
def _archive_remote_json_bytes(raw: bytes) -> bytes:
|
|
8039
|
+
value = json.loads(raw)
|
|
8040
|
+
projected = _archive_remote_json_value(value)
|
|
8041
|
+
return (
|
|
8042
|
+
json.dumps(
|
|
8043
|
+
projected,
|
|
8044
|
+
ensure_ascii=False,
|
|
8045
|
+
sort_keys=True,
|
|
8046
|
+
separators=(",", ":"),
|
|
8047
|
+
)
|
|
8048
|
+
+ "\n"
|
|
8049
|
+
).encode("utf-8")
|
|
8050
|
+
|
|
8051
|
+
|
|
8052
|
+
def resolve_npx_launcher() -> list[str]:
|
|
8053
|
+
"""Resolve npx without asking CreateProcess to execute a Windows .CMD shim."""
|
|
8054
|
+
npx = shutil.which("npx")
|
|
8055
|
+
if not npx:
|
|
8056
|
+
raise FileNotFoundError("npx is not installed or not available on PATH")
|
|
8057
|
+
if os.name != "nt":
|
|
8058
|
+
return [npx]
|
|
8059
|
+
|
|
8060
|
+
node = shutil.which("node")
|
|
8061
|
+
if not node:
|
|
8062
|
+
raise FileNotFoundError("node is required to launch npx on Windows")
|
|
8063
|
+
npx_path = Path(npx)
|
|
8064
|
+
npx_cli = npx_path.parent / "node_modules" / "npm" / "bin" / "npx-cli.js"
|
|
8065
|
+
if not npx_cli.is_file():
|
|
8066
|
+
raise FileNotFoundError(f"npx-cli.js not found beside Node.js: {npx_cli}")
|
|
8067
|
+
return [node, str(npx_cli)]
|
|
8068
|
+
|
|
8069
|
+
|
|
8848
8070
|
def build_archive_package(
|
|
8849
8071
|
project_root: Path,
|
|
8850
8072
|
archive_dir: Path,
|
|
@@ -8875,7 +8097,7 @@ def build_archive_package(
|
|
|
8875
8097
|
# All semantic archive inputs are text and must round-trip as UTF-8.
|
|
8876
8098
|
raw.decode("utf-8")
|
|
8877
8099
|
if media_type == "application/json":
|
|
8878
|
-
|
|
8100
|
+
raw = _archive_remote_json_bytes(raw)
|
|
8879
8101
|
content_by_path[package_path] = raw
|
|
8880
8102
|
entries.append(
|
|
8881
8103
|
{
|
|
@@ -9104,8 +8326,21 @@ def auto_push_archive_core(
|
|
|
9104
8326
|
|
|
9105
8327
|
# The published CLI owns credentials, project binding, durable receipt checks,
|
|
9106
8328
|
# and retries. Generic `push` is intentionally not used for archive bytes.
|
|
8329
|
+
try:
|
|
8330
|
+
launcher = resolve_npx_launcher()
|
|
8331
|
+
except OSError as exc:
|
|
8332
|
+
reason_code = "ARCHIVE_UPLOAD_DEFERRED"
|
|
8333
|
+
try:
|
|
8334
|
+
persist_receipt(upload_status="pending", reason_code=reason_code)
|
|
8335
|
+
except OSError:
|
|
8336
|
+
pass
|
|
8337
|
+
return {
|
|
8338
|
+
**base_result,
|
|
8339
|
+
"reasonCode": reason_code,
|
|
8340
|
+
"warning": f"归档自动上传已延后:{exc}",
|
|
8341
|
+
}
|
|
9107
8342
|
command = [
|
|
9108
|
-
|
|
8343
|
+
*launcher,
|
|
9109
8344
|
"--yes",
|
|
9110
8345
|
"hunter-harness",
|
|
9111
8346
|
"archive",
|
|
@@ -9359,13 +8594,7 @@ def cmd_replay(
|
|
|
9359
8594
|
# Collect without writing into the archive
|
|
9360
8595
|
summary = collect_summary_data(archive_dir, write=False, for_replay=True)
|
|
9361
8596
|
|
|
9362
|
-
|
|
9363
|
-
render_skipped = not html_path.is_file()
|
|
9364
|
-
validate_result = validate_summary(
|
|
9365
|
-
summary,
|
|
9366
|
-
html_path if html_path.is_file() else None,
|
|
9367
|
-
render_skipped=render_skipped,
|
|
9368
|
-
)
|
|
8597
|
+
validate_result = validate_summary_data(summary)
|
|
9369
8598
|
summary.setdefault("reportPipeline", {})["validationIssues"] = (
|
|
9370
8599
|
validate_result.get("issues") or []
|
|
9371
8600
|
)
|
|
@@ -9462,9 +8691,6 @@ def cmd_certify_local_cli(args: argparse.Namespace) -> int:
|
|
|
9462
8691
|
|
|
9463
8692
|
|
|
9464
8693
|
def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
9465
|
-
global RENDER_HTML_ENABLED
|
|
9466
|
-
if getattr(args, "no_html", False):
|
|
9467
|
-
RENDER_HTML_ENABLED = False
|
|
9468
8694
|
change_dir = resolve_path(args.change_dir)
|
|
9469
8695
|
archive_root = resolve_path(args.archive_root)
|
|
9470
8696
|
durable_root = (
|
|
@@ -9516,50 +8742,12 @@ def cmd_replay_cli(args: argparse.Namespace) -> int:
|
|
|
9516
8742
|
return code
|
|
9517
8743
|
|
|
9518
8744
|
|
|
9519
|
-
def _render_html_to(
|
|
9520
|
-
change_dir: Path,
|
|
9521
|
-
summary_path: Path,
|
|
9522
|
-
out_path: Path,
|
|
9523
|
-
) -> dict[str, Any]:
|
|
9524
|
-
"""Render summary HTML to an arbitrary path (node first, python fallback).
|
|
9525
|
-
|
|
9526
|
-
Unlike ``render_final_summary`` this never touches the canonical
|
|
9527
|
-
``reports/final/final-summary.html`` — repair renders into staging.
|
|
9528
|
-
"""
|
|
9529
|
-
project = find_project_root(change_dir)
|
|
9530
|
-
node = resolve_node_path(project)
|
|
9531
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
9532
|
-
if node and RENDER_SCRIPT.is_file():
|
|
9533
|
-
try:
|
|
9534
|
-
proc = subprocess.run(
|
|
9535
|
-
[node, str(RENDER_SCRIPT), "--summary", str(summary_path), "--out", str(out_path)],
|
|
9536
|
-
capture_output=True,
|
|
9537
|
-
text=True,
|
|
9538
|
-
encoding="utf-8",
|
|
9539
|
-
errors="replace",
|
|
9540
|
-
timeout=60,
|
|
9541
|
-
check=False,
|
|
9542
|
-
)
|
|
9543
|
-
if proc.returncode == 0 and out_path.is_file():
|
|
9544
|
-
return {"ok": True, "renderer": "node", "out_path": str(out_path)}
|
|
9545
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
9546
|
-
pass
|
|
9547
|
-
try:
|
|
9548
|
-
summary = read_json(summary_path)
|
|
9549
|
-
html = render_fallback_html(summary)
|
|
9550
|
-
out_path.write_text(html, encoding="utf-8", newline="\n")
|
|
9551
|
-
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
9552
|
-
return {"ok": False, "renderer": "none", "error": str(exc)}
|
|
9553
|
-
return {"ok": True, "renderer": "python-fallback", "out_path": str(out_path)}
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
8745
|
def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
9557
8746
|
"""Versioned repair (task 11 / RET-40).
|
|
9558
8747
|
|
|
9559
|
-
Re-collect a candidate from the frozen sources,
|
|
9560
|
-
|
|
9561
|
-
original summary
|
|
9562
|
-
pointer moves only when both validators pass.
|
|
8748
|
+
Re-collect a candidate from the frozen sources, validate its source and
|
|
8749
|
+
canonical data, then write an immutable ``derived/v<N>/`` plus a repair
|
|
8750
|
+
record. The original summary and manifest are never overwritten.
|
|
9563
8751
|
"""
|
|
9564
8752
|
payload: dict[str, Any] = {
|
|
9565
8753
|
"ok": False,
|
|
@@ -9596,25 +8784,18 @@ def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
|
9596
8784
|
payload["error"] = f"repair collect failed: {exc}"
|
|
9597
8785
|
return 1, payload
|
|
9598
8786
|
|
|
9599
|
-
# 2. stage outside the archive;
|
|
8787
|
+
# 2. stage outside the archive; validate canonical data only.
|
|
9600
8788
|
staging = Path(tempfile.mkdtemp(prefix="harness-repair-"))
|
|
9601
8789
|
try:
|
|
9602
8790
|
staged_summary = staging / "summary-data.json"
|
|
9603
8791
|
write_json(staged_summary, candidate)
|
|
9604
8792
|
source_result = validate_source_consistency(archive_dir, candidate)
|
|
9605
|
-
|
|
9606
|
-
archive_dir, staged_summary, staging / "final-summary.html"
|
|
9607
|
-
)
|
|
9608
|
-
staged_html = staging / "final-summary.html"
|
|
9609
|
-
renderer_result = validate_summary(
|
|
9610
|
-
candidate,
|
|
9611
|
-
staged_html if staged_html.is_file() else None,
|
|
9612
|
-
)
|
|
8793
|
+
summary_result = validate_summary_data(candidate)
|
|
9613
8794
|
payload["validators"] = {
|
|
9614
8795
|
"source": source_result,
|
|
9615
|
-
"
|
|
8796
|
+
"summary": summary_result,
|
|
9616
8797
|
}
|
|
9617
|
-
if not (source_result.get("ok") and
|
|
8798
|
+
if not (source_result.get("ok") and summary_result.get("ok")):
|
|
9618
8799
|
payload["error"] = "repair validators failed; derived version not written"
|
|
9619
8800
|
return 1, payload
|
|
9620
8801
|
|
|
@@ -9637,8 +8818,6 @@ def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
|
9637
8818
|
target_manifest = staged_version_dir / manifest_name
|
|
9638
8819
|
shutil.copy2(source_manifest, target_manifest)
|
|
9639
8820
|
frozen_manifest_hashes[manifest_name] = "sha256:" + sha256_file(target_manifest)
|
|
9640
|
-
if staged_html.is_file():
|
|
9641
|
-
shutil.copy2(staged_html, staged_version_dir / "final-summary.html")
|
|
9642
8821
|
record = {
|
|
9643
8822
|
"version": version,
|
|
9644
8823
|
"createdAt": now_iso(),
|
|
@@ -9647,7 +8826,7 @@ def cmd_repair(archive_dir: Path) -> tuple[int, dict[str, Any]]:
|
|
|
9647
8826
|
"frozenManifestHashes": frozen_manifest_hashes,
|
|
9648
8827
|
"validators": {
|
|
9649
8828
|
"source": {"ok": bool(source_result.get("ok")), "issues": source_result.get("issues") or []},
|
|
9650
|
-
"
|
|
8829
|
+
"summary": {"ok": bool(summary_result.get("ok")), "issues": summary_result.get("issues") or []},
|
|
9651
8830
|
},
|
|
9652
8831
|
}
|
|
9653
8832
|
write_json(staged_version_dir / "repair-record.json", record)
|
|
@@ -9769,11 +8948,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
9769
8948
|
default="release-candidate",
|
|
9770
8949
|
help="release-candidate enforces candidate evidence; record-only archives facts without release eligibility",
|
|
9771
8950
|
)
|
|
9772
|
-
p_fin.add_argument(
|
|
9773
|
-
"--no-html",
|
|
9774
|
-
action="store_true",
|
|
9775
|
-
help="skip local final-summary.html rendering (summary-data.json stays canonical)",
|
|
9776
|
-
)
|
|
9777
8951
|
p_fin.add_argument("--json", action="store_true", default=True)
|
|
9778
8952
|
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
9779
8953
|
|