@hunter-harness/workflow-harness 0.2.33 → 0.2.34
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/README.md +25 -25
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/claude-code/scripts/harness_context.py +717 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/claude-code/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/claude-code/scripts/harness_review.py +16 -1
- package/harness/bundles/general/claude-code/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +717 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +16 -1
- package/harness/bundles/general/codebuddy/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -0
- package/harness/bundles/general/codex/harness-test/reference.md +2 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/codex/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/codex/scripts/harness_context.py +717 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/codex/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/codex/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/codex/scripts/harness_review.py +16 -1
- package/harness/bundles/general/codex/scripts/harness_verification.py +114 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/general/cursor/scripts/harness_archive.py +404 -12
- package/harness/bundles/general/cursor/scripts/harness_context.py +717 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +77 -9
- package/harness/bundles/general/cursor/scripts/harness_phase.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +272 -15
- package/harness/bundles/general/cursor/scripts/harness_report_model.py +420 -0
- package/harness/bundles/general/cursor/scripts/harness_review.py +16 -1
- package/harness/bundles/general/cursor/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -0
- package/harness/bundles/java/claude-code/harness-test/reference.md +2 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/claude-code/scripts/harness_context.py +717 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/claude-code/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/claude-code/scripts/harness_review.py +16 -1
- package/harness/bundles/java/claude-code/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -0
- package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +717 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +16 -1
- package/harness/bundles/java/codebuddy/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-test/checklist.md +1 -0
- package/harness/bundles/java/codex/harness-test/reference.md +2 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/codex/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/codex/scripts/harness_context.py +717 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/codex/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/codex/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/codex/scripts/harness_review.py +16 -1
- package/harness/bundles/java/codex/scripts/harness_verification.py +114 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +215 -106
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +8 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
- 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 +3 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-test/checklist.md +1 -0
- package/harness/bundles/java/cursor/harness-test/reference.md +2 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -2
- package/harness/bundles/java/cursor/scripts/harness_archive.py +404 -12
- package/harness/bundles/java/cursor/scripts/harness_context.py +717 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +77 -9
- package/harness/bundles/java/cursor/scripts/harness_phase.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +272 -15
- package/harness/bundles/java/cursor/scripts/harness_report_model.py +420 -0
- package/harness/bundles/java/cursor/scripts/harness_review.py +16 -1
- package/harness/bundles/java/cursor/scripts/harness_verification.py +114 -0
- package/harness/manifests/general/claude-code.json +42 -26
- package/harness/manifests/general/codebuddy.json +42 -26
- package/harness/manifests/general/codex.json +42 -26
- package/harness/manifests/general/cursor.json +42 -26
- package/harness/manifests/java/claude-code.json +44 -28
- package/harness/manifests/java/codebuddy.json +44 -28
- package/harness/manifests/java/codex.json +44 -28
- package/harness/manifests/java/cursor.json +44 -28
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -74,6 +74,7 @@ import harness_gate as hgate # noqa: E402
|
|
|
74
74
|
import harness_ledger as hl # noqa: E402
|
|
75
75
|
import harness_paths as hp # noqa: E402
|
|
76
76
|
import harness_phase as hphase # noqa: E402
|
|
77
|
+
import harness_report_model as hrm # noqa: E402
|
|
77
78
|
import harness_review as hr # noqa: E402
|
|
78
79
|
|
|
79
80
|
|
|
@@ -1643,6 +1644,12 @@ def build_workflow_timing(
|
|
|
1643
1644
|
if started_dt is not None and cutoff_dt is not None
|
|
1644
1645
|
else 0
|
|
1645
1646
|
)
|
|
1647
|
+
# Segment-level integer conversion can lose sub-millisecond fractions at
|
|
1648
|
+
# each boundary. Attribute only that positive rounding residue to the
|
|
1649
|
+
# otherwise-unattributed bucket so the partition remains exact.
|
|
1650
|
+
rounding_residue = workflow_wall - sum(categories.values())
|
|
1651
|
+
if rounding_residue > 0:
|
|
1652
|
+
categories["unattributed"] += rounding_residue
|
|
1646
1653
|
stage_wall = union_ms(active_intervals)
|
|
1647
1654
|
sessions: list[dict[str, Any]] = []
|
|
1648
1655
|
session_start = started_dt
|
|
@@ -2645,10 +2652,9 @@ def _typed_test_metrics(entry: dict[str, Any], *, total_key: str) -> dict[str, A
|
|
|
2645
2652
|
"executionRate": f"{executed / total:.0%}" if total > 0 else NOT_AVAILABLE,
|
|
2646
2653
|
"source": "committed" if total > 0 else "not-run",
|
|
2647
2654
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
out["skipped"] = int(metrics.get("skipped", 0) or 0)
|
|
2655
|
+
for optional_count in ("blocked", "skipped", "retries"):
|
|
2656
|
+
if optional_count in metrics:
|
|
2657
|
+
out[optional_count] = int(metrics.get(optional_count, 0) or 0)
|
|
2652
2658
|
applicability = entry.get("applicability")
|
|
2653
2659
|
if isinstance(applicability, dict):
|
|
2654
2660
|
out["applicability"] = applicability
|
|
@@ -2665,18 +2671,22 @@ def build_verification_projection(
|
|
|
2665
2671
|
apiContract and browserE2E are distinct typed projections; the legacy
|
|
2666
2672
|
apiTests mapping is retained for schema compatibility.
|
|
2667
2673
|
"""
|
|
2668
|
-
|
|
2674
|
+
effective_ledger = _deepcopy_json(ledger or {})
|
|
2675
|
+
projected_validations = hrm.latest_terminal_validations(effective_ledger)
|
|
2676
|
+
if projected_validations:
|
|
2677
|
+
effective_ledger["validations"] = projected_validations
|
|
2678
|
+
validations = effective_ledger.get("validations") or {}
|
|
2669
2679
|
projection: dict[str, Any] = {
|
|
2670
|
-
"unitTests": _ledger_unit_tests(
|
|
2671
|
-
"apiTests": _ledger_api_tests(
|
|
2672
|
-
"dbCompatibility": _ledger_db_compat(
|
|
2680
|
+
"unitTests": _ledger_unit_tests(effective_ledger),
|
|
2681
|
+
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
2682
|
+
"dbCompatibility": _ledger_db_compat(effective_ledger),
|
|
2673
2683
|
}
|
|
2674
2684
|
api_contract = validations.get("apiContract")
|
|
2675
2685
|
if isinstance(api_contract, dict):
|
|
2676
2686
|
projection["apiContract"] = _typed_test_metrics(
|
|
2677
2687
|
api_contract, total_key="scenariosTotal"
|
|
2678
2688
|
)
|
|
2679
|
-
browser_e2e = validations.get("browserE2E")
|
|
2689
|
+
browser_e2e = validations.get("browserTest") or validations.get("browserE2E")
|
|
2680
2690
|
if isinstance(browser_e2e, dict):
|
|
2681
2691
|
projection["browserE2E"] = _typed_test_metrics(browser_e2e, total_key="total")
|
|
2682
2692
|
performance = validations.get("performance")
|
|
@@ -3023,8 +3033,10 @@ def _compute_final_status(
|
|
|
3023
3033
|
verification: dict[str, Any],
|
|
3024
3034
|
) -> tuple[str, list[str]]:
|
|
3025
3035
|
api = verification.get("apiTests") or {}
|
|
3036
|
+
browser = verification.get("browserE2E") or {}
|
|
3026
3037
|
db = str(verification.get("dbCompatibility") or "")
|
|
3027
3038
|
api_status = str(api.get("status") or "")
|
|
3039
|
+
browser_status = str(browser.get("status") or "")
|
|
3028
3040
|
reasons: list[str] = []
|
|
3029
3041
|
for phase, v in stage_status.items():
|
|
3030
3042
|
if v == "FAIL":
|
|
@@ -3036,12 +3048,18 @@ def _compute_final_status(
|
|
|
3036
3048
|
return "FAIL", [f"unitTests.errors={unit.get('errors')}"]
|
|
3037
3049
|
if int(api.get("failed") or 0) > 0:
|
|
3038
3050
|
return "FAIL", [f"apiTests.failed={api.get('failed')}"]
|
|
3051
|
+
if int(browser.get("failed") or 0) > 0:
|
|
3052
|
+
return "FAIL", [f"browserE2E.failed={browser.get('failed')}"]
|
|
3053
|
+
if browser_status == "FAIL":
|
|
3054
|
+
return "FAIL", ["browserE2E.status=FAIL"]
|
|
3039
3055
|
conditional = {
|
|
3040
3056
|
"USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
|
|
3041
3057
|
"NOT_RUN", "PARTIAL",
|
|
3042
3058
|
}
|
|
3043
3059
|
if api_status in conditional:
|
|
3044
3060
|
reasons.append(f"apiTests.status={api_status}")
|
|
3061
|
+
if browser_status in conditional:
|
|
3062
|
+
reasons.append(f"browserE2E.status={browser_status}")
|
|
3045
3063
|
if db in conditional:
|
|
3046
3064
|
reasons.append(f"dbCompatibility={db}")
|
|
3047
3065
|
if reasons:
|
|
@@ -3258,6 +3276,7 @@ def _review_summary(
|
|
|
3258
3276
|
"redConfirmed": 0,
|
|
3259
3277
|
"yellowFixed": 0,
|
|
3260
3278
|
"yellowDeferred": 0,
|
|
3279
|
+
"currentRiskCount": 0,
|
|
3261
3280
|
"summary": "",
|
|
3262
3281
|
}
|
|
3263
3282
|
sidecar = hr.findings_path(change_dir)
|
|
@@ -3287,6 +3306,8 @@ def _review_summary(
|
|
|
3287
3306
|
for item in yellow_items
|
|
3288
3307
|
if item.get("disposition") in {"DEFERRED", "ACCEPTED_RISK"}
|
|
3289
3308
|
),
|
|
3309
|
+
"currentRiskCount": int(status.get("currentRiskCount") or 0),
|
|
3310
|
+
"currentRisks": list(status.get("currentRisks") or []),
|
|
3290
3311
|
"summary": f"structured review run {status.get('runId') or 'unknown'}",
|
|
3291
3312
|
}
|
|
3292
3313
|
)
|
|
@@ -3759,6 +3780,7 @@ def collect_summary_data(
|
|
|
3759
3780
|
data["archiveCommit"] = identity.get("archiveCommit") or data.get("finalCommit")
|
|
3760
3781
|
data["environmentHash"] = identity.get("environmentHash") or NOT_AVAILABLE
|
|
3761
3782
|
data["changeIdentity"] = identity
|
|
3783
|
+
data = hrm.apply_identity_mirrors(data)
|
|
3762
3784
|
|
|
3763
3785
|
# diffStat / changedFiles
|
|
3764
3786
|
if not for_replay or not data.get("changedFiles"):
|
|
@@ -3823,8 +3845,11 @@ def collect_summary_data(
|
|
|
3823
3845
|
data["gitFacts"] = {
|
|
3824
3846
|
"baseCommit": data.get("baseCommit") or "",
|
|
3825
3847
|
"finalCommit": data.get("finalCommit") or "",
|
|
3848
|
+
"productCommit": data.get("productCommit") or "",
|
|
3826
3849
|
"featureMergeHash": data.get("featureMergeHash") or "",
|
|
3827
3850
|
"releaseTipHash": data.get("releaseTipHash") or "",
|
|
3851
|
+
"productTreeHash": data.get("productTreeHash") or "",
|
|
3852
|
+
"environmentHash": data.get("environmentHash") or "",
|
|
3828
3853
|
"filesChanged": int(diff_for_facts.get("filesChanged") or 0),
|
|
3829
3854
|
"insertions": int(diff_for_facts.get("insertions") or 0),
|
|
3830
3855
|
"deletions": int(diff_for_facts.get("deletions") or 0),
|
|
@@ -3854,10 +3879,12 @@ def collect_summary_data(
|
|
|
3854
3879
|
data["artifactStorage"] = (
|
|
3855
3880
|
storage if isinstance(storage, dict) else {}
|
|
3856
3881
|
)
|
|
3882
|
+
data["artifactStorage"]["available"] = True
|
|
3857
3883
|
sources.append("evidence/artifact-storage.json")
|
|
3858
3884
|
else:
|
|
3859
3885
|
data["artifactStorage"] = {
|
|
3860
3886
|
"schemaVersion": 1,
|
|
3887
|
+
"available": False,
|
|
3861
3888
|
"artifactCount": 0,
|
|
3862
3889
|
"bytesAdded": 0,
|
|
3863
3890
|
"bytesReused": 0,
|
|
@@ -3885,6 +3912,8 @@ def collect_summary_data(
|
|
|
3885
3912
|
existing if for_replay else None,
|
|
3886
3913
|
projected_events,
|
|
3887
3914
|
)
|
|
3915
|
+
review_status = hr.status(change_dir)
|
|
3916
|
+
data["reviewFindings"] = list(review_status.get("items") or [])
|
|
3888
3917
|
if not for_replay:
|
|
3889
3918
|
data["timeline"] = _timeline_from_events(event_summary, projected_events)
|
|
3890
3919
|
else:
|
|
@@ -3934,6 +3963,8 @@ def collect_summary_data(
|
|
|
3934
3963
|
data.setdefault("finalStatusReasons", [])
|
|
3935
3964
|
data.setdefault("riskTier", "unknown")
|
|
3936
3965
|
|
|
3966
|
+
data["normalizedReport"] = hrm.normalize_report(data)
|
|
3967
|
+
|
|
3937
3968
|
# archiveManifest
|
|
3938
3969
|
am = {
|
|
3939
3970
|
"movedFiles": 0,
|
|
@@ -4040,7 +4071,7 @@ def resolve_node_path(project_root: Path) -> str | None:
|
|
|
4040
4071
|
return shutil.which("node")
|
|
4041
4072
|
|
|
4042
4073
|
|
|
4043
|
-
def
|
|
4074
|
+
def _render_legacy_fallback_html(summary: dict[str, Any]) -> str:
|
|
4044
4075
|
"""Render escaped, deterministic HTML with every validate-required fact.
|
|
4045
4076
|
|
|
4046
4077
|
Used when the Node renderer is unavailable or fails. No timestamps / random
|
|
@@ -4217,6 +4248,7 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4217
4248
|
ver = summary.get("verification") or {}
|
|
4218
4249
|
unit = ver.get("unitTests") or {}
|
|
4219
4250
|
api = ver.get("apiTests") or {}
|
|
4251
|
+
browser = ver.get("browserE2E") or {}
|
|
4220
4252
|
parts.append("<h3>Verification</h3>")
|
|
4221
4253
|
parts.append(
|
|
4222
4254
|
"<p>unitTests: run={run} failures={failures} errors={errors} "
|
|
@@ -4239,6 +4271,17 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4239
4271
|
blocked=esc(api.get("blocked")),
|
|
4240
4272
|
)
|
|
4241
4273
|
)
|
|
4274
|
+
parts.append(
|
|
4275
|
+
"<p>browserE2E: status={status} total={total} passed={passed} "
|
|
4276
|
+
"failed={failed} skipped={skipped} retries={retries}</p>".format(
|
|
4277
|
+
status=esc(browser.get("status")),
|
|
4278
|
+
total=esc(browser.get("total")),
|
|
4279
|
+
passed=esc(browser.get("passed")),
|
|
4280
|
+
failed=esc(browser.get("failed")),
|
|
4281
|
+
skipped=esc(browser.get("skipped")),
|
|
4282
|
+
retries=esc(browser.get("retries")),
|
|
4283
|
+
)
|
|
4284
|
+
)
|
|
4242
4285
|
parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
|
|
4243
4286
|
|
|
4244
4287
|
parts.append("<h3>Changed Files</h3><ul>")
|
|
@@ -4275,6 +4318,235 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4275
4318
|
return "\n".join(parts) + "\n"
|
|
4276
4319
|
|
|
4277
4320
|
|
|
4321
|
+
def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
4322
|
+
"""Render the same executive information architecture without Node."""
|
|
4323
|
+
|
|
4324
|
+
def esc(value: Any) -> str:
|
|
4325
|
+
return _html_escape("" if value is None else str(value))
|
|
4326
|
+
|
|
4327
|
+
def rec(value: Any) -> dict[str, Any]:
|
|
4328
|
+
return value if isinstance(value, dict) else {}
|
|
4329
|
+
|
|
4330
|
+
def seq(value: Any) -> list[Any]:
|
|
4331
|
+
return value if isinstance(value, list) else []
|
|
4332
|
+
|
|
4333
|
+
normalized = rec(summary.get("normalizedReport"))
|
|
4334
|
+
outcomes = rec(normalized.get("outcomes"))
|
|
4335
|
+
current = rec(outcomes.get("current")) or {
|
|
4336
|
+
"status": summary.get("finalStatus"),
|
|
4337
|
+
"reasons": seq(summary.get("finalStatusReasons")),
|
|
4338
|
+
"stages": rec(summary.get("stageStatus")),
|
|
4339
|
+
"knownRisks": seq(summary.get("knownRisks")),
|
|
4340
|
+
}
|
|
4341
|
+
release = rec(outcomes.get("release")) or {
|
|
4342
|
+
"intent": summary.get("archiveIntent"),
|
|
4343
|
+
"decision": "NOT_REQUESTED"
|
|
4344
|
+
if summary.get("archiveIntent") == "record-only"
|
|
4345
|
+
else rec(summary.get("releaseDecision")).get("code"),
|
|
4346
|
+
"eligible": rec(summary.get("releaseDecision")).get("releaseEligible"),
|
|
4347
|
+
}
|
|
4348
|
+
identity = rec(normalized.get("identity")) or rec(summary.get("changeIdentity"))
|
|
4349
|
+
verification = rec(normalized.get("verification")) or rec(
|
|
4350
|
+
summary.get("verification")
|
|
4351
|
+
)
|
|
4352
|
+
timing = rec(normalized.get("timing")) or rec(summary.get("timing"))
|
|
4353
|
+
measurements = rec(normalized.get("measurements"))
|
|
4354
|
+
record_only = (
|
|
4355
|
+
release.get("intent") == "record-only"
|
|
4356
|
+
or summary.get("archiveIntent") == "record-only"
|
|
4357
|
+
)
|
|
4358
|
+
product_commit = (
|
|
4359
|
+
identity.get("productCommit")
|
|
4360
|
+
or summary.get("productCommit")
|
|
4361
|
+
or summary.get("finalCommit")
|
|
4362
|
+
or "N/A"
|
|
4363
|
+
)
|
|
4364
|
+
labels = [
|
|
4365
|
+
("后端", ("unitTests", "dbCompatibility")),
|
|
4366
|
+
("Geo", ("geo",)),
|
|
4367
|
+
("前端", ("frontend",)),
|
|
4368
|
+
("浏览器", ("browserE2E",)),
|
|
4369
|
+
("API", ("apiTests",)),
|
|
4370
|
+
]
|
|
4371
|
+
status_labels = {
|
|
4372
|
+
"OK": "通过",
|
|
4373
|
+
"PASS": "通过",
|
|
4374
|
+
"PASSED": "通过",
|
|
4375
|
+
"CONDITIONAL_OK": "有条件通过",
|
|
4376
|
+
"WARN": "警告",
|
|
4377
|
+
"ADVISORY": "建议",
|
|
4378
|
+
"FAIL": "失败",
|
|
4379
|
+
"FAILED": "失败",
|
|
4380
|
+
"ERROR": "错误",
|
|
4381
|
+
"BLOCKED": "阻塞",
|
|
4382
|
+
"NOT_RUN": "未运行",
|
|
4383
|
+
"SKIPPED": "已跳过",
|
|
4384
|
+
"NOT_APPLICABLE": "不适用",
|
|
4385
|
+
"UNKNOWN": "未知",
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
def status_label(value: Any) -> str:
|
|
4389
|
+
status = str(value or "UNKNOWN").upper()
|
|
4390
|
+
return status_labels.get(status, status)
|
|
4391
|
+
|
|
4392
|
+
def verification_status(value: Any) -> str:
|
|
4393
|
+
if isinstance(value, str):
|
|
4394
|
+
return value
|
|
4395
|
+
item = rec(value)
|
|
4396
|
+
if item.get("status"):
|
|
4397
|
+
return str(item["status"])
|
|
4398
|
+
if int(item.get("failures") or 0) + int(item.get("errors") or 0):
|
|
4399
|
+
return "FAIL"
|
|
4400
|
+
if int(item.get("run") or item.get("total") or 0):
|
|
4401
|
+
return "OK"
|
|
4402
|
+
return "NOT_RUN"
|
|
4403
|
+
|
|
4404
|
+
def describe(value: Any) -> str:
|
|
4405
|
+
if isinstance(value, str):
|
|
4406
|
+
return value
|
|
4407
|
+
item = rec(value)
|
|
4408
|
+
return str(
|
|
4409
|
+
item.get("title")
|
|
4410
|
+
or item.get("message")
|
|
4411
|
+
or item.get("summary")
|
|
4412
|
+
or item.get("action")
|
|
4413
|
+
or item.get("remediation")
|
|
4414
|
+
or item
|
|
4415
|
+
)
|
|
4416
|
+
|
|
4417
|
+
def measurement_label(value: Any) -> str:
|
|
4418
|
+
item = rec(value)
|
|
4419
|
+
if item.get("state") in {"unknown", "not_applicable"}:
|
|
4420
|
+
return "N/A"
|
|
4421
|
+
if item.get("state") == "zero":
|
|
4422
|
+
return "0"
|
|
4423
|
+
if item.get("state") == "known":
|
|
4424
|
+
return str(item.get("value"))
|
|
4425
|
+
return "N/A" if value is None or value == "" else str(value)
|
|
4426
|
+
|
|
4427
|
+
risks = (
|
|
4428
|
+
seq(current.get("findings"))
|
|
4429
|
+
or seq(current.get("knownRisks"))
|
|
4430
|
+
or seq(summary.get("knownRisks"))
|
|
4431
|
+
)
|
|
4432
|
+
actions = seq(summary.get("manualActions"))
|
|
4433
|
+
groups: list[str] = []
|
|
4434
|
+
passed = 0
|
|
4435
|
+
for label, keys in labels:
|
|
4436
|
+
statuses = [
|
|
4437
|
+
verification_status(verification[key])
|
|
4438
|
+
for key in keys
|
|
4439
|
+
if key in verification
|
|
4440
|
+
]
|
|
4441
|
+
status = (
|
|
4442
|
+
"FAIL"
|
|
4443
|
+
if any(value in {"FAIL", "ERROR", "BLOCKED"} for value in statuses)
|
|
4444
|
+
else "WARN"
|
|
4445
|
+
if any("WARN" in value or "CONDITIONAL" in value for value in statuses)
|
|
4446
|
+
else "NOT_APPLICABLE"
|
|
4447
|
+
if statuses and all(value == "NOT_APPLICABLE" for value in statuses)
|
|
4448
|
+
else "OK"
|
|
4449
|
+
if statuses
|
|
4450
|
+
and all(
|
|
4451
|
+
value in {"OK", "PASS", "PASSED", "NOT_APPLICABLE"}
|
|
4452
|
+
for value in statuses
|
|
4453
|
+
)
|
|
4454
|
+
else "NOT_RUN"
|
|
4455
|
+
)
|
|
4456
|
+
if status == "OK":
|
|
4457
|
+
passed += 1
|
|
4458
|
+
details = " · ".join(
|
|
4459
|
+
str(rec(verification.get(key)).get("passRate") or status)
|
|
4460
|
+
for key in keys
|
|
4461
|
+
if key in verification
|
|
4462
|
+
) or "未配置该组验证"
|
|
4463
|
+
raw_details = " · ".join(
|
|
4464
|
+
f"{key} · status={verification_status(verification[key])}"
|
|
4465
|
+
+ (
|
|
4466
|
+
f" · failed={rec(verification[key]).get('failed')}"
|
|
4467
|
+
if rec(verification[key]).get("failed") is not None
|
|
4468
|
+
else ""
|
|
4469
|
+
)
|
|
4470
|
+
for key in keys
|
|
4471
|
+
if key in verification
|
|
4472
|
+
)
|
|
4473
|
+
groups.append(
|
|
4474
|
+
f'<article class="verify" title="{esc(raw_details)}"><strong>{esc(label)}</strong>'
|
|
4475
|
+
f'<span title="{esc(status)}">{esc(status_label(status))}</span><small>{esc(details)}</small></article>'
|
|
4476
|
+
)
|
|
4477
|
+
|
|
4478
|
+
commands = seq(rec(summary.get("reportPipeline")).get("commands"))
|
|
4479
|
+
command_rows = "".join(
|
|
4480
|
+
f"<tr><td>{esc(item.get('phase') or '-')}</td>"
|
|
4481
|
+
f"<td><code>{esc(item.get('command'))}</code></td>"
|
|
4482
|
+
f"<td>{esc('OK' if int(item.get('exit_code', item.get('exitCode', 1))) == 0 else 'FAIL')}</td></tr>"
|
|
4483
|
+
for item in commands
|
|
4484
|
+
if isinstance(item, dict)
|
|
4485
|
+
) or '<tr><td colspan="3">没有命令证据</td></tr>'
|
|
4486
|
+
files = seq(summary.get("changedFiles"))
|
|
4487
|
+
file_rows = "".join(
|
|
4488
|
+
f"<tr><td><code>{esc(item.get('path') or item.get('file'))}</code></td>"
|
|
4489
|
+
f"<td>+{esc(item.get('insertions') or 0)}</td>"
|
|
4490
|
+
f"<td>-{esc(item.get('deletions') or 0)}</td></tr>"
|
|
4491
|
+
for item in files
|
|
4492
|
+
if isinstance(item, dict)
|
|
4493
|
+
) or '<tr><td colspan="3">没有变更文件证据</td></tr>'
|
|
4494
|
+
risk_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in risks) or "<li>当前没有未处置风险</li>"
|
|
4495
|
+
action_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in actions) or "<li>无需人工后续动作</li>"
|
|
4496
|
+
release_card = (
|
|
4497
|
+
""
|
|
4498
|
+
if record_only
|
|
4499
|
+
else "<article class=\"card\"><h2>发布与候选</h2>"
|
|
4500
|
+
f"<p>候选证明 / 发布资格:{esc(release.get('decision') or 'NOT_EVALUATED')}</p></article>"
|
|
4501
|
+
)
|
|
4502
|
+
remote = rec(measurements.get("remoteCost"))
|
|
4503
|
+
storage = rec(measurements.get("artifactStorage"))
|
|
4504
|
+
remote_totals = rec(remote.get("totals"))
|
|
4505
|
+
projection = rec(summary.get("projection"))
|
|
4506
|
+
archive_integrity = rec(summary.get("archiveIntegrity"))
|
|
4507
|
+
total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
|
|
4508
|
+
total_files_text = "" if total_files is None else str(total_files)
|
|
4509
|
+
|
|
4510
|
+
return f"""<!doctype html>
|
|
4511
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4512
|
+
<title>Harness 最终报告 · {esc(summary.get("changeName"))}</title>
|
|
4513
|
+
<style>
|
|
4514
|
+
:root{{--bg:#f4f6f8;--card:#fff;--text:#172033;--muted:#667085;--line:#dfe5ec}}
|
|
4515
|
+
@media(prefers-color-scheme:dark){{:root{{--bg:#0b1119;--card:#121a26;--text:#eef3f9;--muted:#9aa8ba;--line:#2a384a}}}}
|
|
4516
|
+
*{{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}}}}
|
|
4517
|
+
</style></head><body><main>
|
|
4518
|
+
<section class="hero"><small>HARNESS · 管理结论</small><h1>{esc(summary.get("changeName"))}</h1>
|
|
4519
|
+
<p>{esc(summary.get("businessGoal") or "未记录业务目标")}</p>
|
|
4520
|
+
<strong title="{esc(current.get("status"))}">{esc(status_label(current.get("status")))}</strong>
|
|
4521
|
+
<p id="finalStatusReasons">{esc(" · ".join(str(item) for item in seq(current.get("reasons"))))}</p>
|
|
4522
|
+
{"<p>归档意图:仅记录 · 未请求发布</p>" if record_only else ""}</section>
|
|
4523
|
+
<section class="metrics"><article class="metric"><small>产品提交</small><strong><code>{esc(str(product_commit)[:10])}</code></strong></article>
|
|
4524
|
+
<article class="metric"><small>验证概览</small><strong>{passed}/5 组通过</strong></article>
|
|
4525
|
+
<article class="metric"><small>风险与动作</small><strong>{len(risks)} / {len(actions)}</strong></article>
|
|
4526
|
+
<article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
|
|
4527
|
+
<article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
|
|
4528
|
+
<article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
|
|
4529
|
+
<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>
|
|
4530
|
+
{release_card}
|
|
4531
|
+
<details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
|
|
4532
|
+
<p>workflowWallClock={esc(timing.get("workflowWallClockMs") if "workflowWallClockMs" in timing else "N/A")}</p>
|
|
4533
|
+
<p>stageActiveExecution={esc(timing.get("stageActiveExecutionMs") if "stageActiveExecutionMs" in timing else "N/A")}</p>
|
|
4534
|
+
<p>stageWallClockSpan={esc(timing.get("stageWallClockSpanMs") if "stageWallClockSpanMs" in timing else "N/A")}</p>
|
|
4535
|
+
<p>reportCutoffAt={esc(timing.get("reportCutoffAt") or "N/A")}</p>
|
|
4536
|
+
<p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
|
|
4537
|
+
<p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
|
|
4538
|
+
<details><summary>技术证据 · 发布与归档治理</summary><div>
|
|
4539
|
+
<p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
|
|
4540
|
+
<p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
|
|
4541
|
+
<p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
|
|
4542
|
+
<details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
|
|
4543
|
+
<details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
|
|
4544
|
+
<details><summary>技术元数据</summary><div><p>productCommit=<code>{esc(product_commit)}</code></p>
|
|
4545
|
+
<p>schemaVersion={esc(summary.get("schemaVersion"))}</p></div></details>
|
|
4546
|
+
</main></body></html>
|
|
4547
|
+
"""
|
|
4548
|
+
|
|
4549
|
+
|
|
4278
4550
|
def render_final_summary(
|
|
4279
4551
|
change_dir: Path,
|
|
4280
4552
|
summary_path: Path,
|
|
@@ -4725,6 +4997,7 @@ def validate_summary(
|
|
|
4725
4997
|
ver = summary.get("verification") or {}
|
|
4726
4998
|
unit = ver.get("unitTests") or {}
|
|
4727
4999
|
api = ver.get("apiTests") or {}
|
|
5000
|
+
browser = ver.get("browserE2E") or {}
|
|
4728
5001
|
pass_rate = unit.get("passRate")
|
|
4729
5002
|
if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
|
|
4730
5003
|
issues.append(
|
|
@@ -4745,6 +5018,22 @@ def validate_summary(
|
|
|
4745
5018
|
"message": f"apiTests.status '{api_status}' not visible in final-summary",
|
|
4746
5019
|
}
|
|
4747
5020
|
)
|
|
5021
|
+
browser_status = str(browser.get("status") or "")
|
|
5022
|
+
if (
|
|
5023
|
+
browser_status
|
|
5024
|
+
and browser_status not in {"", NOT_AVAILABLE}
|
|
5025
|
+
and browser_status not in html
|
|
5026
|
+
):
|
|
5027
|
+
issues.append(
|
|
5028
|
+
{
|
|
5029
|
+
"code": "missing-verification",
|
|
5030
|
+
"severity": "warning",
|
|
5031
|
+
"message": (
|
|
5032
|
+
f"browserE2E.status '{browser_status}' not visible "
|
|
5033
|
+
"in final-summary"
|
|
5034
|
+
),
|
|
5035
|
+
}
|
|
5036
|
+
)
|
|
4748
5037
|
|
|
4749
5038
|
# artifacts / summary-data path hints
|
|
4750
5039
|
am = summary.get("archiveManifest") or {}
|
|
@@ -4777,11 +5066,23 @@ def validate_summary(
|
|
|
4777
5066
|
|
|
4778
5067
|
# status contradiction
|
|
4779
5068
|
final_status = str(summary.get("finalStatus") or "")
|
|
4780
|
-
|
|
5069
|
+
browser_conditional = {
|
|
5070
|
+
"USER_SKIPPED",
|
|
5071
|
+
"BLOCKED",
|
|
5072
|
+
"BLOCKED_BY_ENV",
|
|
5073
|
+
"NOT_RUN",
|
|
5074
|
+
"PARTIAL",
|
|
5075
|
+
}
|
|
5076
|
+
has_skip = api_status == "USER_SKIPPED" or browser_status in browser_conditional or str(
|
|
4781
5077
|
ver.get("dbCompatibility") or ""
|
|
4782
5078
|
) == "BLOCKED_BY_DBA"
|
|
4783
5079
|
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
4784
5080
|
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5081
|
+
has_fail_ver = (
|
|
5082
|
+
has_fail_ver
|
|
5083
|
+
or int(browser.get("failed") or 0) > 0
|
|
5084
|
+
or browser_status == "FAIL"
|
|
5085
|
+
)
|
|
4785
5086
|
stage = summary.get("stageStatus") or {}
|
|
4786
5087
|
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
4787
5088
|
|
|
@@ -4811,13 +5112,26 @@ def validate_summary(
|
|
|
4811
5112
|
ver = summary.get("verification") or {}
|
|
4812
5113
|
unit = ver.get("unitTests") or {}
|
|
4813
5114
|
api = ver.get("apiTests") or {}
|
|
5115
|
+
browser = ver.get("browserE2E") or {}
|
|
4814
5116
|
api_status = str(api.get("status") or "")
|
|
5117
|
+
browser_status = str(browser.get("status") or "")
|
|
4815
5118
|
final_status = str(summary.get("finalStatus") or "")
|
|
4816
|
-
has_skip = api_status == "USER_SKIPPED" or
|
|
5119
|
+
has_skip = api_status == "USER_SKIPPED" or browser_status in {
|
|
5120
|
+
"USER_SKIPPED",
|
|
5121
|
+
"BLOCKED",
|
|
5122
|
+
"BLOCKED_BY_ENV",
|
|
5123
|
+
"NOT_RUN",
|
|
5124
|
+
"PARTIAL",
|
|
5125
|
+
} or str(
|
|
4817
5126
|
ver.get("dbCompatibility") or ""
|
|
4818
5127
|
) == "BLOCKED_BY_DBA"
|
|
4819
5128
|
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
4820
5129
|
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5130
|
+
has_fail_ver = (
|
|
5131
|
+
has_fail_ver
|
|
5132
|
+
or int(browser.get("failed") or 0) > 0
|
|
5133
|
+
or browser_status == "FAIL"
|
|
5134
|
+
)
|
|
4821
5135
|
stage = summary.get("stageStatus") or {}
|
|
4822
5136
|
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
4823
5137
|
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
@@ -4946,6 +5260,84 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
4946
5260
|
"message": f"stageStatus.{phase}={status} but event reducer says {event_status}",
|
|
4947
5261
|
})
|
|
4948
5262
|
|
|
5263
|
+
# All compatibility mirrors must be derived from the canonical identity.
|
|
5264
|
+
identity_doc = summary.get("changeIdentity")
|
|
5265
|
+
identity_doc = identity_doc if isinstance(identity_doc, dict) else {}
|
|
5266
|
+
canonical = hrm.canonical_identity(summary)
|
|
5267
|
+
mirror_sources = {
|
|
5268
|
+
"productCommit": summary.get("productCommit"),
|
|
5269
|
+
"featureMergeHash": summary.get("featureMergeHash"),
|
|
5270
|
+
"releaseTipHash": summary.get("releaseTipHash"),
|
|
5271
|
+
"productTreeHash": summary.get("productTreeHash"),
|
|
5272
|
+
"environmentHash": summary.get("environmentHash"),
|
|
5273
|
+
}
|
|
5274
|
+
for field, mirror in mirror_sources.items():
|
|
5275
|
+
expected = str(canonical.get(field) or "")
|
|
5276
|
+
actual = str(mirror or "")
|
|
5277
|
+
if field in hrm.CONTENT_HASH_FIELDS:
|
|
5278
|
+
expected = expected.removeprefix("sha256:")
|
|
5279
|
+
actual = actual.removeprefix("sha256:")
|
|
5280
|
+
if identity_doc.get(field) and actual and expected != actual:
|
|
5281
|
+
issues.append(
|
|
5282
|
+
{
|
|
5283
|
+
"code": "IDENTITY_MIRROR_MISMATCH",
|
|
5284
|
+
"severity": "error",
|
|
5285
|
+
"message": f"{field} mirror differs from changeIdentity",
|
|
5286
|
+
"expected": str(canonical.get(field) or ""),
|
|
5287
|
+
"actual": str(mirror or ""),
|
|
5288
|
+
}
|
|
5289
|
+
)
|
|
5290
|
+
|
|
5291
|
+
# Timing categories form a disjoint, wall-clock-conserving partition.
|
|
5292
|
+
timing = summary.get("timing")
|
|
5293
|
+
timing = timing if isinstance(timing, dict) else {}
|
|
5294
|
+
if timing:
|
|
5295
|
+
wall = int(timing.get("workflowWallClockMs") or 0)
|
|
5296
|
+
parts = sum(
|
|
5297
|
+
int(timing.get(key) or 0)
|
|
5298
|
+
for key in (
|
|
5299
|
+
"attributedStageUnionMs",
|
|
5300
|
+
"externalWaitMs",
|
|
5301
|
+
"pausedMs",
|
|
5302
|
+
"agentOrToolUnattributedMs",
|
|
5303
|
+
)
|
|
5304
|
+
)
|
|
5305
|
+
declared_delta = int(timing.get("conservationDeltaMs") or 0)
|
|
5306
|
+
if parts != wall or declared_delta != wall - parts:
|
|
5307
|
+
issues.append(
|
|
5308
|
+
{
|
|
5309
|
+
"code": "TIMING_CONSERVATION_MISMATCH",
|
|
5310
|
+
"severity": "error",
|
|
5311
|
+
"message": (
|
|
5312
|
+
f"timing categories total {parts}ms, wall clock is "
|
|
5313
|
+
f"{wall}ms, declared delta is {declared_delta}ms"
|
|
5314
|
+
),
|
|
5315
|
+
}
|
|
5316
|
+
)
|
|
5317
|
+
|
|
5318
|
+
candidate_gate_for_release = summary.get("candidateVerification")
|
|
5319
|
+
candidate_gate_for_release = (
|
|
5320
|
+
candidate_gate_for_release
|
|
5321
|
+
if isinstance(candidate_gate_for_release, dict)
|
|
5322
|
+
else {}
|
|
5323
|
+
)
|
|
5324
|
+
candidate_status = str(
|
|
5325
|
+
candidate_gate_for_release.get("status")
|
|
5326
|
+
or candidate_gate_for_release.get("code")
|
|
5327
|
+
or ""
|
|
5328
|
+
).upper()
|
|
5329
|
+
if bool(summary.get("releaseEligible")) and (
|
|
5330
|
+
candidate_gate_for_release.get("ok") is False
|
|
5331
|
+
or candidate_status in {"FAIL", "FAILED", "ERROR", "BLOCKED"}
|
|
5332
|
+
):
|
|
5333
|
+
issues.append(
|
|
5334
|
+
{
|
|
5335
|
+
"code": "RELEASE_ELIGIBILITY_CONTRADICTION",
|
|
5336
|
+
"severity": "error",
|
|
5337
|
+
"message": "releaseEligible=true while candidate verification is not successful",
|
|
5338
|
+
}
|
|
5339
|
+
)
|
|
5340
|
+
|
|
4949
5341
|
# Candidate receipt and summary must describe the same immutable subject.
|
|
4950
5342
|
candidate_gate = summary.get("candidateVerification")
|
|
4951
5343
|
candidate_gate = candidate_gate if isinstance(candidate_gate, dict) else {}
|