@hunter-harness/workflow-harness 0.2.33 → 0.2.35
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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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 +415 -15
- 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
|
|
|
@@ -620,6 +621,7 @@ def evaluate_product_ci_gate(change_dir: Path) -> dict[str, Any]:
|
|
|
620
621
|
|
|
621
622
|
if assurance == "local-reproducible" and provider == "local-harness":
|
|
622
623
|
local_required = {
|
|
624
|
+
"subject.environmentHash": environment_hash,
|
|
623
625
|
"verification.commandSetHash": command_set_hash,
|
|
624
626
|
"verification.ledgerHash": ledger_hash,
|
|
625
627
|
"verification.toolchainHashes": verification.get("toolchainHashes"),
|
|
@@ -1046,6 +1048,14 @@ def certify_local_candidate(
|
|
|
1046
1048
|
f"ledger={recorded_tree_hash} current={current_tree_hash}"
|
|
1047
1049
|
)
|
|
1048
1050
|
product_tree_hash = current_tree_hash
|
|
1051
|
+
environment_hashes = sorted(
|
|
1052
|
+
{str(item["environmentHash"]) for item in selected}
|
|
1053
|
+
)
|
|
1054
|
+
subject_environment_hash = (
|
|
1055
|
+
environment_hashes[0]
|
|
1056
|
+
if len(environment_hashes) == 1
|
|
1057
|
+
else _candidate_value_hash(environment_hashes)
|
|
1058
|
+
)
|
|
1049
1059
|
|
|
1050
1060
|
receipt = {
|
|
1051
1061
|
"schemaVersion": 2,
|
|
@@ -1056,6 +1066,7 @@ def certify_local_candidate(
|
|
|
1056
1066
|
"subject": {
|
|
1057
1067
|
"productCommit": product_commit,
|
|
1058
1068
|
"productTreeHash": product_tree_hash,
|
|
1069
|
+
"environmentHash": subject_environment_hash,
|
|
1059
1070
|
},
|
|
1060
1071
|
"verification": {
|
|
1061
1072
|
"requiredValidations": required,
|
|
@@ -1067,9 +1078,7 @@ def certify_local_candidate(
|
|
|
1067
1078
|
"toolchainHashes": sorted(
|
|
1068
1079
|
{str(item["toolchainHash"]) for item in selected}
|
|
1069
1080
|
),
|
|
1070
|
-
"environmentHashes":
|
|
1071
|
-
{str(item["environmentHash"]) for item in selected}
|
|
1072
|
-
),
|
|
1081
|
+
"environmentHashes": environment_hashes,
|
|
1073
1082
|
"dependencyHashes": sorted(
|
|
1074
1083
|
{str(item["inputsHash"]) for item in selected}
|
|
1075
1084
|
),
|
|
@@ -1643,6 +1652,12 @@ def build_workflow_timing(
|
|
|
1643
1652
|
if started_dt is not None and cutoff_dt is not None
|
|
1644
1653
|
else 0
|
|
1645
1654
|
)
|
|
1655
|
+
# Segment-level integer conversion can lose sub-millisecond fractions at
|
|
1656
|
+
# each boundary. Attribute only that positive rounding residue to the
|
|
1657
|
+
# otherwise-unattributed bucket so the partition remains exact.
|
|
1658
|
+
rounding_residue = workflow_wall - sum(categories.values())
|
|
1659
|
+
if rounding_residue > 0:
|
|
1660
|
+
categories["unattributed"] += rounding_residue
|
|
1646
1661
|
stage_wall = union_ms(active_intervals)
|
|
1647
1662
|
sessions: list[dict[str, Any]] = []
|
|
1648
1663
|
session_start = started_dt
|
|
@@ -2645,10 +2660,9 @@ def _typed_test_metrics(entry: dict[str, Any], *, total_key: str) -> dict[str, A
|
|
|
2645
2660
|
"executionRate": f"{executed / total:.0%}" if total > 0 else NOT_AVAILABLE,
|
|
2646
2661
|
"source": "committed" if total > 0 else "not-run",
|
|
2647
2662
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
out["skipped"] = int(metrics.get("skipped", 0) or 0)
|
|
2663
|
+
for optional_count in ("blocked", "skipped", "retries"):
|
|
2664
|
+
if optional_count in metrics:
|
|
2665
|
+
out[optional_count] = int(metrics.get(optional_count, 0) or 0)
|
|
2652
2666
|
applicability = entry.get("applicability")
|
|
2653
2667
|
if isinstance(applicability, dict):
|
|
2654
2668
|
out["applicability"] = applicability
|
|
@@ -2665,18 +2679,22 @@ def build_verification_projection(
|
|
|
2665
2679
|
apiContract and browserE2E are distinct typed projections; the legacy
|
|
2666
2680
|
apiTests mapping is retained for schema compatibility.
|
|
2667
2681
|
"""
|
|
2668
|
-
|
|
2682
|
+
effective_ledger = _deepcopy_json(ledger or {})
|
|
2683
|
+
projected_validations = hrm.latest_terminal_validations(effective_ledger)
|
|
2684
|
+
if projected_validations:
|
|
2685
|
+
effective_ledger["validations"] = projected_validations
|
|
2686
|
+
validations = effective_ledger.get("validations") or {}
|
|
2669
2687
|
projection: dict[str, Any] = {
|
|
2670
|
-
"unitTests": _ledger_unit_tests(
|
|
2671
|
-
"apiTests": _ledger_api_tests(
|
|
2672
|
-
"dbCompatibility": _ledger_db_compat(
|
|
2688
|
+
"unitTests": _ledger_unit_tests(effective_ledger),
|
|
2689
|
+
"apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
|
|
2690
|
+
"dbCompatibility": _ledger_db_compat(effective_ledger),
|
|
2673
2691
|
}
|
|
2674
2692
|
api_contract = validations.get("apiContract")
|
|
2675
2693
|
if isinstance(api_contract, dict):
|
|
2676
2694
|
projection["apiContract"] = _typed_test_metrics(
|
|
2677
2695
|
api_contract, total_key="scenariosTotal"
|
|
2678
2696
|
)
|
|
2679
|
-
browser_e2e = validations.get("browserE2E")
|
|
2697
|
+
browser_e2e = validations.get("browserTest") or validations.get("browserE2E")
|
|
2680
2698
|
if isinstance(browser_e2e, dict):
|
|
2681
2699
|
projection["browserE2E"] = _typed_test_metrics(browser_e2e, total_key="total")
|
|
2682
2700
|
performance = validations.get("performance")
|
|
@@ -3023,8 +3041,10 @@ def _compute_final_status(
|
|
|
3023
3041
|
verification: dict[str, Any],
|
|
3024
3042
|
) -> tuple[str, list[str]]:
|
|
3025
3043
|
api = verification.get("apiTests") or {}
|
|
3044
|
+
browser = verification.get("browserE2E") or {}
|
|
3026
3045
|
db = str(verification.get("dbCompatibility") or "")
|
|
3027
3046
|
api_status = str(api.get("status") or "")
|
|
3047
|
+
browser_status = str(browser.get("status") or "")
|
|
3028
3048
|
reasons: list[str] = []
|
|
3029
3049
|
for phase, v in stage_status.items():
|
|
3030
3050
|
if v == "FAIL":
|
|
@@ -3036,12 +3056,18 @@ def _compute_final_status(
|
|
|
3036
3056
|
return "FAIL", [f"unitTests.errors={unit.get('errors')}"]
|
|
3037
3057
|
if int(api.get("failed") or 0) > 0:
|
|
3038
3058
|
return "FAIL", [f"apiTests.failed={api.get('failed')}"]
|
|
3059
|
+
if int(browser.get("failed") or 0) > 0:
|
|
3060
|
+
return "FAIL", [f"browserE2E.failed={browser.get('failed')}"]
|
|
3061
|
+
if browser_status == "FAIL":
|
|
3062
|
+
return "FAIL", ["browserE2E.status=FAIL"]
|
|
3039
3063
|
conditional = {
|
|
3040
3064
|
"USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
|
|
3041
3065
|
"NOT_RUN", "PARTIAL",
|
|
3042
3066
|
}
|
|
3043
3067
|
if api_status in conditional:
|
|
3044
3068
|
reasons.append(f"apiTests.status={api_status}")
|
|
3069
|
+
if browser_status in conditional:
|
|
3070
|
+
reasons.append(f"browserE2E.status={browser_status}")
|
|
3045
3071
|
if db in conditional:
|
|
3046
3072
|
reasons.append(f"dbCompatibility={db}")
|
|
3047
3073
|
if reasons:
|
|
@@ -3258,6 +3284,7 @@ def _review_summary(
|
|
|
3258
3284
|
"redConfirmed": 0,
|
|
3259
3285
|
"yellowFixed": 0,
|
|
3260
3286
|
"yellowDeferred": 0,
|
|
3287
|
+
"currentRiskCount": 0,
|
|
3261
3288
|
"summary": "",
|
|
3262
3289
|
}
|
|
3263
3290
|
sidecar = hr.findings_path(change_dir)
|
|
@@ -3287,6 +3314,8 @@ def _review_summary(
|
|
|
3287
3314
|
for item in yellow_items
|
|
3288
3315
|
if item.get("disposition") in {"DEFERRED", "ACCEPTED_RISK"}
|
|
3289
3316
|
),
|
|
3317
|
+
"currentRiskCount": int(status.get("currentRiskCount") or 0),
|
|
3318
|
+
"currentRisks": list(status.get("currentRisks") or []),
|
|
3290
3319
|
"summary": f"structured review run {status.get('runId') or 'unknown'}",
|
|
3291
3320
|
}
|
|
3292
3321
|
)
|
|
@@ -3759,6 +3788,7 @@ def collect_summary_data(
|
|
|
3759
3788
|
data["archiveCommit"] = identity.get("archiveCommit") or data.get("finalCommit")
|
|
3760
3789
|
data["environmentHash"] = identity.get("environmentHash") or NOT_AVAILABLE
|
|
3761
3790
|
data["changeIdentity"] = identity
|
|
3791
|
+
data = hrm.apply_identity_mirrors(data)
|
|
3762
3792
|
|
|
3763
3793
|
# diffStat / changedFiles
|
|
3764
3794
|
if not for_replay or not data.get("changedFiles"):
|
|
@@ -3823,8 +3853,11 @@ def collect_summary_data(
|
|
|
3823
3853
|
data["gitFacts"] = {
|
|
3824
3854
|
"baseCommit": data.get("baseCommit") or "",
|
|
3825
3855
|
"finalCommit": data.get("finalCommit") or "",
|
|
3856
|
+
"productCommit": data.get("productCommit") or "",
|
|
3826
3857
|
"featureMergeHash": data.get("featureMergeHash") or "",
|
|
3827
3858
|
"releaseTipHash": data.get("releaseTipHash") or "",
|
|
3859
|
+
"productTreeHash": data.get("productTreeHash") or "",
|
|
3860
|
+
"environmentHash": data.get("environmentHash") or "",
|
|
3828
3861
|
"filesChanged": int(diff_for_facts.get("filesChanged") or 0),
|
|
3829
3862
|
"insertions": int(diff_for_facts.get("insertions") or 0),
|
|
3830
3863
|
"deletions": int(diff_for_facts.get("deletions") or 0),
|
|
@@ -3854,10 +3887,12 @@ def collect_summary_data(
|
|
|
3854
3887
|
data["artifactStorage"] = (
|
|
3855
3888
|
storage if isinstance(storage, dict) else {}
|
|
3856
3889
|
)
|
|
3890
|
+
data["artifactStorage"]["available"] = True
|
|
3857
3891
|
sources.append("evidence/artifact-storage.json")
|
|
3858
3892
|
else:
|
|
3859
3893
|
data["artifactStorage"] = {
|
|
3860
3894
|
"schemaVersion": 1,
|
|
3895
|
+
"available": False,
|
|
3861
3896
|
"artifactCount": 0,
|
|
3862
3897
|
"bytesAdded": 0,
|
|
3863
3898
|
"bytesReused": 0,
|
|
@@ -3885,6 +3920,8 @@ def collect_summary_data(
|
|
|
3885
3920
|
existing if for_replay else None,
|
|
3886
3921
|
projected_events,
|
|
3887
3922
|
)
|
|
3923
|
+
review_status = hr.status(change_dir)
|
|
3924
|
+
data["reviewFindings"] = list(review_status.get("items") or [])
|
|
3888
3925
|
if not for_replay:
|
|
3889
3926
|
data["timeline"] = _timeline_from_events(event_summary, projected_events)
|
|
3890
3927
|
else:
|
|
@@ -3934,6 +3971,8 @@ def collect_summary_data(
|
|
|
3934
3971
|
data.setdefault("finalStatusReasons", [])
|
|
3935
3972
|
data.setdefault("riskTier", "unknown")
|
|
3936
3973
|
|
|
3974
|
+
data["normalizedReport"] = hrm.normalize_report(data)
|
|
3975
|
+
|
|
3937
3976
|
# archiveManifest
|
|
3938
3977
|
am = {
|
|
3939
3978
|
"movedFiles": 0,
|
|
@@ -4040,7 +4079,7 @@ def resolve_node_path(project_root: Path) -> str | None:
|
|
|
4040
4079
|
return shutil.which("node")
|
|
4041
4080
|
|
|
4042
4081
|
|
|
4043
|
-
def
|
|
4082
|
+
def _render_legacy_fallback_html(summary: dict[str, Any]) -> str:
|
|
4044
4083
|
"""Render escaped, deterministic HTML with every validate-required fact.
|
|
4045
4084
|
|
|
4046
4085
|
Used when the Node renderer is unavailable or fails. No timestamps / random
|
|
@@ -4217,6 +4256,7 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4217
4256
|
ver = summary.get("verification") or {}
|
|
4218
4257
|
unit = ver.get("unitTests") or {}
|
|
4219
4258
|
api = ver.get("apiTests") or {}
|
|
4259
|
+
browser = ver.get("browserE2E") or {}
|
|
4220
4260
|
parts.append("<h3>Verification</h3>")
|
|
4221
4261
|
parts.append(
|
|
4222
4262
|
"<p>unitTests: run={run} failures={failures} errors={errors} "
|
|
@@ -4239,6 +4279,17 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4239
4279
|
blocked=esc(api.get("blocked")),
|
|
4240
4280
|
)
|
|
4241
4281
|
)
|
|
4282
|
+
parts.append(
|
|
4283
|
+
"<p>browserE2E: status={status} total={total} passed={passed} "
|
|
4284
|
+
"failed={failed} skipped={skipped} retries={retries}</p>".format(
|
|
4285
|
+
status=esc(browser.get("status")),
|
|
4286
|
+
total=esc(browser.get("total")),
|
|
4287
|
+
passed=esc(browser.get("passed")),
|
|
4288
|
+
failed=esc(browser.get("failed")),
|
|
4289
|
+
skipped=esc(browser.get("skipped")),
|
|
4290
|
+
retries=esc(browser.get("retries")),
|
|
4291
|
+
)
|
|
4292
|
+
)
|
|
4242
4293
|
parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
|
|
4243
4294
|
|
|
4244
4295
|
parts.append("<h3>Changed Files</h3><ul>")
|
|
@@ -4275,6 +4326,235 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
|
4275
4326
|
return "\n".join(parts) + "\n"
|
|
4276
4327
|
|
|
4277
4328
|
|
|
4329
|
+
def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
4330
|
+
"""Render the same executive information architecture without Node."""
|
|
4331
|
+
|
|
4332
|
+
def esc(value: Any) -> str:
|
|
4333
|
+
return _html_escape("" if value is None else str(value))
|
|
4334
|
+
|
|
4335
|
+
def rec(value: Any) -> dict[str, Any]:
|
|
4336
|
+
return value if isinstance(value, dict) else {}
|
|
4337
|
+
|
|
4338
|
+
def seq(value: Any) -> list[Any]:
|
|
4339
|
+
return value if isinstance(value, list) else []
|
|
4340
|
+
|
|
4341
|
+
normalized = rec(summary.get("normalizedReport"))
|
|
4342
|
+
outcomes = rec(normalized.get("outcomes"))
|
|
4343
|
+
current = rec(outcomes.get("current")) or {
|
|
4344
|
+
"status": summary.get("finalStatus"),
|
|
4345
|
+
"reasons": seq(summary.get("finalStatusReasons")),
|
|
4346
|
+
"stages": rec(summary.get("stageStatus")),
|
|
4347
|
+
"knownRisks": seq(summary.get("knownRisks")),
|
|
4348
|
+
}
|
|
4349
|
+
release = rec(outcomes.get("release")) or {
|
|
4350
|
+
"intent": summary.get("archiveIntent"),
|
|
4351
|
+
"decision": "NOT_REQUESTED"
|
|
4352
|
+
if summary.get("archiveIntent") == "record-only"
|
|
4353
|
+
else rec(summary.get("releaseDecision")).get("code"),
|
|
4354
|
+
"eligible": rec(summary.get("releaseDecision")).get("releaseEligible"),
|
|
4355
|
+
}
|
|
4356
|
+
identity = rec(normalized.get("identity")) or rec(summary.get("changeIdentity"))
|
|
4357
|
+
verification = rec(normalized.get("verification")) or rec(
|
|
4358
|
+
summary.get("verification")
|
|
4359
|
+
)
|
|
4360
|
+
timing = rec(normalized.get("timing")) or rec(summary.get("timing"))
|
|
4361
|
+
measurements = rec(normalized.get("measurements"))
|
|
4362
|
+
record_only = (
|
|
4363
|
+
release.get("intent") == "record-only"
|
|
4364
|
+
or summary.get("archiveIntent") == "record-only"
|
|
4365
|
+
)
|
|
4366
|
+
product_commit = (
|
|
4367
|
+
identity.get("productCommit")
|
|
4368
|
+
or summary.get("productCommit")
|
|
4369
|
+
or summary.get("finalCommit")
|
|
4370
|
+
or "N/A"
|
|
4371
|
+
)
|
|
4372
|
+
labels = [
|
|
4373
|
+
("后端", ("unitTests", "dbCompatibility")),
|
|
4374
|
+
("Geo", ("geo",)),
|
|
4375
|
+
("前端", ("frontend",)),
|
|
4376
|
+
("浏览器", ("browserE2E",)),
|
|
4377
|
+
("API", ("apiTests",)),
|
|
4378
|
+
]
|
|
4379
|
+
status_labels = {
|
|
4380
|
+
"OK": "通过",
|
|
4381
|
+
"PASS": "通过",
|
|
4382
|
+
"PASSED": "通过",
|
|
4383
|
+
"CONDITIONAL_OK": "有条件通过",
|
|
4384
|
+
"WARN": "警告",
|
|
4385
|
+
"ADVISORY": "建议",
|
|
4386
|
+
"FAIL": "失败",
|
|
4387
|
+
"FAILED": "失败",
|
|
4388
|
+
"ERROR": "错误",
|
|
4389
|
+
"BLOCKED": "阻塞",
|
|
4390
|
+
"NOT_RUN": "未运行",
|
|
4391
|
+
"SKIPPED": "已跳过",
|
|
4392
|
+
"NOT_APPLICABLE": "不适用",
|
|
4393
|
+
"UNKNOWN": "未知",
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
def status_label(value: Any) -> str:
|
|
4397
|
+
status = str(value or "UNKNOWN").upper()
|
|
4398
|
+
return status_labels.get(status, status)
|
|
4399
|
+
|
|
4400
|
+
def verification_status(value: Any) -> str:
|
|
4401
|
+
if isinstance(value, str):
|
|
4402
|
+
return value
|
|
4403
|
+
item = rec(value)
|
|
4404
|
+
if item.get("status"):
|
|
4405
|
+
return str(item["status"])
|
|
4406
|
+
if int(item.get("failures") or 0) + int(item.get("errors") or 0):
|
|
4407
|
+
return "FAIL"
|
|
4408
|
+
if int(item.get("run") or item.get("total") or 0):
|
|
4409
|
+
return "OK"
|
|
4410
|
+
return "NOT_RUN"
|
|
4411
|
+
|
|
4412
|
+
def describe(value: Any) -> str:
|
|
4413
|
+
if isinstance(value, str):
|
|
4414
|
+
return value
|
|
4415
|
+
item = rec(value)
|
|
4416
|
+
return str(
|
|
4417
|
+
item.get("title")
|
|
4418
|
+
or item.get("message")
|
|
4419
|
+
or item.get("summary")
|
|
4420
|
+
or item.get("action")
|
|
4421
|
+
or item.get("remediation")
|
|
4422
|
+
or item
|
|
4423
|
+
)
|
|
4424
|
+
|
|
4425
|
+
def measurement_label(value: Any) -> str:
|
|
4426
|
+
item = rec(value)
|
|
4427
|
+
if item.get("state") in {"unknown", "not_applicable"}:
|
|
4428
|
+
return "N/A"
|
|
4429
|
+
if item.get("state") == "zero":
|
|
4430
|
+
return "0"
|
|
4431
|
+
if item.get("state") == "known":
|
|
4432
|
+
return str(item.get("value"))
|
|
4433
|
+
return "N/A" if value is None or value == "" else str(value)
|
|
4434
|
+
|
|
4435
|
+
risks = (
|
|
4436
|
+
seq(current.get("findings"))
|
|
4437
|
+
or seq(current.get("knownRisks"))
|
|
4438
|
+
or seq(summary.get("knownRisks"))
|
|
4439
|
+
)
|
|
4440
|
+
actions = seq(summary.get("manualActions"))
|
|
4441
|
+
groups: list[str] = []
|
|
4442
|
+
passed = 0
|
|
4443
|
+
for label, keys in labels:
|
|
4444
|
+
statuses = [
|
|
4445
|
+
verification_status(verification[key])
|
|
4446
|
+
for key in keys
|
|
4447
|
+
if key in verification
|
|
4448
|
+
]
|
|
4449
|
+
status = (
|
|
4450
|
+
"FAIL"
|
|
4451
|
+
if any(value in {"FAIL", "ERROR", "BLOCKED"} for value in statuses)
|
|
4452
|
+
else "WARN"
|
|
4453
|
+
if any("WARN" in value or "CONDITIONAL" in value for value in statuses)
|
|
4454
|
+
else "NOT_APPLICABLE"
|
|
4455
|
+
if statuses and all(value == "NOT_APPLICABLE" for value in statuses)
|
|
4456
|
+
else "OK"
|
|
4457
|
+
if statuses
|
|
4458
|
+
and all(
|
|
4459
|
+
value in {"OK", "PASS", "PASSED", "NOT_APPLICABLE"}
|
|
4460
|
+
for value in statuses
|
|
4461
|
+
)
|
|
4462
|
+
else "NOT_RUN"
|
|
4463
|
+
)
|
|
4464
|
+
if status == "OK":
|
|
4465
|
+
passed += 1
|
|
4466
|
+
details = " · ".join(
|
|
4467
|
+
str(rec(verification.get(key)).get("passRate") or status)
|
|
4468
|
+
for key in keys
|
|
4469
|
+
if key in verification
|
|
4470
|
+
) or "未配置该组验证"
|
|
4471
|
+
raw_details = " · ".join(
|
|
4472
|
+
f"{key} · status={verification_status(verification[key])}"
|
|
4473
|
+
+ (
|
|
4474
|
+
f" · failed={rec(verification[key]).get('failed')}"
|
|
4475
|
+
if rec(verification[key]).get("failed") is not None
|
|
4476
|
+
else ""
|
|
4477
|
+
)
|
|
4478
|
+
for key in keys
|
|
4479
|
+
if key in verification
|
|
4480
|
+
)
|
|
4481
|
+
groups.append(
|
|
4482
|
+
f'<article class="verify" title="{esc(raw_details)}"><strong>{esc(label)}</strong>'
|
|
4483
|
+
f'<span title="{esc(status)}">{esc(status_label(status))}</span><small>{esc(details)}</small></article>'
|
|
4484
|
+
)
|
|
4485
|
+
|
|
4486
|
+
commands = seq(rec(summary.get("reportPipeline")).get("commands"))
|
|
4487
|
+
command_rows = "".join(
|
|
4488
|
+
f"<tr><td>{esc(item.get('phase') or '-')}</td>"
|
|
4489
|
+
f"<td><code>{esc(item.get('command'))}</code></td>"
|
|
4490
|
+
f"<td>{esc('OK' if int(item.get('exit_code', item.get('exitCode', 1))) == 0 else 'FAIL')}</td></tr>"
|
|
4491
|
+
for item in commands
|
|
4492
|
+
if isinstance(item, dict)
|
|
4493
|
+
) or '<tr><td colspan="3">没有命令证据</td></tr>'
|
|
4494
|
+
files = seq(summary.get("changedFiles"))
|
|
4495
|
+
file_rows = "".join(
|
|
4496
|
+
f"<tr><td><code>{esc(item.get('path') or item.get('file'))}</code></td>"
|
|
4497
|
+
f"<td>+{esc(item.get('insertions') or 0)}</td>"
|
|
4498
|
+
f"<td>-{esc(item.get('deletions') or 0)}</td></tr>"
|
|
4499
|
+
for item in files
|
|
4500
|
+
if isinstance(item, dict)
|
|
4501
|
+
) or '<tr><td colspan="3">没有变更文件证据</td></tr>'
|
|
4502
|
+
risk_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in risks) or "<li>当前没有未处置风险</li>"
|
|
4503
|
+
action_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in actions) or "<li>无需人工后续动作</li>"
|
|
4504
|
+
release_card = (
|
|
4505
|
+
""
|
|
4506
|
+
if record_only
|
|
4507
|
+
else "<article class=\"card\"><h2>发布与候选</h2>"
|
|
4508
|
+
f"<p>候选证明 / 发布资格:{esc(release.get('decision') or 'NOT_EVALUATED')}</p></article>"
|
|
4509
|
+
)
|
|
4510
|
+
remote = rec(measurements.get("remoteCost"))
|
|
4511
|
+
storage = rec(measurements.get("artifactStorage"))
|
|
4512
|
+
remote_totals = rec(remote.get("totals"))
|
|
4513
|
+
projection = rec(summary.get("projection"))
|
|
4514
|
+
archive_integrity = rec(summary.get("archiveIntegrity"))
|
|
4515
|
+
total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
|
|
4516
|
+
total_files_text = "" if total_files is None else str(total_files)
|
|
4517
|
+
|
|
4518
|
+
return f"""<!doctype html>
|
|
4519
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4520
|
+
<title>Harness 最终报告 · {esc(summary.get("changeName"))}</title>
|
|
4521
|
+
<style>
|
|
4522
|
+
:root{{--bg:#f4f6f8;--card:#fff;--text:#172033;--muted:#667085;--line:#dfe5ec}}
|
|
4523
|
+
@media(prefers-color-scheme:dark){{:root{{--bg:#0b1119;--card:#121a26;--text:#eef3f9;--muted:#9aa8ba;--line:#2a384a}}}}
|
|
4524
|
+
*{{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}}}}
|
|
4525
|
+
</style></head><body><main>
|
|
4526
|
+
<section class="hero"><small>HARNESS · 管理结论</small><h1>{esc(summary.get("changeName"))}</h1>
|
|
4527
|
+
<p>{esc(summary.get("businessGoal") or "未记录业务目标")}</p>
|
|
4528
|
+
<strong title="{esc(current.get("status"))}">{esc(status_label(current.get("status")))}</strong>
|
|
4529
|
+
<p id="finalStatusReasons">{esc(" · ".join(str(item) for item in seq(current.get("reasons"))))}</p>
|
|
4530
|
+
{"<p>归档意图:仅记录 · 未请求发布</p>" if record_only else ""}</section>
|
|
4531
|
+
<section class="metrics"><article class="metric"><small>产品提交</small><strong><code>{esc(str(product_commit)[:10])}</code></strong></article>
|
|
4532
|
+
<article class="metric"><small>验证概览</small><strong>{passed}/5 组通过</strong></article>
|
|
4533
|
+
<article class="metric"><small>风险与动作</small><strong>{len(risks)} / {len(actions)}</strong></article>
|
|
4534
|
+
<article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
|
|
4535
|
+
<article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
|
|
4536
|
+
<article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
|
|
4537
|
+
<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>
|
|
4538
|
+
{release_card}
|
|
4539
|
+
<details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
|
|
4540
|
+
<p>workflowWallClock={esc(timing.get("workflowWallClockMs") if "workflowWallClockMs" in timing else "N/A")}</p>
|
|
4541
|
+
<p>stageActiveExecution={esc(timing.get("stageActiveExecutionMs") if "stageActiveExecutionMs" in timing else "N/A")}</p>
|
|
4542
|
+
<p>stageWallClockSpan={esc(timing.get("stageWallClockSpanMs") if "stageWallClockSpanMs" in timing else "N/A")}</p>
|
|
4543
|
+
<p>reportCutoffAt={esc(timing.get("reportCutoffAt") or "N/A")}</p>
|
|
4544
|
+
<p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
|
|
4545
|
+
<p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
|
|
4546
|
+
<details><summary>技术证据 · 发布与归档治理</summary><div>
|
|
4547
|
+
<p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
|
|
4548
|
+
<p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
|
|
4549
|
+
<p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
|
|
4550
|
+
<details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
|
|
4551
|
+
<details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
|
|
4552
|
+
<details><summary>技术元数据</summary><div><p>productCommit=<code>{esc(product_commit)}</code></p>
|
|
4553
|
+
<p>schemaVersion={esc(summary.get("schemaVersion"))}</p></div></details>
|
|
4554
|
+
</main></body></html>
|
|
4555
|
+
"""
|
|
4556
|
+
|
|
4557
|
+
|
|
4278
4558
|
def render_final_summary(
|
|
4279
4559
|
change_dir: Path,
|
|
4280
4560
|
summary_path: Path,
|
|
@@ -4725,6 +5005,7 @@ def validate_summary(
|
|
|
4725
5005
|
ver = summary.get("verification") or {}
|
|
4726
5006
|
unit = ver.get("unitTests") or {}
|
|
4727
5007
|
api = ver.get("apiTests") or {}
|
|
5008
|
+
browser = ver.get("browserE2E") or {}
|
|
4728
5009
|
pass_rate = unit.get("passRate")
|
|
4729
5010
|
if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
|
|
4730
5011
|
issues.append(
|
|
@@ -4745,6 +5026,22 @@ def validate_summary(
|
|
|
4745
5026
|
"message": f"apiTests.status '{api_status}' not visible in final-summary",
|
|
4746
5027
|
}
|
|
4747
5028
|
)
|
|
5029
|
+
browser_status = str(browser.get("status") or "")
|
|
5030
|
+
if (
|
|
5031
|
+
browser_status
|
|
5032
|
+
and browser_status not in {"", NOT_AVAILABLE}
|
|
5033
|
+
and browser_status not in html
|
|
5034
|
+
):
|
|
5035
|
+
issues.append(
|
|
5036
|
+
{
|
|
5037
|
+
"code": "missing-verification",
|
|
5038
|
+
"severity": "warning",
|
|
5039
|
+
"message": (
|
|
5040
|
+
f"browserE2E.status '{browser_status}' not visible "
|
|
5041
|
+
"in final-summary"
|
|
5042
|
+
),
|
|
5043
|
+
}
|
|
5044
|
+
)
|
|
4748
5045
|
|
|
4749
5046
|
# artifacts / summary-data path hints
|
|
4750
5047
|
am = summary.get("archiveManifest") or {}
|
|
@@ -4777,11 +5074,23 @@ def validate_summary(
|
|
|
4777
5074
|
|
|
4778
5075
|
# status contradiction
|
|
4779
5076
|
final_status = str(summary.get("finalStatus") or "")
|
|
4780
|
-
|
|
5077
|
+
browser_conditional = {
|
|
5078
|
+
"USER_SKIPPED",
|
|
5079
|
+
"BLOCKED",
|
|
5080
|
+
"BLOCKED_BY_ENV",
|
|
5081
|
+
"NOT_RUN",
|
|
5082
|
+
"PARTIAL",
|
|
5083
|
+
}
|
|
5084
|
+
has_skip = api_status == "USER_SKIPPED" or browser_status in browser_conditional or str(
|
|
4781
5085
|
ver.get("dbCompatibility") or ""
|
|
4782
5086
|
) == "BLOCKED_BY_DBA"
|
|
4783
5087
|
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
4784
5088
|
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5089
|
+
has_fail_ver = (
|
|
5090
|
+
has_fail_ver
|
|
5091
|
+
or int(browser.get("failed") or 0) > 0
|
|
5092
|
+
or browser_status == "FAIL"
|
|
5093
|
+
)
|
|
4785
5094
|
stage = summary.get("stageStatus") or {}
|
|
4786
5095
|
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
4787
5096
|
|
|
@@ -4811,13 +5120,26 @@ def validate_summary(
|
|
|
4811
5120
|
ver = summary.get("verification") or {}
|
|
4812
5121
|
unit = ver.get("unitTests") or {}
|
|
4813
5122
|
api = ver.get("apiTests") or {}
|
|
5123
|
+
browser = ver.get("browserE2E") or {}
|
|
4814
5124
|
api_status = str(api.get("status") or "")
|
|
5125
|
+
browser_status = str(browser.get("status") or "")
|
|
4815
5126
|
final_status = str(summary.get("finalStatus") or "")
|
|
4816
|
-
has_skip = api_status == "USER_SKIPPED" or
|
|
5127
|
+
has_skip = api_status == "USER_SKIPPED" or browser_status in {
|
|
5128
|
+
"USER_SKIPPED",
|
|
5129
|
+
"BLOCKED",
|
|
5130
|
+
"BLOCKED_BY_ENV",
|
|
5131
|
+
"NOT_RUN",
|
|
5132
|
+
"PARTIAL",
|
|
5133
|
+
} or str(
|
|
4817
5134
|
ver.get("dbCompatibility") or ""
|
|
4818
5135
|
) == "BLOCKED_BY_DBA"
|
|
4819
5136
|
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
4820
5137
|
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
5138
|
+
has_fail_ver = (
|
|
5139
|
+
has_fail_ver
|
|
5140
|
+
or int(browser.get("failed") or 0) > 0
|
|
5141
|
+
or browser_status == "FAIL"
|
|
5142
|
+
)
|
|
4821
5143
|
stage = summary.get("stageStatus") or {}
|
|
4822
5144
|
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
4823
5145
|
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
@@ -4946,6 +5268,84 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
|
|
|
4946
5268
|
"message": f"stageStatus.{phase}={status} but event reducer says {event_status}",
|
|
4947
5269
|
})
|
|
4948
5270
|
|
|
5271
|
+
# All compatibility mirrors must be derived from the canonical identity.
|
|
5272
|
+
identity_doc = summary.get("changeIdentity")
|
|
5273
|
+
identity_doc = identity_doc if isinstance(identity_doc, dict) else {}
|
|
5274
|
+
canonical = hrm.canonical_identity(summary)
|
|
5275
|
+
mirror_sources = {
|
|
5276
|
+
"productCommit": summary.get("productCommit"),
|
|
5277
|
+
"featureMergeHash": summary.get("featureMergeHash"),
|
|
5278
|
+
"releaseTipHash": summary.get("releaseTipHash"),
|
|
5279
|
+
"productTreeHash": summary.get("productTreeHash"),
|
|
5280
|
+
"environmentHash": summary.get("environmentHash"),
|
|
5281
|
+
}
|
|
5282
|
+
for field, mirror in mirror_sources.items():
|
|
5283
|
+
expected = str(canonical.get(field) or "")
|
|
5284
|
+
actual = str(mirror or "")
|
|
5285
|
+
if field in hrm.CONTENT_HASH_FIELDS:
|
|
5286
|
+
expected = expected.removeprefix("sha256:")
|
|
5287
|
+
actual = actual.removeprefix("sha256:")
|
|
5288
|
+
if identity_doc.get(field) and actual and expected != actual:
|
|
5289
|
+
issues.append(
|
|
5290
|
+
{
|
|
5291
|
+
"code": "IDENTITY_MIRROR_MISMATCH",
|
|
5292
|
+
"severity": "error",
|
|
5293
|
+
"message": f"{field} mirror differs from changeIdentity",
|
|
5294
|
+
"expected": str(canonical.get(field) or ""),
|
|
5295
|
+
"actual": str(mirror or ""),
|
|
5296
|
+
}
|
|
5297
|
+
)
|
|
5298
|
+
|
|
5299
|
+
# Timing categories form a disjoint, wall-clock-conserving partition.
|
|
5300
|
+
timing = summary.get("timing")
|
|
5301
|
+
timing = timing if isinstance(timing, dict) else {}
|
|
5302
|
+
if timing:
|
|
5303
|
+
wall = int(timing.get("workflowWallClockMs") or 0)
|
|
5304
|
+
parts = sum(
|
|
5305
|
+
int(timing.get(key) or 0)
|
|
5306
|
+
for key in (
|
|
5307
|
+
"attributedStageUnionMs",
|
|
5308
|
+
"externalWaitMs",
|
|
5309
|
+
"pausedMs",
|
|
5310
|
+
"agentOrToolUnattributedMs",
|
|
5311
|
+
)
|
|
5312
|
+
)
|
|
5313
|
+
declared_delta = int(timing.get("conservationDeltaMs") or 0)
|
|
5314
|
+
if parts != wall or declared_delta != wall - parts:
|
|
5315
|
+
issues.append(
|
|
5316
|
+
{
|
|
5317
|
+
"code": "TIMING_CONSERVATION_MISMATCH",
|
|
5318
|
+
"severity": "error",
|
|
5319
|
+
"message": (
|
|
5320
|
+
f"timing categories total {parts}ms, wall clock is "
|
|
5321
|
+
f"{wall}ms, declared delta is {declared_delta}ms"
|
|
5322
|
+
),
|
|
5323
|
+
}
|
|
5324
|
+
)
|
|
5325
|
+
|
|
5326
|
+
candidate_gate_for_release = summary.get("candidateVerification")
|
|
5327
|
+
candidate_gate_for_release = (
|
|
5328
|
+
candidate_gate_for_release
|
|
5329
|
+
if isinstance(candidate_gate_for_release, dict)
|
|
5330
|
+
else {}
|
|
5331
|
+
)
|
|
5332
|
+
candidate_status = str(
|
|
5333
|
+
candidate_gate_for_release.get("status")
|
|
5334
|
+
or candidate_gate_for_release.get("code")
|
|
5335
|
+
or ""
|
|
5336
|
+
).upper()
|
|
5337
|
+
if bool(summary.get("releaseEligible")) and (
|
|
5338
|
+
candidate_gate_for_release.get("ok") is False
|
|
5339
|
+
or candidate_status in {"FAIL", "FAILED", "ERROR", "BLOCKED"}
|
|
5340
|
+
):
|
|
5341
|
+
issues.append(
|
|
5342
|
+
{
|
|
5343
|
+
"code": "RELEASE_ELIGIBILITY_CONTRADICTION",
|
|
5344
|
+
"severity": "error",
|
|
5345
|
+
"message": "releaseEligible=true while candidate verification is not successful",
|
|
5346
|
+
}
|
|
5347
|
+
)
|
|
5348
|
+
|
|
4949
5349
|
# Candidate receipt and summary must describe the same immutable subject.
|
|
4950
5350
|
candidate_gate = summary.get("candidateVerification")
|
|
4951
5351
|
candidate_gate = candidate_gate if isinstance(candidate_gate, dict) else {}
|