@hunter-harness/workflow-harness 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +29 -6
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +29 -6
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +29 -6
- package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-execute/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +29 -6
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/general/pi/.harness-build.json +6 -0
- package/harness/bundles/general/pi/CONTEXT.md +98 -0
- package/harness/bundles/general/pi/README.md +478 -0
- package/harness/bundles/general/pi/contracts/build-profile-v3.schema.json +302 -0
- package/harness/bundles/general/pi/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/general/pi/contracts/doctor-capability.schema.json +43 -0
- package/harness/bundles/general/pi/contracts/external-platform-observation.schema.json +62 -0
- package/harness/bundles/general/pi/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/general/pi/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/general/pi/contracts/run-session.schema.json +142 -0
- package/harness/bundles/general/pi/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/general/pi/contracts/service-session.schema.json +147 -0
- package/harness/bundles/general/pi/contracts/workflow-policy.json +574 -0
- package/harness/bundles/general/pi/harness-archive/SKILL.md +322 -0
- package/harness/bundles/general/pi/harness-archive/checklist.md +71 -0
- package/harness/bundles/general/pi/harness-archive/reference.md +150 -0
- package/harness/bundles/general/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/harness/bundles/general/pi/harness-archive/templates/summary-data-template.json +218 -0
- package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +116 -0
- package/harness/bundles/general/pi/harness-codebase-map/checklist.md +126 -0
- package/harness/bundles/general/pi/harness-codebase-map/reference.md +364 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/STACK.md +55 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/TESTING.md +64 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/harness/bundles/general/pi/harness-codebase-map/templates/map-summary.md +64 -0
- package/harness/bundles/general/pi/harness-execute/SKILL.md +170 -0
- package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +103 -0
- package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +97 -0
- package/harness/bundles/general/pi/harness-plan/SKILL.md +158 -0
- package/harness/bundles/general/pi/harness-plan/checklist.md +243 -0
- package/harness/bundles/general/pi/harness-plan/protocols.md +133 -0
- package/harness/bundles/general/pi/harness-plan/reference.md +468 -0
- package/harness/bundles/general/pi/harness-pull/SKILL.md +55 -0
- package/harness/bundles/general/pi/harness-push/SKILL.md +83 -0
- package/harness/bundles/general/pi/harness-review/SKILL.md +175 -0
- package/harness/bundles/general/pi/harness-review/checklist.md +124 -0
- package/harness/bundles/general/pi/harness-review/protocols.md +69 -0
- package/harness/bundles/general/pi/harness-review/reference.md +114 -0
- package/harness/bundles/general/pi/harness-run/SKILL.md +188 -0
- package/harness/bundles/general/pi/harness-run/checklist.md +279 -0
- package/harness/bundles/general/pi/harness-run/protocols.md +141 -0
- package/harness/bundles/general/pi/harness-run/reference.md +886 -0
- package/harness/bundles/general/pi/harness-submit/SKILL.md +182 -0
- package/harness/bundles/general/pi/harness-submit/checklist.md +452 -0
- package/harness/bundles/general/pi/harness-submit/reference.md +152 -0
- package/harness/bundles/general/pi/harness-sync/SKILL.md +72 -0
- package/harness/bundles/general/pi/harness-sync/reference.md +176 -0
- package/harness/bundles/general/pi/harness-test/SKILL.md +250 -0
- package/harness/bundles/general/pi/harness-test/checklist.md +359 -0
- package/harness/bundles/general/pi/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/pi/harness-test/pitfalls.md +307 -0
- package/harness/bundles/general/pi/harness-test/reference.md +880 -0
- package/harness/bundles/general/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/pi/protocols/archive-report-protocol.md +196 -0
- package/harness/bundles/general/pi/protocols/ci-layering-protocol.md +22 -0
- package/harness/bundles/general/pi/protocols/evidence-based-reporting-protocol.md +134 -0
- package/harness/bundles/general/pi/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/general/pi/protocols/external-platform-protocol.md +27 -0
- package/harness/bundles/general/pi/protocols/ledger-protocol.md +365 -0
- package/harness/bundles/general/pi/protocols/memory-closeout-protocol.md +21 -0
- package/harness/bundles/general/pi/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/pi/protocols/powershell-protocol.md +238 -0
- package/harness/bundles/general/pi/protocols/registry-governance-protocol.md +31 -0
- package/harness/bundles/general/pi/protocols/report-pipeline-protocol.md +172 -0
- package/harness/bundles/general/pi/protocols/sensitive-info-protocol.md +110 -0
- package/harness/bundles/general/pi/protocols/state-layout-protocol.md +209 -0
- package/harness/bundles/general/pi/protocols/submit-protocol.md +78 -0
- package/harness/bundles/general/pi/protocols/verification-graph-protocol.md +32 -0
- package/harness/bundles/general/pi/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/pi/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/pi/scripts/harness_archive.py +11345 -0
- package/harness/bundles/general/pi/scripts/harness_change.py +1626 -0
- package/harness/bundles/general/pi/scripts/harness_check_gate.py +113 -0
- package/harness/bundles/general/pi/scripts/harness_context.py +1774 -0
- package/harness/bundles/general/pi/scripts/harness_deploy.py +968 -0
- package/harness/bundles/general/pi/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/general/pi/scripts/harness_environment.py +1690 -0
- package/harness/bundles/general/pi/scripts/harness_events.py +2234 -0
- package/harness/bundles/general/pi/scripts/harness_events_sync.py +1425 -0
- package/harness/bundles/general/pi/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/general/pi/scripts/harness_fixback.py +1975 -0
- package/harness/bundles/general/pi/scripts/harness_gate.py +3816 -0
- package/harness/bundles/general/pi/scripts/harness_headless.py +37 -0
- package/harness/bundles/general/pi/scripts/harness_integration.py +2564 -0
- package/harness/bundles/general/pi/scripts/harness_knowledge_candidates.py +796 -0
- package/harness/bundles/general/pi/scripts/harness_ledger.py +3556 -0
- package/harness/bundles/general/pi/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/pi/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/general/pi/scripts/harness_paths.py +676 -0
- package/harness/bundles/general/pi/scripts/harness_phase.py +1726 -0
- package/harness/bundles/general/pi/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/general/pi/scripts/harness_plan_finalize.py +1016 -0
- package/harness/bundles/general/pi/scripts/harness_preflight.py +650 -0
- package/harness/bundles/general/pi/scripts/harness_process.py +1065 -0
- package/harness/bundles/general/pi/scripts/harness_profile.py +1817 -0
- package/harness/bundles/general/pi/scripts/harness_report_model.py +442 -0
- package/harness/bundles/general/pi/scripts/harness_retry.py +175 -0
- package/harness/bundles/general/pi/scripts/harness_review.py +631 -0
- package/harness/bundles/general/pi/scripts/harness_runtime.py +2243 -0
- package/harness/bundles/general/pi/scripts/harness_service.py +3355 -0
- package/harness/bundles/general/pi/scripts/harness_state.py +365 -0
- package/harness/bundles/general/pi/scripts/harness_sync.py +407 -0
- package/harness/bundles/general/pi/scripts/harness_test_cleanup.py +175 -0
- package/harness/bundles/general/pi/scripts/harness_test_guard.py +1899 -0
- package/harness/bundles/general/pi/scripts/harness_test_runner.py +1586 -0
- package/harness/bundles/general/pi/scripts/harness_verification.py +593 -0
- package/harness/bundles/general/pi/scripts/harness_workflow_policy.py +266 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +29 -6
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +29 -6
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +29 -6
- package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-execute/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +29 -6
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +398 -0
- package/harness/bundles/java/pi/.harness-build.json +6 -0
- package/harness/bundles/java/pi/CONTEXT.md +98 -0
- package/harness/bundles/java/pi/README.md +478 -0
- package/harness/bundles/java/pi/contracts/build-profile-v3.schema.json +302 -0
- package/harness/bundles/java/pi/contracts/ci-metrics.schema.json +36 -0
- package/harness/bundles/java/pi/contracts/doctor-capability.schema.json +43 -0
- package/harness/bundles/java/pi/contracts/external-platform-observation.schema.json +62 -0
- package/harness/bundles/java/pi/contracts/fixtures/managed-execution.json +341 -0
- package/harness/bundles/java/pi/contracts/process-identity.schema.json +132 -0
- package/harness/bundles/java/pi/contracts/run-session.schema.json +142 -0
- package/harness/bundles/java/pi/contracts/service-retirement-receipt.schema.json +85 -0
- package/harness/bundles/java/pi/contracts/service-session.schema.json +147 -0
- package/harness/bundles/java/pi/contracts/workflow-policy.json +574 -0
- package/harness/bundles/java/pi/harness-apidoc/SKILL.md +97 -0
- package/harness/bundles/java/pi/harness-apidoc/checklist.md +142 -0
- package/harness/bundles/java/pi/harness-apidoc/reference.md +233 -0
- package/harness/bundles/java/pi/harness-archive/SKILL.md +322 -0
- package/harness/bundles/java/pi/harness-archive/checklist.md +71 -0
- package/harness/bundles/java/pi/harness-archive/reference.md +150 -0
- package/harness/bundles/java/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/harness/bundles/java/pi/harness-archive/templates/summary-data-template.json +218 -0
- package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +116 -0
- package/harness/bundles/java/pi/harness-codebase-map/checklist.md +126 -0
- package/harness/bundles/java/pi/harness-codebase-map/reference.md +364 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/STACK.md +55 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/TESTING.md +64 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/harness/bundles/java/pi/harness-codebase-map/templates/map-summary.md +64 -0
- package/harness/bundles/java/pi/harness-execute/SKILL.md +170 -0
- package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +103 -0
- package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +97 -0
- package/harness/bundles/java/pi/harness-package/SKILL.md +94 -0
- package/harness/bundles/java/pi/harness-package/checklist.md +322 -0
- package/harness/bundles/java/pi/harness-package/reference.md +210 -0
- package/harness/bundles/java/pi/harness-plan/SKILL.md +158 -0
- package/harness/bundles/java/pi/harness-plan/checklist.md +243 -0
- package/harness/bundles/java/pi/harness-plan/protocols.md +133 -0
- package/harness/bundles/java/pi/harness-plan/reference.md +468 -0
- package/harness/bundles/java/pi/harness-pull/SKILL.md +55 -0
- package/harness/bundles/java/pi/harness-push/SKILL.md +83 -0
- package/harness/bundles/java/pi/harness-review/SKILL.md +175 -0
- package/harness/bundles/java/pi/harness-review/checklist.md +124 -0
- package/harness/bundles/java/pi/harness-review/protocols.md +69 -0
- package/harness/bundles/java/pi/harness-review/reference.md +114 -0
- package/harness/bundles/java/pi/harness-run/SKILL.md +206 -0
- package/harness/bundles/java/pi/harness-run/checklist.md +252 -0
- package/harness/bundles/java/pi/harness-run/protocols.md +141 -0
- package/harness/bundles/java/pi/harness-run/reference.md +820 -0
- package/harness/bundles/java/pi/harness-submit/SKILL.md +189 -0
- package/harness/bundles/java/pi/harness-submit/checklist.md +452 -0
- package/harness/bundles/java/pi/harness-submit/reference.md +152 -0
- package/harness/bundles/java/pi/harness-sync/SKILL.md +72 -0
- package/harness/bundles/java/pi/harness-sync/reference.md +176 -0
- package/harness/bundles/java/pi/harness-test/SKILL.md +264 -0
- package/harness/bundles/java/pi/harness-test/checklist.md +320 -0
- package/harness/bundles/java/pi/harness-test/pitfalls-java.md +16 -0
- package/harness/bundles/java/pi/harness-test/pitfalls.md +307 -0
- package/harness/bundles/java/pi/harness-test/reference.md +792 -0
- package/harness/bundles/java/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/pi/protocols/archive-report-protocol.md +196 -0
- package/harness/bundles/java/pi/protocols/ci-layering-protocol.md +22 -0
- package/harness/bundles/java/pi/protocols/evidence-based-reporting-protocol.md +134 -0
- package/harness/bundles/java/pi/protocols/execution-session-protocol.md +52 -0
- package/harness/bundles/java/pi/protocols/external-platform-protocol.md +27 -0
- package/harness/bundles/java/pi/protocols/ledger-protocol.md +365 -0
- package/harness/bundles/java/pi/protocols/memory-closeout-protocol.md +21 -0
- package/harness/bundles/java/pi/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/pi/protocols/powershell-protocol.md +238 -0
- package/harness/bundles/java/pi/protocols/registry-governance-protocol.md +31 -0
- package/harness/bundles/java/pi/protocols/report-pipeline-protocol.md +172 -0
- package/harness/bundles/java/pi/protocols/sensitive-info-protocol.md +110 -0
- package/harness/bundles/java/pi/protocols/state-layout-protocol.md +209 -0
- package/harness/bundles/java/pi/protocols/submit-protocol.md +78 -0
- package/harness/bundles/java/pi/protocols/verification-graph-protocol.md +32 -0
- package/harness/bundles/java/pi/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/pi/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/pi/scripts/harness_archive.py +11345 -0
- package/harness/bundles/java/pi/scripts/harness_change.py +1626 -0
- package/harness/bundles/java/pi/scripts/harness_check_gate.py +113 -0
- package/harness/bundles/java/pi/scripts/harness_context.py +1774 -0
- package/harness/bundles/java/pi/scripts/harness_deploy.py +968 -0
- package/harness/bundles/java/pi/scripts/harness_efficiency.py +443 -0
- package/harness/bundles/java/pi/scripts/harness_environment.py +1690 -0
- package/harness/bundles/java/pi/scripts/harness_events.py +2234 -0
- package/harness/bundles/java/pi/scripts/harness_events_sync.py +1425 -0
- package/harness/bundles/java/pi/scripts/harness_execution_contracts.py +596 -0
- package/harness/bundles/java/pi/scripts/harness_fixback.py +1975 -0
- package/harness/bundles/java/pi/scripts/harness_gate.py +3816 -0
- package/harness/bundles/java/pi/scripts/harness_headless.py +37 -0
- package/harness/bundles/java/pi/scripts/harness_integration.py +2564 -0
- package/harness/bundles/java/pi/scripts/harness_knowledge_candidates.py +796 -0
- package/harness/bundles/java/pi/scripts/harness_ledger.py +3556 -0
- package/harness/bundles/java/pi/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/pi/scripts/harness_orchestration.py +344 -0
- package/harness/bundles/java/pi/scripts/harness_paths.py +676 -0
- package/harness/bundles/java/pi/scripts/harness_phase.py +1726 -0
- package/harness/bundles/java/pi/scripts/harness_plan_aggregate.py +249 -0
- package/harness/bundles/java/pi/scripts/harness_plan_finalize.py +1016 -0
- package/harness/bundles/java/pi/scripts/harness_preflight.py +650 -0
- package/harness/bundles/java/pi/scripts/harness_process.py +1065 -0
- package/harness/bundles/java/pi/scripts/harness_profile.py +1817 -0
- package/harness/bundles/java/pi/scripts/harness_report_model.py +442 -0
- package/harness/bundles/java/pi/scripts/harness_retry.py +175 -0
- package/harness/bundles/java/pi/scripts/harness_review.py +631 -0
- package/harness/bundles/java/pi/scripts/harness_runtime.py +2243 -0
- package/harness/bundles/java/pi/scripts/harness_service.py +3355 -0
- package/harness/bundles/java/pi/scripts/harness_state.py +365 -0
- package/harness/bundles/java/pi/scripts/harness_sync.py +407 -0
- package/harness/bundles/java/pi/scripts/harness_test_cleanup.py +175 -0
- package/harness/bundles/java/pi/scripts/harness_test_guard.py +1899 -0
- package/harness/bundles/java/pi/scripts/harness_test_runner.py +1586 -0
- package/harness/bundles/java/pi/scripts/harness_verification.py +593 -0
- package/harness/bundles/java/pi/scripts/harness_workflow_policy.py +266 -0
- package/harness/manifests/general/claude-code.json +17 -17
- package/harness/manifests/general/codebuddy.json +17 -17
- package/harness/manifests/general/codex.json +17 -17
- package/harness/manifests/general/cursor.json +17 -17
- package/harness/manifests/general/pi.json +475 -0
- package/harness/manifests/java/claude-code.json +19 -19
- package/harness/manifests/java/codebuddy.json +19 -19
- package/harness/manifests/java/codex.json +19 -19
- package/harness/manifests/java/cursor.json +19 -19
- package/harness/manifests/java/pi.json +499 -0
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1975 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Batch related fixback issues without weakening RED/GREEN evidence."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
21
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
23
|
+
|
|
24
|
+
from harness_paths import resolve_state_dir_for_contract
|
|
25
|
+
import harness_events as he
|
|
26
|
+
import harness_review as hr
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION = 1
|
|
30
|
+
_BATCH_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
31
|
+
RISK_VERIFICATIONS = {
|
|
32
|
+
"security": {"security", "unitTestFull"},
|
|
33
|
+
"migration": {"migration", "unitTestFull"},
|
|
34
|
+
"public-contract": {"apiTest", "unitTestFull"},
|
|
35
|
+
"database": {"dbCompatibility", "unitTestFull"},
|
|
36
|
+
"browser": {"browserTest"},
|
|
37
|
+
"performance": {"performance"},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def now_iso() -> str:
|
|
42
|
+
return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _state_root(change_dir: Path) -> Path:
|
|
46
|
+
return Path(resolve_state_dir_for_contract(change_dir)).resolve()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _batch_root(change_dir: Path) -> Path:
|
|
50
|
+
return _state_root(change_dir) / "fixback" / "batches"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _batch_path(change_dir: Path, batch_id: str) -> Path:
|
|
54
|
+
if not _BATCH_ID.fullmatch(batch_id):
|
|
55
|
+
raise ValueError(f"FIXBACK_BATCH_ID_INVALID: {batch_id}")
|
|
56
|
+
return _batch_root(change_dir) / f"{batch_id}.json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
text = json.dumps(value, ensure_ascii=False, indent=2) + "\n"
|
|
62
|
+
fd, raw = tempfile.mkstemp(
|
|
63
|
+
prefix=f".{path.name}.",
|
|
64
|
+
suffix=".tmp",
|
|
65
|
+
dir=str(path.parent),
|
|
66
|
+
)
|
|
67
|
+
temporary = Path(raw)
|
|
68
|
+
try:
|
|
69
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
70
|
+
handle.write(text)
|
|
71
|
+
handle.flush()
|
|
72
|
+
os.fsync(handle.fileno())
|
|
73
|
+
os.replace(temporary, path)
|
|
74
|
+
finally:
|
|
75
|
+
temporary.unlink(missing_ok=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load(change_dir: Path, batch_id: str) -> dict[str, Any]:
|
|
79
|
+
path = _batch_path(change_dir, batch_id)
|
|
80
|
+
try:
|
|
81
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
82
|
+
except FileNotFoundError as exc:
|
|
83
|
+
raise ValueError(f"FIXBACK_BATCH_NOT_FOUND: {batch_id}") from exc
|
|
84
|
+
if not isinstance(value, dict):
|
|
85
|
+
raise ValueError(f"FIXBACK_BATCH_CORRUPT: {batch_id}")
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _all_batches(change_dir: Path) -> list[dict[str, Any]]:
|
|
90
|
+
root = _batch_root(change_dir)
|
|
91
|
+
if not root.is_dir():
|
|
92
|
+
return []
|
|
93
|
+
batches: list[dict[str, Any]] = []
|
|
94
|
+
for path in sorted(root.glob("*.json")):
|
|
95
|
+
try:
|
|
96
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
97
|
+
except (OSError, json.JSONDecodeError):
|
|
98
|
+
continue
|
|
99
|
+
if isinstance(value, dict):
|
|
100
|
+
batches.append(value)
|
|
101
|
+
return batches
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _session_path(change_dir: Path) -> Path:
|
|
105
|
+
return _state_root(change_dir) / "runtime" / "fixback-session.json"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _next_step(batch: dict[str, Any]) -> str:
|
|
109
|
+
issues = [item for item in batch.get("issues", []) if isinstance(item, dict)]
|
|
110
|
+
if not issues:
|
|
111
|
+
return "add-issues"
|
|
112
|
+
if any(item.get("status") != "RESOLVED" for item in issues):
|
|
113
|
+
return "resolve-issues"
|
|
114
|
+
receipts = batch.get("receipts") if isinstance(batch.get("receipts"), dict) else {}
|
|
115
|
+
if not receipts.get("affected"):
|
|
116
|
+
return "verify-affected"
|
|
117
|
+
if not receipts.get("review"):
|
|
118
|
+
return "review"
|
|
119
|
+
return "close"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def review_fixback_plan(change_dir: Path) -> dict[str, Any]:
|
|
123
|
+
"""Read Review sidecars and return only actionable product-code fixes."""
|
|
124
|
+
|
|
125
|
+
review_root = _state_root(change_dir) / "reports" / "review"
|
|
126
|
+
findings_path = review_root / "review-findings.json"
|
|
127
|
+
dispositions_path = review_root / "fixback-dispositions.json"
|
|
128
|
+
if not findings_path.is_file() or not dispositions_path.is_file():
|
|
129
|
+
return {
|
|
130
|
+
"ok": False,
|
|
131
|
+
"code": "FIXBACK_REVIEW_OUTPUTS_REQUIRED",
|
|
132
|
+
"message": "评审结构化记录不完整,无法安全创建修复批次。",
|
|
133
|
+
"recoveryAction": "先补齐评审发现与处置记录,再重新执行修复",
|
|
134
|
+
}
|
|
135
|
+
try:
|
|
136
|
+
findings_doc = json.loads(findings_path.read_text(encoding="utf-8-sig"))
|
|
137
|
+
dispositions_doc = json.loads(
|
|
138
|
+
dispositions_path.read_text(encoding="utf-8-sig")
|
|
139
|
+
)
|
|
140
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
141
|
+
return {
|
|
142
|
+
"ok": False,
|
|
143
|
+
"code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
|
|
144
|
+
"message": f"评审结构化记录无法读取:{exc}",
|
|
145
|
+
}
|
|
146
|
+
if not isinstance(findings_doc, dict) or not isinstance(dispositions_doc, dict):
|
|
147
|
+
return {
|
|
148
|
+
"ok": False,
|
|
149
|
+
"code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
|
|
150
|
+
"message": "评审结构化记录格式无效。",
|
|
151
|
+
}
|
|
152
|
+
if findings_doc.get("runId") != dispositions_doc.get("runId"):
|
|
153
|
+
return {
|
|
154
|
+
"ok": False,
|
|
155
|
+
"code": "FIXBACK_REVIEW_RUN_MISMATCH",
|
|
156
|
+
"message": "评审发现与处置记录不属于同一轮评审。",
|
|
157
|
+
}
|
|
158
|
+
finding_problems = hr.validate_findings(findings_doc, require_ids=True)
|
|
159
|
+
known_ids = {
|
|
160
|
+
str(item.get("id"))
|
|
161
|
+
for item in findings_doc.get("findings", [])
|
|
162
|
+
if isinstance(item, dict) and item.get("id")
|
|
163
|
+
}
|
|
164
|
+
disposition_problems = hr.validate_dispositions(dispositions_doc, known_ids)
|
|
165
|
+
if finding_problems or disposition_problems:
|
|
166
|
+
return {
|
|
167
|
+
"ok": False,
|
|
168
|
+
"code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
|
|
169
|
+
"message": "评审结构化记录不完整,无法安全创建修复批次。",
|
|
170
|
+
"problems": [*finding_problems, *disposition_problems],
|
|
171
|
+
"recoveryAction": "重新写入完整的评审发现与处置记录后重试",
|
|
172
|
+
}
|
|
173
|
+
dispositions = {
|
|
174
|
+
str(item.get("findingId")): str(item.get("disposition") or "UNKNOWN")
|
|
175
|
+
for item in dispositions_doc.get("dispositions", [])
|
|
176
|
+
if isinstance(item, dict) and item.get("findingId")
|
|
177
|
+
}
|
|
178
|
+
finding_ids = {
|
|
179
|
+
str(item.get("id"))
|
|
180
|
+
for item in findings_doc.get("findings", [])
|
|
181
|
+
if isinstance(item, dict) and item.get("id")
|
|
182
|
+
}
|
|
183
|
+
invalid_actions = sorted(
|
|
184
|
+
str(item.get("id") or f"index-{index}")
|
|
185
|
+
for index, item in enumerate(findings_doc.get("findings", []))
|
|
186
|
+
if isinstance(item, dict)
|
|
187
|
+
and item.get("fixbackAction") not in {"code", "manual", "workflow"}
|
|
188
|
+
)
|
|
189
|
+
missing_dispositions = sorted(finding_ids - set(dispositions))
|
|
190
|
+
if invalid_actions or missing_dispositions:
|
|
191
|
+
return {
|
|
192
|
+
"ok": False,
|
|
193
|
+
"code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
|
|
194
|
+
"message": "评审项缺少明确的修复动作或处置状态。",
|
|
195
|
+
"invalidActions": invalid_actions,
|
|
196
|
+
"missingDispositions": missing_dispositions,
|
|
197
|
+
"recoveryAction": "补齐评审发现的 fixbackAction 与 disposition 后重试",
|
|
198
|
+
}
|
|
199
|
+
actionable: list[dict[str, Any]] = []
|
|
200
|
+
informational: list[dict[str, Any]] = []
|
|
201
|
+
for finding in findings_doc.get("findings", []):
|
|
202
|
+
if not isinstance(finding, dict) or not finding.get("id"):
|
|
203
|
+
continue
|
|
204
|
+
disposition = dispositions.get(str(finding["id"]), "UNKNOWN")
|
|
205
|
+
action = str(finding.get("fixbackAction"))
|
|
206
|
+
item = {
|
|
207
|
+
"issueId": str(finding["id"]),
|
|
208
|
+
"summary": str(finding.get("title") or "评审问题"),
|
|
209
|
+
"severity": str(finding.get("severity") or "YELLOW"),
|
|
210
|
+
"path": str(finding.get("path") or ""),
|
|
211
|
+
"line": int(finding.get("line") or 0),
|
|
212
|
+
"riskTags": sorted(
|
|
213
|
+
{
|
|
214
|
+
str(tag)
|
|
215
|
+
for tag in finding.get("riskTags", [])
|
|
216
|
+
if str(tag).strip()
|
|
217
|
+
}
|
|
218
|
+
),
|
|
219
|
+
"disposition": disposition,
|
|
220
|
+
"fixbackAction": action,
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
action == "code"
|
|
224
|
+
and item["severity"] in {"RED", "YELLOW"}
|
|
225
|
+
and disposition in {"OPEN", "UNKNOWN"}
|
|
226
|
+
):
|
|
227
|
+
actionable.append(item)
|
|
228
|
+
else:
|
|
229
|
+
informational.append(item)
|
|
230
|
+
if not actionable:
|
|
231
|
+
return {
|
|
232
|
+
"ok": True,
|
|
233
|
+
"code": "FIXBACK_NOTHING_TO_APPLY",
|
|
234
|
+
"message": "本轮评审没有需要执行的代码修复。",
|
|
235
|
+
"reviewRunId": findings_doc.get("runId"),
|
|
236
|
+
"issues": [],
|
|
237
|
+
"informationalItems": informational,
|
|
238
|
+
"writesState": False,
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
"ok": True,
|
|
242
|
+
"code": "FIXBACK_READY",
|
|
243
|
+
"message": f"已找到 {len(actionable)} 个需要处理的代码修复项。",
|
|
244
|
+
"reviewRunId": findings_doc.get("runId"),
|
|
245
|
+
"issues": actionable,
|
|
246
|
+
"informationalItems": informational,
|
|
247
|
+
"writesState": False,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def open_review_batch(
|
|
252
|
+
change_dir: Path,
|
|
253
|
+
*,
|
|
254
|
+
plan: dict[str, Any],
|
|
255
|
+
batch_id: str,
|
|
256
|
+
product_identity: str,
|
|
257
|
+
run_id: str,
|
|
258
|
+
attempt: int,
|
|
259
|
+
) -> dict[str, Any]:
|
|
260
|
+
"""Atomically create a populated batch from a validated Review plan."""
|
|
261
|
+
|
|
262
|
+
issues = plan.get("issues") if isinstance(plan, dict) else None
|
|
263
|
+
if plan.get("code") != "FIXBACK_READY" or not isinstance(issues, list) or not issues:
|
|
264
|
+
raise ValueError("FIXBACK_NOTHING_TO_APPLY")
|
|
265
|
+
if not product_identity.strip():
|
|
266
|
+
raise ValueError("FIXBACK_PRODUCT_IDENTITY_REQUIRED")
|
|
267
|
+
opened = [item for item in _all_batches(change_dir) if item.get("status") == "OPEN"]
|
|
268
|
+
if opened:
|
|
269
|
+
existing = sorted(opened, key=lambda item: str(item.get("openedAt") or ""))[0]
|
|
270
|
+
if existing.get("sourceReviewRunId") == plan.get("reviewRunId"):
|
|
271
|
+
result = dict(existing)
|
|
272
|
+
result.update(
|
|
273
|
+
{
|
|
274
|
+
"ok": True,
|
|
275
|
+
"code": "FIXBACK_RESUMED",
|
|
276
|
+
"resumed": True,
|
|
277
|
+
"runId": run_id,
|
|
278
|
+
"attempt": max(1, int(attempt)),
|
|
279
|
+
"nextStep": _next_step(existing),
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
return result
|
|
283
|
+
raise ValueError(
|
|
284
|
+
"FIXBACK_BATCH_ALREADY_OPEN: " + str(existing.get("batchId"))
|
|
285
|
+
)
|
|
286
|
+
path = _batch_path(change_dir, batch_id)
|
|
287
|
+
if path.exists():
|
|
288
|
+
raise ValueError(f"FIXBACK_BATCH_EXISTS: {batch_id}")
|
|
289
|
+
opened_at = now_iso()
|
|
290
|
+
issue_entries = [
|
|
291
|
+
{
|
|
292
|
+
"issueId": str(item["issueId"]),
|
|
293
|
+
"summary": str(item["summary"]),
|
|
294
|
+
"status": "OPEN",
|
|
295
|
+
"riskTags": list(item.get("riskTags") or []),
|
|
296
|
+
"severity": item.get("severity"),
|
|
297
|
+
"path": item.get("path"),
|
|
298
|
+
"line": item.get("line"),
|
|
299
|
+
"redEvidence": None,
|
|
300
|
+
"greenEvidence": None,
|
|
301
|
+
"changedFiles": [],
|
|
302
|
+
"resolvedAt": None,
|
|
303
|
+
}
|
|
304
|
+
for item in issues
|
|
305
|
+
]
|
|
306
|
+
batch = {
|
|
307
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
308
|
+
"batchId": batch_id,
|
|
309
|
+
"status": "OPEN",
|
|
310
|
+
"rootCause": "处理评审中明确要求的代码修复项",
|
|
311
|
+
"sourceReviewRunId": plan.get("reviewRunId"),
|
|
312
|
+
"openedAt": opened_at,
|
|
313
|
+
"updatedAt": opened_at,
|
|
314
|
+
"closedAt": None,
|
|
315
|
+
"baseProductIdentity": product_identity,
|
|
316
|
+
"finalProductIdentity": None,
|
|
317
|
+
"issues": issue_entries,
|
|
318
|
+
"changedFiles": [],
|
|
319
|
+
"requiredVerifications": _required_verifications(issue_entries),
|
|
320
|
+
"receipts": {"affected": None, "review": None},
|
|
321
|
+
"verificationRuns": {"affected": 0, "review": 0},
|
|
322
|
+
}
|
|
323
|
+
_write_json(path, batch)
|
|
324
|
+
session = {
|
|
325
|
+
"schemaVersion": 1,
|
|
326
|
+
"status": "ACTIVE",
|
|
327
|
+
"batchId": batch_id,
|
|
328
|
+
"runId": run_id,
|
|
329
|
+
"attempt": max(1, int(attempt)),
|
|
330
|
+
"nextStep": _next_step(batch),
|
|
331
|
+
"updatedAt": opened_at,
|
|
332
|
+
}
|
|
333
|
+
_write_json(_session_path(change_dir), session)
|
|
334
|
+
he.append_event(
|
|
335
|
+
change_dir,
|
|
336
|
+
phase="execute",
|
|
337
|
+
type_="decision",
|
|
338
|
+
note=f"已从评审结果创建修复批次,共 {len(issue_entries)} 个代码修复项。",
|
|
339
|
+
run_id=run_id,
|
|
340
|
+
attempt=session["attempt"],
|
|
341
|
+
trigger="review-fixback",
|
|
342
|
+
from_phase="review",
|
|
343
|
+
)
|
|
344
|
+
return {
|
|
345
|
+
"ok": True,
|
|
346
|
+
"code": "FIXBACK_OPENED",
|
|
347
|
+
"resumed": False,
|
|
348
|
+
**batch,
|
|
349
|
+
**session,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _default_context_prepare(**kwargs: Any) -> dict[str, Any]:
|
|
354
|
+
import harness_context as hctx
|
|
355
|
+
|
|
356
|
+
return hctx.prepare_context(
|
|
357
|
+
kwargs["project"],
|
|
358
|
+
phase="execute",
|
|
359
|
+
executor=kwargs["executor"],
|
|
360
|
+
change=kwargs["change"],
|
|
361
|
+
trigger="review-fixback",
|
|
362
|
+
preparation_id=kwargs["preparation_id"],
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _default_context_begin(**kwargs: Any) -> dict[str, Any]:
|
|
367
|
+
import harness_context as hctx
|
|
368
|
+
|
|
369
|
+
return hctx.begin_transition(
|
|
370
|
+
kwargs["project"],
|
|
371
|
+
kwargs["change"],
|
|
372
|
+
phase="execute",
|
|
373
|
+
executor=kwargs["executor"],
|
|
374
|
+
preparation_id=kwargs["preparation_id"],
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _default_context_cancel(**kwargs: Any) -> dict[str, Any]:
|
|
379
|
+
import harness_context as hctx
|
|
380
|
+
|
|
381
|
+
return hctx.cancel_prepared_context(
|
|
382
|
+
kwargs["project"],
|
|
383
|
+
kwargs["change"],
|
|
384
|
+
phase="execute",
|
|
385
|
+
executor=kwargs["executor"],
|
|
386
|
+
preparation_id=kwargs["preparation_id"],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _default_gate_begin(**kwargs: Any) -> dict[str, Any]:
|
|
391
|
+
command = [
|
|
392
|
+
sys.executable,
|
|
393
|
+
str(SCRIPTS_DIR / "harness_gate.py"),
|
|
394
|
+
"begin",
|
|
395
|
+
"--json",
|
|
396
|
+
"--phase",
|
|
397
|
+
"execute",
|
|
398
|
+
"--change",
|
|
399
|
+
str(kwargs["change"]),
|
|
400
|
+
"--project",
|
|
401
|
+
str(kwargs["project"]),
|
|
402
|
+
"--skills-root",
|
|
403
|
+
str(kwargs["skills_root"]),
|
|
404
|
+
"--run-id",
|
|
405
|
+
str(kwargs["run_id"]),
|
|
406
|
+
"--note",
|
|
407
|
+
"开始处理评审中确认需要修改的代码问题。",
|
|
408
|
+
# 显式标记这一轮是 review fixback。以前靠 gate 嗅探 note 里有没有
|
|
409
|
+
# "fixback" 字样——而上面这条 note 一个都没有,于是 fixback 的 run 事件
|
|
410
|
+
# 从来没被打上 trigger/from_phase。
|
|
411
|
+
"--fixback",
|
|
412
|
+
]
|
|
413
|
+
for option, key in (
|
|
414
|
+
("--executor-tool", "executor_tool"),
|
|
415
|
+
("--executor-agent", "executor_agent"),
|
|
416
|
+
("--executor-model", "executor_model"),
|
|
417
|
+
):
|
|
418
|
+
value = kwargs.get(key)
|
|
419
|
+
if value:
|
|
420
|
+
command.extend([option, str(value)])
|
|
421
|
+
if kwargs.get("task") is not None:
|
|
422
|
+
command.extend(["--task", str(kwargs["task"])])
|
|
423
|
+
completed = subprocess.run(
|
|
424
|
+
command,
|
|
425
|
+
cwd=str(kwargs["project"]),
|
|
426
|
+
capture_output=True,
|
|
427
|
+
text=True,
|
|
428
|
+
encoding="utf-8",
|
|
429
|
+
errors="replace",
|
|
430
|
+
check=False,
|
|
431
|
+
)
|
|
432
|
+
raw = completed.stdout.strip() or completed.stderr.strip()
|
|
433
|
+
try:
|
|
434
|
+
payload = json.loads(raw)
|
|
435
|
+
except json.JSONDecodeError:
|
|
436
|
+
payload = {
|
|
437
|
+
"ok": False,
|
|
438
|
+
"code": "FIXBACK_GATE_COMMAND_FAILED",
|
|
439
|
+
"message": raw or "修复门禁未返回有效结果。",
|
|
440
|
+
}
|
|
441
|
+
if not isinstance(payload, dict):
|
|
442
|
+
payload = {
|
|
443
|
+
"ok": False,
|
|
444
|
+
"code": "FIXBACK_GATE_COMMAND_FAILED",
|
|
445
|
+
"message": "修复门禁返回了无效结果。",
|
|
446
|
+
}
|
|
447
|
+
payload.setdefault("ok", completed.returncode == 0)
|
|
448
|
+
return payload
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def launch_review_fixback(
|
|
452
|
+
*,
|
|
453
|
+
project: Path,
|
|
454
|
+
change: str,
|
|
455
|
+
change_dir: Path,
|
|
456
|
+
executor: str,
|
|
457
|
+
skills_root: Path,
|
|
458
|
+
product_identity: str,
|
|
459
|
+
run_id: str | None = None,
|
|
460
|
+
attempt: int = 2,
|
|
461
|
+
batch_id: str | None = None,
|
|
462
|
+
task: int | None = None,
|
|
463
|
+
executor_tool: str | None = None,
|
|
464
|
+
executor_agent: str | None = None,
|
|
465
|
+
executor_model: str | None = None,
|
|
466
|
+
context_prepare: Any = None,
|
|
467
|
+
context_begin: Any = None,
|
|
468
|
+
gate_begin: Any = None,
|
|
469
|
+
context_cancel: Any = None,
|
|
470
|
+
) -> dict[str, Any]:
|
|
471
|
+
"""Plan, claim and begin Review Fixback through one fail-closed command."""
|
|
472
|
+
|
|
473
|
+
project = Path(project).resolve()
|
|
474
|
+
change_dir = Path(change_dir).resolve()
|
|
475
|
+
plan = review_fixback_plan(change_dir)
|
|
476
|
+
if not plan.get("ok") or plan.get("code") == "FIXBACK_NOTHING_TO_APPLY":
|
|
477
|
+
return plan
|
|
478
|
+
effective_run_id = run_id or "fixback-" + uuid.uuid4().hex
|
|
479
|
+
effective_attempt = max(2, int(attempt))
|
|
480
|
+
if batch_id is None:
|
|
481
|
+
source = f"{change}:{plan.get('reviewRunId') or 'review'}"
|
|
482
|
+
batch_id = "fb-" + hashlib.sha256(source.encode("utf-8")).hexdigest()[:16]
|
|
483
|
+
prepare_fn = context_prepare or _default_context_prepare
|
|
484
|
+
begin_fn = context_begin or _default_context_begin
|
|
485
|
+
gate_fn = gate_begin or _default_gate_begin
|
|
486
|
+
cancel_fn = context_cancel or _default_context_cancel
|
|
487
|
+
started_at = time.monotonic()
|
|
488
|
+
start_event = he.append_event(
|
|
489
|
+
change_dir,
|
|
490
|
+
phase="execute",
|
|
491
|
+
type_="phase.prepare.start",
|
|
492
|
+
note="正在检查评审修复的前置条件。",
|
|
493
|
+
run_id=effective_run_id,
|
|
494
|
+
attempt=effective_attempt,
|
|
495
|
+
trigger="review-fixback",
|
|
496
|
+
from_phase="review",
|
|
497
|
+
executor_tool=executor_tool,
|
|
498
|
+
executor_agent=executor_agent,
|
|
499
|
+
executor_model=executor_model,
|
|
500
|
+
)
|
|
501
|
+
if not start_event.get("ok"):
|
|
502
|
+
return start_event
|
|
503
|
+
callback_args = {
|
|
504
|
+
"project": project,
|
|
505
|
+
"change": change,
|
|
506
|
+
"change_dir": change_dir,
|
|
507
|
+
"executor": executor,
|
|
508
|
+
"skills_root": Path(skills_root).resolve(),
|
|
509
|
+
"run_id": effective_run_id,
|
|
510
|
+
"attempt": effective_attempt,
|
|
511
|
+
"task": task,
|
|
512
|
+
"executor_tool": executor_tool,
|
|
513
|
+
"executor_agent": executor_agent,
|
|
514
|
+
"executor_model": executor_model,
|
|
515
|
+
"preparation_id": effective_run_id,
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
def blocked(result: dict[str, Any], *, cancel: bool) -> dict[str, Any]:
|
|
519
|
+
cancellation = cancel_fn(**callback_args) if cancel else None
|
|
520
|
+
duration = max(0, round((time.monotonic() - started_at) * 1000))
|
|
521
|
+
code = str(result.get("code") or "FIXBACK_PREPARATION_BLOCKED")
|
|
522
|
+
message = str(result.get("message") or result.get("error") or "前置条件未满足,修复未启动。")
|
|
523
|
+
he.append_event(
|
|
524
|
+
change_dir,
|
|
525
|
+
phase="execute",
|
|
526
|
+
type_="phase.prepare.end",
|
|
527
|
+
status="BLOCKED",
|
|
528
|
+
code=code,
|
|
529
|
+
message=message,
|
|
530
|
+
run_id=effective_run_id,
|
|
531
|
+
attempt=effective_attempt,
|
|
532
|
+
trigger="review-fixback",
|
|
533
|
+
from_phase="review",
|
|
534
|
+
orchestration_active_ms=duration,
|
|
535
|
+
wall_clock_ms=duration,
|
|
536
|
+
executor_tool=executor_tool,
|
|
537
|
+
executor_agent=executor_agent,
|
|
538
|
+
executor_model=executor_model,
|
|
539
|
+
)
|
|
540
|
+
return {
|
|
541
|
+
"ok": False,
|
|
542
|
+
"code": code,
|
|
543
|
+
"message": message,
|
|
544
|
+
"recoveryAction": "处理提示的前置条件后重新执行 /harness-execute --fixback",
|
|
545
|
+
"preparationDurationMs": duration,
|
|
546
|
+
"contextCancellation": cancellation,
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
prepared = prepare_fn(**callback_args)
|
|
550
|
+
if not prepared.get("ok"):
|
|
551
|
+
return blocked(prepared, cancel=False)
|
|
552
|
+
begun = begin_fn(**callback_args)
|
|
553
|
+
if not begun.get("ok"):
|
|
554
|
+
return blocked(begun, cancel=True)
|
|
555
|
+
gate = gate_fn(**callback_args)
|
|
556
|
+
if not gate.get("ok"):
|
|
557
|
+
return blocked(gate, cancel=True)
|
|
558
|
+
batch = open_review_batch(
|
|
559
|
+
change_dir,
|
|
560
|
+
plan=plan,
|
|
561
|
+
batch_id=batch_id,
|
|
562
|
+
product_identity=product_identity,
|
|
563
|
+
run_id=effective_run_id,
|
|
564
|
+
attempt=effective_attempt,
|
|
565
|
+
)
|
|
566
|
+
duration = max(0, round((time.monotonic() - started_at) * 1000))
|
|
567
|
+
he.append_event(
|
|
568
|
+
change_dir,
|
|
569
|
+
phase="execute",
|
|
570
|
+
type_="phase.prepare.end",
|
|
571
|
+
status="STARTED",
|
|
572
|
+
message="前置条件已满足,修复编码已启动。",
|
|
573
|
+
run_id=effective_run_id,
|
|
574
|
+
attempt=effective_attempt,
|
|
575
|
+
trigger="review-fixback",
|
|
576
|
+
from_phase="review",
|
|
577
|
+
orchestration_active_ms=duration,
|
|
578
|
+
wall_clock_ms=duration,
|
|
579
|
+
executor_tool=executor_tool,
|
|
580
|
+
executor_agent=executor_agent,
|
|
581
|
+
executor_model=executor_model,
|
|
582
|
+
)
|
|
583
|
+
return {
|
|
584
|
+
"ok": True,
|
|
585
|
+
"code": "FIXBACK_STARTED",
|
|
586
|
+
"message": f"修复编码已启动,共 {len(batch.get('issues') or [])} 个代码修复项。",
|
|
587
|
+
"runId": effective_run_id,
|
|
588
|
+
"attempt": effective_attempt,
|
|
589
|
+
# close 的 --final-product-identity 必须与 GREEN 证据一致。回显这个值,
|
|
590
|
+
# 调用方就不用自己再推导一遍(推导方式不同就会撞身份不匹配)。
|
|
591
|
+
"resolvedProductIdentity": product_identity,
|
|
592
|
+
"preparationDurationMs": duration,
|
|
593
|
+
"context": prepared,
|
|
594
|
+
"transition": begun,
|
|
595
|
+
"gate": gate,
|
|
596
|
+
"batch": batch,
|
|
597
|
+
# 证据要求随开批次一起交底:晚一步暴露,调用方就已经改完代码,
|
|
598
|
+
# 只能回退伪造 RED。
|
|
599
|
+
"evidenceContract": evidence_contract(
|
|
600
|
+
change_dir=str(change_dir),
|
|
601
|
+
batch_id=str(batch.get("batchId") or ""),
|
|
602
|
+
product_identity=str(batch.get("productIdentity") or ""),
|
|
603
|
+
),
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def resume_batch(
|
|
608
|
+
change_dir: Path,
|
|
609
|
+
*,
|
|
610
|
+
batch_id: str | None,
|
|
611
|
+
product_identity: str | None,
|
|
612
|
+
root_cause: str | None,
|
|
613
|
+
run_id: str | None,
|
|
614
|
+
attempt: int | None,
|
|
615
|
+
) -> dict[str, Any]:
|
|
616
|
+
"""Open once or resume the active Fixback session with a stable run identity."""
|
|
617
|
+
opened = [item for item in _all_batches(change_dir) if item.get("status") == "OPEN"]
|
|
618
|
+
resumed = bool(opened)
|
|
619
|
+
if opened:
|
|
620
|
+
batch = sorted(opened, key=lambda item: str(item.get("openedAt") or ""))[0]
|
|
621
|
+
else:
|
|
622
|
+
if not batch_id or not product_identity or not root_cause:
|
|
623
|
+
raise ValueError(
|
|
624
|
+
"FIXBACK_RESUME_INPUT_REQUIRED: new sessions require batch-id, "
|
|
625
|
+
"product-identity and root-cause"
|
|
626
|
+
)
|
|
627
|
+
batch = open_batch(
|
|
628
|
+
change_dir,
|
|
629
|
+
batch_id=batch_id,
|
|
630
|
+
product_identity=product_identity,
|
|
631
|
+
root_cause=root_cause,
|
|
632
|
+
)
|
|
633
|
+
session_path = _session_path(change_dir)
|
|
634
|
+
existing: dict[str, Any] = {}
|
|
635
|
+
if session_path.is_file():
|
|
636
|
+
try:
|
|
637
|
+
loaded = json.loads(session_path.read_text(encoding="utf-8-sig"))
|
|
638
|
+
existing = loaded if isinstance(loaded, dict) else {}
|
|
639
|
+
except (OSError, json.JSONDecodeError):
|
|
640
|
+
existing = {}
|
|
641
|
+
same_open_session = (
|
|
642
|
+
existing.get("status") == "ACTIVE"
|
|
643
|
+
and existing.get("batchId") == batch.get("batchId")
|
|
644
|
+
)
|
|
645
|
+
effective_run_id = (
|
|
646
|
+
str(existing.get("runId"))
|
|
647
|
+
if same_open_session and existing.get("runId")
|
|
648
|
+
else str(run_id or "fixback-" + uuid.uuid4().hex)
|
|
649
|
+
)
|
|
650
|
+
effective_attempt = (
|
|
651
|
+
int(existing.get("attempt"))
|
|
652
|
+
if same_open_session and isinstance(existing.get("attempt"), int)
|
|
653
|
+
else max(1, int(attempt or 1))
|
|
654
|
+
)
|
|
655
|
+
next_step = _next_step(batch)
|
|
656
|
+
session = {
|
|
657
|
+
"schemaVersion": 1,
|
|
658
|
+
"status": "ACTIVE",
|
|
659
|
+
"batchId": batch.get("batchId"),
|
|
660
|
+
"runId": effective_run_id,
|
|
661
|
+
"attempt": effective_attempt,
|
|
662
|
+
"nextStep": next_step,
|
|
663
|
+
"updatedAt": now_iso(),
|
|
664
|
+
}
|
|
665
|
+
_write_json(session_path, session)
|
|
666
|
+
he.append_event(
|
|
667
|
+
change_dir,
|
|
668
|
+
phase="execute",
|
|
669
|
+
type_="decision",
|
|
670
|
+
note=(
|
|
671
|
+
"已恢复现有修复批次,将从未完成步骤继续。"
|
|
672
|
+
if resumed
|
|
673
|
+
else "已创建修复批次,将按问题逐项执行 RED/GREEN。"
|
|
674
|
+
),
|
|
675
|
+
run_id=effective_run_id,
|
|
676
|
+
)
|
|
677
|
+
return {
|
|
678
|
+
"ok": True,
|
|
679
|
+
"code": "FIXBACK_RESUMED" if resumed else "FIXBACK_OPENED",
|
|
680
|
+
"resumed": resumed,
|
|
681
|
+
"batchId": batch.get("batchId"),
|
|
682
|
+
"runId": effective_run_id,
|
|
683
|
+
"attempt": effective_attempt,
|
|
684
|
+
"nextStep": next_step,
|
|
685
|
+
"requiredVerifications": batch.get("requiredVerifications", []),
|
|
686
|
+
"changedFiles": batch.get("changedFiles", []),
|
|
687
|
+
"sessionPath": str(session_path),
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def invalidate_affected_evidence(
|
|
692
|
+
change_dir: Path,
|
|
693
|
+
*,
|
|
694
|
+
changed_files: list[str],
|
|
695
|
+
batch_id: str,
|
|
696
|
+
) -> dict[str, Any]:
|
|
697
|
+
"""Invalidate only verification targets whose declared inputs were changed."""
|
|
698
|
+
normalized = {
|
|
699
|
+
str(path).replace("\\", "/").lstrip("./")
|
|
700
|
+
for path in changed_files
|
|
701
|
+
if str(path).strip()
|
|
702
|
+
}
|
|
703
|
+
ledger_path = _state_root(change_dir) / "evidence" / "verification-ledger.json"
|
|
704
|
+
if not normalized or not ledger_path.is_file():
|
|
705
|
+
return {"ok": True, "targetIds": [], "validations": []}
|
|
706
|
+
try:
|
|
707
|
+
ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
|
|
708
|
+
except (OSError, json.JSONDecodeError):
|
|
709
|
+
return {"ok": False, "code": "FIXBACK_LEDGER_INVALID"}
|
|
710
|
+
if not isinstance(ledger, dict):
|
|
711
|
+
return {"ok": False, "code": "FIXBACK_LEDGER_INVALID"}
|
|
712
|
+
|
|
713
|
+
def affected(entry: dict[str, Any]) -> bool:
|
|
714
|
+
inputs = entry.get("inputsFiles")
|
|
715
|
+
if not isinstance(inputs, list):
|
|
716
|
+
return False
|
|
717
|
+
input_set = {
|
|
718
|
+
str(path).replace("\\", "/").lstrip("./")
|
|
719
|
+
for path in inputs
|
|
720
|
+
if str(path).strip()
|
|
721
|
+
}
|
|
722
|
+
return bool(normalized & input_set)
|
|
723
|
+
|
|
724
|
+
invalidation = {
|
|
725
|
+
"code": "FIXBACK_AFFECTED_INPUT_CHANGED",
|
|
726
|
+
"batchId": batch_id,
|
|
727
|
+
"changedFiles": sorted(normalized),
|
|
728
|
+
"invalidatedAt": now_iso(),
|
|
729
|
+
}
|
|
730
|
+
target_ids: list[str] = []
|
|
731
|
+
targets = ledger.get("verificationTargets")
|
|
732
|
+
if isinstance(targets, dict):
|
|
733
|
+
for target_id, target in targets.items():
|
|
734
|
+
if isinstance(target, dict) and affected(target):
|
|
735
|
+
target["reusable"] = False
|
|
736
|
+
target["invalidation"] = dict(invalidation)
|
|
737
|
+
target_ids.append(str(target_id))
|
|
738
|
+
validation_names: list[str] = []
|
|
739
|
+
validations = ledger.get("validations")
|
|
740
|
+
if isinstance(validations, dict):
|
|
741
|
+
for name, entry in validations.items():
|
|
742
|
+
if isinstance(entry, dict) and affected(entry):
|
|
743
|
+
entry["reusable"] = False
|
|
744
|
+
entry["invalidation"] = dict(invalidation)
|
|
745
|
+
validation_names.append(str(name))
|
|
746
|
+
_write_json(ledger_path, ledger)
|
|
747
|
+
return {
|
|
748
|
+
"ok": True,
|
|
749
|
+
"targetIds": sorted(target_ids),
|
|
750
|
+
"validations": sorted(validation_names),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _required_verifications(issues: list[dict[str, Any]]) -> list[str]:
|
|
755
|
+
required = {"affected", "review"}
|
|
756
|
+
for issue in issues:
|
|
757
|
+
for risk in issue.get("riskTags", []):
|
|
758
|
+
required.update(RISK_VERIFICATIONS.get(str(risk), set()))
|
|
759
|
+
return sorted(required)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _project_root(change_dir: Path) -> Path:
|
|
763
|
+
resolved = change_dir.resolve()
|
|
764
|
+
if (
|
|
765
|
+
resolved.parent.name == "changes"
|
|
766
|
+
and resolved.parent.parent.name == ".harness"
|
|
767
|
+
):
|
|
768
|
+
return resolved.parent.parent.parent
|
|
769
|
+
return resolved
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _is_within(path: Path, roots: list[Path]) -> bool:
|
|
773
|
+
for root in roots:
|
|
774
|
+
try:
|
|
775
|
+
path.relative_to(root)
|
|
776
|
+
return True
|
|
777
|
+
except ValueError:
|
|
778
|
+
continue
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _evidence_ledger_path(change_dir: Path) -> Path:
|
|
783
|
+
return _state_root(change_dir) / "fixback" / "evidence-ledger.json"
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _load_evidence_ledger(change_dir: Path) -> dict[str, Any]:
|
|
787
|
+
path = _evidence_ledger_path(change_dir)
|
|
788
|
+
if not path.is_file():
|
|
789
|
+
return {"schemaVersion": 1, "evidence": {}}
|
|
790
|
+
try:
|
|
791
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
792
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
793
|
+
raise ValueError(f"FIXBACK_EVIDENCE_LEDGER_INVALID: {exc}") from exc
|
|
794
|
+
if (
|
|
795
|
+
not isinstance(value, dict)
|
|
796
|
+
or value.get("schemaVersion") != 1
|
|
797
|
+
or not isinstance(value.get("evidence"), dict)
|
|
798
|
+
):
|
|
799
|
+
raise ValueError("FIXBACK_EVIDENCE_LEDGER_INVALID")
|
|
800
|
+
return value
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _resolve_evidence_path(change_dir: Path, raw_path: str) -> Path:
|
|
804
|
+
raw = Path(raw_path)
|
|
805
|
+
contract_root = change_dir.resolve()
|
|
806
|
+
state_root = _state_root(change_dir)
|
|
807
|
+
project_root = _project_root(change_dir)
|
|
808
|
+
candidates = (
|
|
809
|
+
[raw.resolve()]
|
|
810
|
+
if raw.is_absolute()
|
|
811
|
+
else [
|
|
812
|
+
(contract_root / raw).resolve(),
|
|
813
|
+
(state_root / raw).resolve(),
|
|
814
|
+
(project_root / raw).resolve(),
|
|
815
|
+
]
|
|
816
|
+
)
|
|
817
|
+
path = next((candidate for candidate in candidates if candidate.is_file()), None)
|
|
818
|
+
if path is None:
|
|
819
|
+
raise ValueError(evidence_error_message("FIXBACK_EVIDENCE_MISSING", raw_path))
|
|
820
|
+
if not _is_within(path, [contract_root, state_root, project_root]):
|
|
821
|
+
raise ValueError(
|
|
822
|
+
evidence_error_message("FIXBACK_EVIDENCE_OUTSIDE_PROJECT", raw_path)
|
|
823
|
+
)
|
|
824
|
+
return path
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def evidence_contract(
|
|
828
|
+
*,
|
|
829
|
+
change_dir: str | None = None,
|
|
830
|
+
batch_id: str | None = None,
|
|
831
|
+
product_identity: str | None = None,
|
|
832
|
+
) -> dict[str, Any]:
|
|
833
|
+
"""开批次时就交出证据契约,而不是等 resolve-issue 报错才逼调用方逆向。
|
|
834
|
+
|
|
835
|
+
契约本身没变过:每个 code 项需要一条修复前的 RED(会话 FAIL)与一条修复后的
|
|
836
|
+
GREEN(会话 OK),两者都必须是 run-start 产出的托管会话证据,且先注册后引用。
|
|
837
|
+
问题在于它此前只写在实现里——调用方通常已经改完代码才发现 RED 要"修复前",
|
|
838
|
+
于是把改动回退、跑一次假 RED、再改回来,凭空多出一轮返工与一段脏工作树。
|
|
839
|
+
"""
|
|
840
|
+
change = change_dir or "<change-dir>"
|
|
841
|
+
batch = batch_id or "<batch-id>"
|
|
842
|
+
product = product_identity or "<product-identity>"
|
|
843
|
+
return {
|
|
844
|
+
"why": "RED 证明问题真实存在,GREEN 证明修复真的生效;缺任一条都无法关批次。",
|
|
845
|
+
"order": [
|
|
846
|
+
"1. 修复前:先用 run-start 跑一次能复现该问题的命令,得到 status=FAIL 的会话(RED)",
|
|
847
|
+
"2. 实施修复",
|
|
848
|
+
"3. 修复后:用同一条命令再跑一次 run-start,得到 status=OK 的会话(GREEN)",
|
|
849
|
+
"4. 两条证据用 evidence-template 生成,再分别 register-evidence 注册",
|
|
850
|
+
"5. resolve-issue 引用这两个已注册的证据文件",
|
|
851
|
+
"6. 关批次前还需两张收据:affected(kind=verification,受影响链的托管会话)"
|
|
852
|
+
"与 review(kind=review,指向 review-findings.json)",
|
|
853
|
+
"7. close 引用这两张收据,--final-product-identity 取 launch-review 回显的 "
|
|
854
|
+
"resolvedProductIdentity",
|
|
855
|
+
],
|
|
856
|
+
"antiPattern": (
|
|
857
|
+
"不要先改完再把修改回退来凑 RED:那样 RED 证明的只是回退后的状态,"
|
|
858
|
+
"不是原始缺陷,中途还会留下脏工作树。"
|
|
859
|
+
),
|
|
860
|
+
"commands": {
|
|
861
|
+
"collectSession": (
|
|
862
|
+
"python harness_runtime.py run-start --state-root "
|
|
863
|
+
f"{change} --verification <名称> --working-directory . "
|
|
864
|
+
f"--product-identity {product} --json -- <复现/验证命令>"
|
|
865
|
+
),
|
|
866
|
+
"awaitSession": (
|
|
867
|
+
"python harness_runtime.py run-status --state-root "
|
|
868
|
+
f"{change} --session-id <sessionId> --wait --json"
|
|
869
|
+
),
|
|
870
|
+
"register": (
|
|
871
|
+
"python harness_fixback.py register-evidence --change-dir "
|
|
872
|
+
f"{change} --evidence <证据 JSON 路径>"
|
|
873
|
+
),
|
|
874
|
+
"resolve": (
|
|
875
|
+
"python harness_fixback.py resolve-issue --change-dir "
|
|
876
|
+
f"{change} --batch-id {batch} --issue-id <issueId> "
|
|
877
|
+
"--red-evidence <red.json> --green-evidence <green.json>"
|
|
878
|
+
),
|
|
879
|
+
"template": (
|
|
880
|
+
"python harness_fixback.py evidence-template --change-dir "
|
|
881
|
+
f"{change} --kind red|green|verification|review "
|
|
882
|
+
"--session <sessionId> --out <证据 JSON 路径>"
|
|
883
|
+
),
|
|
884
|
+
"close": (
|
|
885
|
+
"python harness_fixback.py close --change-dir "
|
|
886
|
+
f"{change} --batch-id {batch} --final-product-identity {product} "
|
|
887
|
+
"--affected-receipt <verification.json> --review-receipt <review.json>"
|
|
888
|
+
),
|
|
889
|
+
},
|
|
890
|
+
"evidenceFile": {
|
|
891
|
+
"schemaVersion": 2,
|
|
892
|
+
"kind": ["red", "green"],
|
|
893
|
+
"status": {"red": "FAIL", "green": "OK"},
|
|
894
|
+
"provenance": {
|
|
895
|
+
"type": "managed-run-session",
|
|
896
|
+
"note": "指向 run-start 产出的 runtime/run-sessions/<sessionId>/session.json",
|
|
897
|
+
"requiredFields": [
|
|
898
|
+
"sessionId",
|
|
899
|
+
"commandHash",
|
|
900
|
+
"resultDigest",
|
|
901
|
+
"runReceiptPath",
|
|
902
|
+
],
|
|
903
|
+
"hint": "这四个字段都从 session.json 直接读;用 evidence-template 生成即可,不必手抄。",
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
"reviewReceipt": {
|
|
907
|
+
"kind": "review",
|
|
908
|
+
"provenance": {
|
|
909
|
+
"type": "harness-review",
|
|
910
|
+
"reviewReportPath": "reports/review/review-findings.json",
|
|
911
|
+
"engine": "harness-review-6d",
|
|
912
|
+
},
|
|
913
|
+
"blocks": (
|
|
914
|
+
"只有 disposition 为 OPEN/未登记 的 RED/YELLOW 会挡住收据。"
|
|
915
|
+
"已判定 FIXED / NOT_APPLICABLE 的放行;ACCEPTED_RISK / DEFERRED 放行"
|
|
916
|
+
"并记入收据的 residualRisks。"
|
|
917
|
+
),
|
|
918
|
+
"antiPattern": (
|
|
919
|
+
"不要为了让 close 通过而清空或删改 review-findings.json——那是发现的真相源,"
|
|
920
|
+
"写空等于抹掉整轮审查的审计轨迹。处置结论写进 fixback-dispositions.json:"
|
|
921
|
+
"python harness_review.py write-dispositions --change-dir <dir> --input <json>"
|
|
922
|
+
),
|
|
923
|
+
},
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
_EVIDENCE_ERROR_HINTS = {
|
|
928
|
+
"FIXBACK_EVIDENCE_MISSING": (
|
|
929
|
+
"路径未找到证据文件。它应当是 run-start 产出的托管会话证据 JSON"
|
|
930
|
+
"(schemaVersion=2,kind=red|green,provenance.type=managed-run-session);"
|
|
931
|
+
"先用 run-start 采集会话,再据此写证据文件。"
|
|
932
|
+
),
|
|
933
|
+
"FIXBACK_EVIDENCE_UNREGISTERED": (
|
|
934
|
+
"证据文件存在但未注册,或注册后内容已变。"
|
|
935
|
+
"先执行 register-evidence 注册(内容改动后需重新注册),再引用。"
|
|
936
|
+
),
|
|
937
|
+
"FIXBACK_EVIDENCE_INVALID": (
|
|
938
|
+
"证据文件不是合法的 schemaVersion=2 托管会话证据。"
|
|
939
|
+
"检查 kind / status / provenance 三个字段。"
|
|
940
|
+
),
|
|
941
|
+
"FIXBACK_EVIDENCE_OUTSIDE_PROJECT": (
|
|
942
|
+
"证据路径落在项目之外。证据必须放在变更目录或项目内,便于随归档留痕。"
|
|
943
|
+
),
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def evidence_error_message(code: str, raw_path: str) -> str:
|
|
948
|
+
"""把只回显路径的错误码,补成能直接照做的一句话。"""
|
|
949
|
+
hint = _EVIDENCE_ERROR_HINTS.get(code)
|
|
950
|
+
base = f"{code}: {raw_path}"
|
|
951
|
+
return base if hint is None else f"{base} — {hint}"
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
# 一条发现"还没被处置"才该挡住收据。OPEN 是显式待办,UNKNOWN 是根本没登记;
|
|
955
|
+
# 其余(FIXED / NOT_APPLICABLE / ACCEPTED_RISK / DEFERRED)都已有人做过判断。
|
|
956
|
+
_UNPROCESSED_DISPOSITIONS = {"OPEN", "UNKNOWN"}
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _load_dispositions(change_dir: Path) -> dict[str, str]:
|
|
960
|
+
"""读 fixback-dispositions.json;缺失或损坏都按"无处置"处理,不抛。"""
|
|
961
|
+
path = hr.dispositions_path(change_dir)
|
|
962
|
+
if not path.is_file():
|
|
963
|
+
return {}
|
|
964
|
+
try:
|
|
965
|
+
doc = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
966
|
+
except (OSError, json.JSONDecodeError):
|
|
967
|
+
return {}
|
|
968
|
+
if not isinstance(doc, dict):
|
|
969
|
+
return {}
|
|
970
|
+
mapping: dict[str, str] = {}
|
|
971
|
+
for item in doc.get("dispositions") or []:
|
|
972
|
+
if isinstance(item, dict):
|
|
973
|
+
finding_id = item.get("findingId")
|
|
974
|
+
if isinstance(finding_id, str) and finding_id.strip():
|
|
975
|
+
mapping[finding_id] = str(item.get("disposition") or "")
|
|
976
|
+
return mapping
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def classify_review_findings(
|
|
980
|
+
change_dir: Path, findings: Any
|
|
981
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
982
|
+
"""按 disposition 把 RED/YELLOW 分成"未处置(阻塞)"与"已接受的残留风险"。
|
|
983
|
+
|
|
984
|
+
此前这里只看 severity:只要 findings 里还有一条 RED/YELLOW,收据就永远注册
|
|
985
|
+
不了。唯一能过关的做法是把 review-findings.json 清空——2026-08-19 的执行记录
|
|
986
|
+
里就是这么干的,3 条原始发现加 2 条复审发现的审计轨迹被一次写空抹掉。
|
|
987
|
+
|
|
988
|
+
findings sidecar 是发现的真相源,dispositions sidecar 才是处置的真相源。
|
|
989
|
+
门禁该问的是"还有没有没人处理的问题",不是"还有没有问题"。
|
|
990
|
+
"""
|
|
991
|
+
dispositions = _load_dispositions(change_dir)
|
|
992
|
+
blocking: list[dict[str, Any]] = []
|
|
993
|
+
residual: list[dict[str, Any]] = []
|
|
994
|
+
for item in findings or []:
|
|
995
|
+
if not isinstance(item, dict) or item.get("severity") not in {"RED", "YELLOW"}:
|
|
996
|
+
continue
|
|
997
|
+
finding_id = str(item.get("id") or "")
|
|
998
|
+
disposition = dispositions.get(finding_id) or "UNKNOWN"
|
|
999
|
+
if disposition not in hr.DISPOSITIONS:
|
|
1000
|
+
disposition = "UNKNOWN"
|
|
1001
|
+
entry = {
|
|
1002
|
+
"findingId": finding_id,
|
|
1003
|
+
"severity": item.get("severity"),
|
|
1004
|
+
"title": item.get("title"),
|
|
1005
|
+
"disposition": disposition,
|
|
1006
|
+
}
|
|
1007
|
+
if disposition in _UNPROCESSED_DISPOSITIONS:
|
|
1008
|
+
blocking.append(entry)
|
|
1009
|
+
elif disposition in hr.CURRENT_RISK_DISPOSITIONS:
|
|
1010
|
+
residual.append(entry)
|
|
1011
|
+
return residual, blocking
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _provenance_error(code: str, problems: list[dict[str, Any]], recovery: str) -> ValueError:
|
|
1015
|
+
"""把只有错误码的失败,补成"哪个字段、期望什么、实际什么、怎么修"。"""
|
|
1016
|
+
detail = "; ".join(
|
|
1017
|
+
f"{item['field']}: expected={item['expected']!r} actual={item['actual']!r}"
|
|
1018
|
+
for item in problems
|
|
1019
|
+
)
|
|
1020
|
+
return ValueError(f"{code}: {detail} — {recovery}")
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def resolve_product_identity(change_dir: Path) -> str:
|
|
1024
|
+
"""推导当前产品身份。
|
|
1025
|
+
|
|
1026
|
+
`--product-identity` 在 5 个子命令里必填,却在全仓文档里零命中;调用方只能去
|
|
1027
|
+
读源码猜,猜完还要在 close 时原样复述一遍。规范取值就是当前 HEAD——把它做成
|
|
1028
|
+
可推导的默认值,猜的环节就没有了。
|
|
1029
|
+
"""
|
|
1030
|
+
project_root = _project_root(change_dir)
|
|
1031
|
+
try:
|
|
1032
|
+
result = subprocess.run(
|
|
1033
|
+
["git", "rev-parse", "HEAD"],
|
|
1034
|
+
cwd=str(project_root),
|
|
1035
|
+
capture_output=True,
|
|
1036
|
+
text=True,
|
|
1037
|
+
encoding="utf-8",
|
|
1038
|
+
timeout=10,
|
|
1039
|
+
check=False,
|
|
1040
|
+
)
|
|
1041
|
+
except (OSError, subprocess.SubprocessError):
|
|
1042
|
+
result = None
|
|
1043
|
+
if result is not None and result.returncode == 0:
|
|
1044
|
+
head = (result.stdout or "").strip()
|
|
1045
|
+
if head:
|
|
1046
|
+
return head
|
|
1047
|
+
# 非 git 工作区(或 git 不可用)仍需一个稳定值,否则两次推导不一致会让
|
|
1048
|
+
# close 的身份比对无故失败。
|
|
1049
|
+
digest = hashlib.sha256(str(project_root).encode("utf-8")).hexdigest()
|
|
1050
|
+
return f"unversioned:{digest}"
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
_EVIDENCE_KINDS = ("red", "green", "verification", "review")
|
|
1054
|
+
_KIND_EXPECTED_SESSION_STATUS = {"red": "FAIL", "green": "OK", "verification": "OK"}
|
|
1055
|
+
_KIND_EVIDENCE_STATUS = {"red": "FAIL", "green": "OK", "verification": "OK", "review": "OK"}
|
|
1056
|
+
_KIND_DEFAULT_GATES = {"verification": ["affected"], "review": ["review"]}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def evidence_template(
|
|
1060
|
+
change_dir: Path,
|
|
1061
|
+
*,
|
|
1062
|
+
kind: str,
|
|
1063
|
+
session_id: str | None = None,
|
|
1064
|
+
out: str | None = None,
|
|
1065
|
+
evidence_id: str | None = None,
|
|
1066
|
+
product_identity: str | None = None,
|
|
1067
|
+
passed_gates: list[str] | None = None,
|
|
1068
|
+
) -> dict[str, Any]:
|
|
1069
|
+
"""从托管会话/审查 sidecar 直接生成可注册的证据文件。
|
|
1070
|
+
|
|
1071
|
+
ledger 早就有 scenario-receipt-template,一次就能给对骨架;fixback 没有同类
|
|
1072
|
+
入口,于是每条证据都得手写 JSON,再靠 FIXBACK_*_PROVENANCE_INVALID 反推缺了
|
|
1073
|
+
哪个字段。sessionId / commandHash / resultDigest 三个字段全都能从 session.json
|
|
1074
|
+
直接读出来——让人手抄它们没有任何意义。
|
|
1075
|
+
"""
|
|
1076
|
+
if kind not in _EVIDENCE_KINDS:
|
|
1077
|
+
return {
|
|
1078
|
+
"ok": False,
|
|
1079
|
+
"code": "FIXBACK_EVIDENCE_KIND_INVALID",
|
|
1080
|
+
"kind": kind,
|
|
1081
|
+
"recoveryAction": f"--kind 必须是 {'/'.join(_EVIDENCE_KINDS)} 之一。",
|
|
1082
|
+
}
|
|
1083
|
+
state_root = _state_root(change_dir)
|
|
1084
|
+
identity = (product_identity or "").strip()
|
|
1085
|
+
gates = list(passed_gates) if passed_gates else list(_KIND_DEFAULT_GATES.get(kind, []))
|
|
1086
|
+
|
|
1087
|
+
if kind == "review":
|
|
1088
|
+
review_path = (state_root / "reports" / "review" / "review-findings.json").resolve()
|
|
1089
|
+
if not review_path.is_file():
|
|
1090
|
+
return {
|
|
1091
|
+
"ok": False,
|
|
1092
|
+
"code": "FIXBACK_REVIEW_FINDINGS_MISSING",
|
|
1093
|
+
"expectedPath": str(review_path),
|
|
1094
|
+
"recoveryAction": (
|
|
1095
|
+
"先用 harness_review.py write-findings 写入本轮审查发现,"
|
|
1096
|
+
"再生成 review 证据。"
|
|
1097
|
+
),
|
|
1098
|
+
}
|
|
1099
|
+
review = json.loads(review_path.read_text(encoding="utf-8-sig"))
|
|
1100
|
+
residual, blocking = classify_review_findings(change_dir, review.get("findings"))
|
|
1101
|
+
if blocking:
|
|
1102
|
+
return {
|
|
1103
|
+
"ok": False,
|
|
1104
|
+
"code": "FIXBACK_REVIEW_FINDINGS_UNDISPOSITIONED",
|
|
1105
|
+
"blocking": blocking,
|
|
1106
|
+
"recoveryAction": (
|
|
1107
|
+
"以下发现尚未处置:"
|
|
1108
|
+
+ ", ".join(str(item["findingId"]) for item in blocking)
|
|
1109
|
+
+ "。用 harness_review.py write-dispositions 标注 FIXED / "
|
|
1110
|
+
"ACCEPTED_RISK / DEFERRED / NOT_APPLICABLE 后重试;"
|
|
1111
|
+
"不要删改 review-findings.json——它是发现的真相源。"
|
|
1112
|
+
),
|
|
1113
|
+
}
|
|
1114
|
+
provenance = {
|
|
1115
|
+
"type": "harness-review",
|
|
1116
|
+
"reviewReportPath": str(review_path),
|
|
1117
|
+
"reviewRunId": review.get("runId"),
|
|
1118
|
+
"engine": "harness-review-6d",
|
|
1119
|
+
"sourceDigest": "sha256:" + hashlib.sha256(review_path.read_bytes()).hexdigest(),
|
|
1120
|
+
}
|
|
1121
|
+
resolved_id = evidence_id or f"review-{review.get('runId') or uuid.uuid4().hex}"
|
|
1122
|
+
if not identity:
|
|
1123
|
+
identity = resolve_product_identity(change_dir)
|
|
1124
|
+
extra: dict[str, Any] = {"residualRisks": residual}
|
|
1125
|
+
else:
|
|
1126
|
+
if not (session_id or "").strip():
|
|
1127
|
+
return {
|
|
1128
|
+
"ok": False,
|
|
1129
|
+
"code": "FIXBACK_SESSION_REQUIRED",
|
|
1130
|
+
"recoveryAction": (
|
|
1131
|
+
f"--kind {kind} 需要 --session <sessionId>;先用 "
|
|
1132
|
+
"harness_runtime.py run-start 采集托管会话,再用它的 sessionId。"
|
|
1133
|
+
),
|
|
1134
|
+
}
|
|
1135
|
+
run_path = (
|
|
1136
|
+
state_root / "runtime" / "run-sessions" / str(session_id) / "session.json"
|
|
1137
|
+
).resolve()
|
|
1138
|
+
if not run_path.is_file():
|
|
1139
|
+
return {
|
|
1140
|
+
"ok": False,
|
|
1141
|
+
"code": "FIXBACK_SESSION_MISSING",
|
|
1142
|
+
"expectedPath": str(run_path),
|
|
1143
|
+
"recoveryAction": (
|
|
1144
|
+
"该 sessionId 下没有 session.json。确认 run-start 的 "
|
|
1145
|
+
f"--state-root 指向 {state_root},并用 run-status --wait 等到终态。"
|
|
1146
|
+
),
|
|
1147
|
+
}
|
|
1148
|
+
run = json.loads(run_path.read_text(encoding="utf-8-sig"))
|
|
1149
|
+
expected_status = _KIND_EXPECTED_SESSION_STATUS[kind]
|
|
1150
|
+
actual_status = str(run.get("status") or "")
|
|
1151
|
+
if actual_status != expected_status:
|
|
1152
|
+
return {
|
|
1153
|
+
"ok": False,
|
|
1154
|
+
"code": "FIXBACK_SESSION_STATUS_MISMATCH",
|
|
1155
|
+
"kind": kind,
|
|
1156
|
+
"expectedStatus": expected_status,
|
|
1157
|
+
"actualStatus": actual_status,
|
|
1158
|
+
"recoveryAction": (
|
|
1159
|
+
f"kind={kind} 需要 status={expected_status} 的会话,实际是 "
|
|
1160
|
+
f"{actual_status or '(空)'}。RED 必须来自**修复前**能复现问题的失败"
|
|
1161
|
+
"会话(FAIL);不要先改完再回退代码去凑 RED——那证明的是回退后的"
|
|
1162
|
+
"状态,不是原始缺陷。"
|
|
1163
|
+
),
|
|
1164
|
+
}
|
|
1165
|
+
provenance = {
|
|
1166
|
+
"type": "managed-run-session",
|
|
1167
|
+
"runReceiptPath": str(run_path),
|
|
1168
|
+
"sessionId": run.get("sessionId"),
|
|
1169
|
+
"commandHash": run.get("commandHash"),
|
|
1170
|
+
"resultDigest": run.get("resultDigest"),
|
|
1171
|
+
}
|
|
1172
|
+
resolved_id = evidence_id or f"{kind}-{session_id}"
|
|
1173
|
+
if not identity:
|
|
1174
|
+
identity = str(run.get("productIdentity") or "").strip() or resolve_product_identity(
|
|
1175
|
+
change_dir
|
|
1176
|
+
)
|
|
1177
|
+
extra = {}
|
|
1178
|
+
|
|
1179
|
+
evidence = {
|
|
1180
|
+
"schemaVersion": 2,
|
|
1181
|
+
"kind": kind,
|
|
1182
|
+
"status": _KIND_EVIDENCE_STATUS[kind],
|
|
1183
|
+
"evidenceId": resolved_id,
|
|
1184
|
+
"productIdentity": identity,
|
|
1185
|
+
"passedGates": gates,
|
|
1186
|
+
"provenance": provenance,
|
|
1187
|
+
}
|
|
1188
|
+
target = Path(out) if out else Path("runtime") / f"fixback-{resolved_id}.json"
|
|
1189
|
+
path = target if target.is_absolute() else (change_dir / target)
|
|
1190
|
+
path = path.resolve()
|
|
1191
|
+
if not _is_within(path, [change_dir.resolve(), state_root, _project_root(change_dir)]):
|
|
1192
|
+
return {
|
|
1193
|
+
"ok": False,
|
|
1194
|
+
"code": "FIXBACK_EVIDENCE_OUTSIDE_PROJECT",
|
|
1195
|
+
"path": str(path),
|
|
1196
|
+
"recoveryAction": "--out 必须落在变更目录或项目内,证据才能随归档留痕。",
|
|
1197
|
+
}
|
|
1198
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1199
|
+
path.write_text(
|
|
1200
|
+
json.dumps(evidence, ensure_ascii=False, indent=2) + "\n",
|
|
1201
|
+
encoding="utf-8",
|
|
1202
|
+
newline="\n",
|
|
1203
|
+
)
|
|
1204
|
+
return {
|
|
1205
|
+
"ok": True,
|
|
1206
|
+
"code": "FIXBACK_EVIDENCE_TEMPLATE_WRITTEN",
|
|
1207
|
+
"path": str(path),
|
|
1208
|
+
"kind": kind,
|
|
1209
|
+
"evidenceId": resolved_id,
|
|
1210
|
+
"productIdentity": identity,
|
|
1211
|
+
"evidence": evidence,
|
|
1212
|
+
"nextAction": (
|
|
1213
|
+
"python harness_fixback.py register-evidence --change-dir "
|
|
1214
|
+
f"{change_dir} --evidence {path}"
|
|
1215
|
+
),
|
|
1216
|
+
**extra,
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
1221
|
+
"""Register evidence only after validating its authoritative provenance."""
|
|
1222
|
+
path = _resolve_evidence_path(change_dir, raw_path)
|
|
1223
|
+
try:
|
|
1224
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1225
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1226
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}: {exc}") from exc
|
|
1227
|
+
if not isinstance(value, dict) or value.get("schemaVersion") != 2:
|
|
1228
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
1229
|
+
kind = str(value.get("kind") or "")
|
|
1230
|
+
status = str(value.get("status") or "").upper()
|
|
1231
|
+
evidence_id = str(value.get("evidenceId") or "").strip()
|
|
1232
|
+
product_identity = str(value.get("productIdentity") or "").strip()
|
|
1233
|
+
passed_gates = value.get("passedGates")
|
|
1234
|
+
provenance = value.get("provenance")
|
|
1235
|
+
if (
|
|
1236
|
+
kind not in {"red", "green", "verification", "review"}
|
|
1237
|
+
or status not in {"FAIL", "PASS", "OK"}
|
|
1238
|
+
or not evidence_id
|
|
1239
|
+
or not product_identity
|
|
1240
|
+
or not isinstance(passed_gates, list)
|
|
1241
|
+
or any(not isinstance(item, str) or not item.strip() for item in passed_gates)
|
|
1242
|
+
or not isinstance(provenance, dict)
|
|
1243
|
+
):
|
|
1244
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
1245
|
+
state_root = _state_root(change_dir)
|
|
1246
|
+
residual_risks: list[dict[str, Any]] = []
|
|
1247
|
+
if kind in {"red", "green", "verification"}:
|
|
1248
|
+
if provenance.get("type") != "managed-run-session":
|
|
1249
|
+
raise ValueError("FIXBACK_RUN_PROVENANCE_REQUIRED")
|
|
1250
|
+
run_path = _resolve_evidence_path(
|
|
1251
|
+
change_dir, str(provenance.get("runReceiptPath") or "")
|
|
1252
|
+
)
|
|
1253
|
+
expected_run_root = (state_root / "runtime" / "run-sessions").resolve()
|
|
1254
|
+
if not _is_within(run_path, [expected_run_root]) or run_path.name != "session.json":
|
|
1255
|
+
raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID")
|
|
1256
|
+
try:
|
|
1257
|
+
run = json.loads(run_path.read_text(encoding="utf-8-sig"))
|
|
1258
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1259
|
+
raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID") from exc
|
|
1260
|
+
expected_status = "FAIL" if kind == "red" else "OK"
|
|
1261
|
+
if not isinstance(run, dict):
|
|
1262
|
+
raise _provenance_error(
|
|
1263
|
+
"FIXBACK_RUN_PROVENANCE_INVALID",
|
|
1264
|
+
[{"field": "session.json", "expected": "object", "actual": type(run).__name__}],
|
|
1265
|
+
"run-start 产出的 session.json 应当是一个 JSON 对象。",
|
|
1266
|
+
)
|
|
1267
|
+
# 逐字段比对而不是一个大 or:只回显错误码时,调用方唯一的出路是去读源码
|
|
1268
|
+
# 反推是哪一项不匹配(2026-08-19 的执行记录里就读了三遍)。
|
|
1269
|
+
checks = [
|
|
1270
|
+
("status", expected_status, run.get("status")),
|
|
1271
|
+
("testProcessStarted", True, run.get("testProcessStarted")),
|
|
1272
|
+
("session.productIdentity", product_identity, run.get("productIdentity")),
|
|
1273
|
+
("provenance.sessionId", run.get("sessionId"), provenance.get("sessionId")),
|
|
1274
|
+
("provenance.commandHash", run.get("commandHash"), provenance.get("commandHash")),
|
|
1275
|
+
("provenance.resultDigest", run.get("resultDigest"), provenance.get("resultDigest")),
|
|
1276
|
+
]
|
|
1277
|
+
problems = [
|
|
1278
|
+
{"field": field, "expected": expected, "actual": actual}
|
|
1279
|
+
for field, expected, actual in checks
|
|
1280
|
+
if expected != actual
|
|
1281
|
+
]
|
|
1282
|
+
for field in ("commandHash", "resultDigest"):
|
|
1283
|
+
value = str(run.get(field) or "")
|
|
1284
|
+
if not value.startswith("sha256:"):
|
|
1285
|
+
problems.append(
|
|
1286
|
+
{"field": f"session.{field}", "expected": "sha256:...", "actual": value}
|
|
1287
|
+
)
|
|
1288
|
+
if not str(run.get("endedAt") or "").strip():
|
|
1289
|
+
problems.append({"field": "session.endedAt", "expected": "非空时间戳", "actual": ""})
|
|
1290
|
+
if problems:
|
|
1291
|
+
raise _provenance_error(
|
|
1292
|
+
"FIXBACK_RUN_PROVENANCE_INVALID",
|
|
1293
|
+
problems,
|
|
1294
|
+
"不要手改证据文件去对齐;用 `harness_fixback.py evidence-template "
|
|
1295
|
+
f"--change-dir {change_dir} --kind {kind} --session <sessionId>` "
|
|
1296
|
+
"重新生成,它直接从 session.json 取这些字段。",
|
|
1297
|
+
)
|
|
1298
|
+
else:
|
|
1299
|
+
if provenance.get("type") != "harness-review":
|
|
1300
|
+
raise ValueError("FIXBACK_REVIEW_PROVENANCE_REQUIRED")
|
|
1301
|
+
review_path = _resolve_evidence_path(
|
|
1302
|
+
change_dir, str(provenance.get("reviewReportPath") or "")
|
|
1303
|
+
)
|
|
1304
|
+
expected_review_path = (
|
|
1305
|
+
state_root / "reports" / "review" / "review-findings.json"
|
|
1306
|
+
).resolve()
|
|
1307
|
+
if review_path != expected_review_path:
|
|
1308
|
+
raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID")
|
|
1309
|
+
try:
|
|
1310
|
+
review = json.loads(review_path.read_text(encoding="utf-8-sig"))
|
|
1311
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1312
|
+
raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID") from exc
|
|
1313
|
+
source_digest = "sha256:" + hashlib.sha256(review_path.read_bytes()).hexdigest()
|
|
1314
|
+
findings = review.get("findings") if isinstance(review, dict) else None
|
|
1315
|
+
problems: list[dict[str, Any]] = []
|
|
1316
|
+
if not isinstance(review, dict) or review.get("schemaVersion") != 1:
|
|
1317
|
+
problems.append({
|
|
1318
|
+
"field": "review-findings.schemaVersion",
|
|
1319
|
+
"expected": 1,
|
|
1320
|
+
"actual": review.get("schemaVersion") if isinstance(review, dict) else None,
|
|
1321
|
+
})
|
|
1322
|
+
if not isinstance(findings, list):
|
|
1323
|
+
problems.append({
|
|
1324
|
+
"field": "review-findings.findings",
|
|
1325
|
+
"expected": "list",
|
|
1326
|
+
"actual": type(findings).__name__,
|
|
1327
|
+
})
|
|
1328
|
+
for field, expected, actual in (
|
|
1329
|
+
(
|
|
1330
|
+
"provenance.reviewRunId",
|
|
1331
|
+
review.get("runId") if isinstance(review, dict) else None,
|
|
1332
|
+
provenance.get("reviewRunId"),
|
|
1333
|
+
),
|
|
1334
|
+
("provenance.engine", "harness-review-6d", provenance.get("engine")),
|
|
1335
|
+
("provenance.sourceDigest", source_digest, provenance.get("sourceDigest")),
|
|
1336
|
+
):
|
|
1337
|
+
if expected != actual:
|
|
1338
|
+
problems.append({"field": field, "expected": expected, "actual": actual})
|
|
1339
|
+
if problems:
|
|
1340
|
+
raise _provenance_error(
|
|
1341
|
+
"FIXBACK_REVIEW_PROVENANCE_INVALID",
|
|
1342
|
+
problems,
|
|
1343
|
+
"用 `harness_fixback.py evidence-template --change-dir "
|
|
1344
|
+
f"{change_dir} --kind review` 重新生成 review 证据。",
|
|
1345
|
+
)
|
|
1346
|
+
# 只有"没人处置过"的 RED/YELLOW 才该挡住收据;已判定 FIXED / ACCEPTED_RISK /
|
|
1347
|
+
# DEFERRED / NOT_APPLICABLE 的照常放行,findings sidecar 原样保留。
|
|
1348
|
+
residual_risks, blocking = classify_review_findings(change_dir, findings)
|
|
1349
|
+
if blocking:
|
|
1350
|
+
raise _provenance_error(
|
|
1351
|
+
"FIXBACK_REVIEW_PROVENANCE_INVALID",
|
|
1352
|
+
[
|
|
1353
|
+
{
|
|
1354
|
+
"field": f"finding[{item['findingId']}]",
|
|
1355
|
+
"expected": "已处置(FIXED / ACCEPTED_RISK / DEFERRED / NOT_APPLICABLE)",
|
|
1356
|
+
"actual": item["disposition"],
|
|
1357
|
+
}
|
|
1358
|
+
for item in blocking
|
|
1359
|
+
],
|
|
1360
|
+
"用 `harness_review.py write-dispositions` 为这些发现登记处置结论后重试。"
|
|
1361
|
+
"**不要**清空或删改 review-findings.json——它是发现的真相源,"
|
|
1362
|
+
"处置结论属于 fixback-dispositions.json。",
|
|
1363
|
+
)
|
|
1364
|
+
record = {
|
|
1365
|
+
"path": str(path),
|
|
1366
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
1367
|
+
"evidenceId": evidence_id,
|
|
1368
|
+
"kind": kind,
|
|
1369
|
+
"status": status,
|
|
1370
|
+
"productIdentity": product_identity,
|
|
1371
|
+
"passedGates": sorted(set(passed_gates)),
|
|
1372
|
+
"provenance": provenance,
|
|
1373
|
+
# 已接受/已推迟的风险必须随收据留痕,否则"放行"会读成"没有问题"。
|
|
1374
|
+
"residualRisks": residual_risks,
|
|
1375
|
+
"registeredAt": now_iso(),
|
|
1376
|
+
}
|
|
1377
|
+
ledger = _load_evidence_ledger(change_dir)
|
|
1378
|
+
existing = ledger["evidence"].get(evidence_id)
|
|
1379
|
+
if existing is not None:
|
|
1380
|
+
# residualRisks 是从 dispositions 派生的上下文,不属于证据自身的身份;
|
|
1381
|
+
# 把它算进比对会让"处置结论更新后重新注册"误报 ID 复用。
|
|
1382
|
+
derived = {"registeredAt", "residualRisks"}
|
|
1383
|
+
comparable = {
|
|
1384
|
+
key: value
|
|
1385
|
+
for key, value in record.items()
|
|
1386
|
+
if key not in derived
|
|
1387
|
+
}
|
|
1388
|
+
existing_comparable = {
|
|
1389
|
+
key: value
|
|
1390
|
+
for key, value in existing.items()
|
|
1391
|
+
if key not in derived
|
|
1392
|
+
} if isinstance(existing, dict) else {}
|
|
1393
|
+
if existing_comparable != comparable:
|
|
1394
|
+
raise ValueError(f"FIXBACK_EVIDENCE_ID_REUSED: {evidence_id}")
|
|
1395
|
+
return existing
|
|
1396
|
+
ledger["evidence"][evidence_id] = record
|
|
1397
|
+
_write_json(_evidence_ledger_path(change_dir), ledger)
|
|
1398
|
+
return record
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _evidence_record(
|
|
1402
|
+
change_dir: Path,
|
|
1403
|
+
raw_path: str,
|
|
1404
|
+
*,
|
|
1405
|
+
kind: str,
|
|
1406
|
+
statuses: set[str],
|
|
1407
|
+
product_identity: str | None = None,
|
|
1408
|
+
) -> dict[str, Any]:
|
|
1409
|
+
path = _resolve_evidence_path(change_dir, raw_path)
|
|
1410
|
+
try:
|
|
1411
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1412
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1413
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}: {exc}") from exc
|
|
1414
|
+
if not isinstance(value, dict):
|
|
1415
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
1416
|
+
status = str(value.get("status") or "").upper()
|
|
1417
|
+
evidence_product = str(value.get("productIdentity") or "").strip()
|
|
1418
|
+
evidence_id = str(value.get("evidenceId") or "").strip()
|
|
1419
|
+
if (
|
|
1420
|
+
value.get("schemaVersion") != 2
|
|
1421
|
+
or value.get("kind") != kind
|
|
1422
|
+
or status not in statuses
|
|
1423
|
+
or not evidence_product
|
|
1424
|
+
or not evidence_id
|
|
1425
|
+
):
|
|
1426
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
1427
|
+
passed_gates = value.get("passedGates")
|
|
1428
|
+
if not isinstance(passed_gates, list) or any(
|
|
1429
|
+
not isinstance(item, str) or not item.strip()
|
|
1430
|
+
for item in passed_gates
|
|
1431
|
+
):
|
|
1432
|
+
raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
|
|
1433
|
+
ledger_record = _load_evidence_ledger(change_dir)["evidence"].get(evidence_id)
|
|
1434
|
+
current_sha = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1435
|
+
if (
|
|
1436
|
+
not isinstance(ledger_record, dict)
|
|
1437
|
+
or ledger_record.get("path") != str(path)
|
|
1438
|
+
or ledger_record.get("sha256") != current_sha
|
|
1439
|
+
or ledger_record.get("kind") != kind
|
|
1440
|
+
or ledger_record.get("status") != status
|
|
1441
|
+
or ledger_record.get("productIdentity") != evidence_product
|
|
1442
|
+
or ledger_record.get("passedGates") != sorted(set(passed_gates))
|
|
1443
|
+
):
|
|
1444
|
+
raise ValueError(
|
|
1445
|
+
evidence_error_message("FIXBACK_EVIDENCE_UNREGISTERED", raw_path)
|
|
1446
|
+
)
|
|
1447
|
+
if product_identity is not None and evidence_product != product_identity:
|
|
1448
|
+
raise ValueError(
|
|
1449
|
+
"FIXBACK_EVIDENCE_IDENTITY_MISMATCH: "
|
|
1450
|
+
f"{raw_path}: expected={product_identity} actual={evidence_product}"
|
|
1451
|
+
)
|
|
1452
|
+
return {
|
|
1453
|
+
"path": str(path),
|
|
1454
|
+
"sha256": current_sha,
|
|
1455
|
+
"evidenceId": evidence_id,
|
|
1456
|
+
"kind": kind,
|
|
1457
|
+
"status": status,
|
|
1458
|
+
"productIdentity": evidence_product,
|
|
1459
|
+
"passedGates": sorted(set(passed_gates)),
|
|
1460
|
+
"provenance": ledger_record["provenance"],
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _changed_file_record(change_dir: Path, raw_path: str) -> dict[str, str]:
|
|
1465
|
+
root = _project_root(change_dir)
|
|
1466
|
+
raw = Path(raw_path)
|
|
1467
|
+
path = raw.resolve() if raw.is_absolute() else (root / raw).resolve()
|
|
1468
|
+
if not path.is_file():
|
|
1469
|
+
raise ValueError(f"FIXBACK_CHANGED_FILE_MISSING: {raw_path}")
|
|
1470
|
+
if not _is_within(path, [root]):
|
|
1471
|
+
raise ValueError(f"FIXBACK_CHANGED_FILE_OUTSIDE_PROJECT: {raw_path}")
|
|
1472
|
+
return {
|
|
1473
|
+
"path": str(path.relative_to(root)).replace("\\", "/"),
|
|
1474
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _evidence_digest_matches(record: Any) -> bool:
|
|
1479
|
+
if not isinstance(record, dict):
|
|
1480
|
+
return False
|
|
1481
|
+
try:
|
|
1482
|
+
path = Path(str(record["path"]))
|
|
1483
|
+
expected = str(record["sha256"])
|
|
1484
|
+
return path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == expected
|
|
1485
|
+
except (KeyError, OSError):
|
|
1486
|
+
return False
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def open_batch(
|
|
1490
|
+
change_dir: Path,
|
|
1491
|
+
*,
|
|
1492
|
+
batch_id: str,
|
|
1493
|
+
product_identity: str,
|
|
1494
|
+
root_cause: str,
|
|
1495
|
+
) -> dict[str, Any]:
|
|
1496
|
+
if not product_identity.strip():
|
|
1497
|
+
raise ValueError("FIXBACK_PRODUCT_IDENTITY_REQUIRED")
|
|
1498
|
+
if not root_cause.strip():
|
|
1499
|
+
raise ValueError("FIXBACK_ROOT_CAUSE_REQUIRED")
|
|
1500
|
+
open_batches = [
|
|
1501
|
+
item for item in _all_batches(change_dir)
|
|
1502
|
+
if item.get("status") == "OPEN"
|
|
1503
|
+
]
|
|
1504
|
+
if open_batches:
|
|
1505
|
+
raise ValueError(
|
|
1506
|
+
"FIXBACK_BATCH_ALREADY_OPEN: " + str(open_batches[0].get("batchId"))
|
|
1507
|
+
)
|
|
1508
|
+
path = _batch_path(change_dir, batch_id)
|
|
1509
|
+
if path.exists():
|
|
1510
|
+
raise ValueError(f"FIXBACK_BATCH_EXISTS: {batch_id}")
|
|
1511
|
+
opened_at = now_iso()
|
|
1512
|
+
batch = {
|
|
1513
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
1514
|
+
"batchId": batch_id,
|
|
1515
|
+
"status": "OPEN",
|
|
1516
|
+
"rootCause": root_cause,
|
|
1517
|
+
"openedAt": opened_at,
|
|
1518
|
+
"updatedAt": opened_at,
|
|
1519
|
+
"closedAt": None,
|
|
1520
|
+
"baseProductIdentity": product_identity,
|
|
1521
|
+
"finalProductIdentity": None,
|
|
1522
|
+
"issues": [],
|
|
1523
|
+
"changedFiles": [],
|
|
1524
|
+
"requiredVerifications": ["affected", "review"],
|
|
1525
|
+
"receipts": {
|
|
1526
|
+
"affected": None,
|
|
1527
|
+
"review": None,
|
|
1528
|
+
},
|
|
1529
|
+
"verificationRuns": {
|
|
1530
|
+
"affected": 0,
|
|
1531
|
+
"review": 0,
|
|
1532
|
+
},
|
|
1533
|
+
}
|
|
1534
|
+
_write_json(path, batch)
|
|
1535
|
+
return batch
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def add_issue(
|
|
1539
|
+
change_dir: Path,
|
|
1540
|
+
*,
|
|
1541
|
+
batch_id: str,
|
|
1542
|
+
issue_id: str,
|
|
1543
|
+
summary: str,
|
|
1544
|
+
risk_tags: list[str],
|
|
1545
|
+
) -> dict[str, Any]:
|
|
1546
|
+
batch = _load(change_dir, batch_id)
|
|
1547
|
+
if batch.get("status") != "OPEN":
|
|
1548
|
+
raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
|
|
1549
|
+
if any(item.get("issueId") == issue_id for item in batch["issues"]):
|
|
1550
|
+
raise ValueError(f"FIXBACK_ISSUE_EXISTS: {issue_id}")
|
|
1551
|
+
issue = {
|
|
1552
|
+
"issueId": issue_id,
|
|
1553
|
+
"summary": summary,
|
|
1554
|
+
"status": "OPEN",
|
|
1555
|
+
"riskTags": sorted(set(risk_tags)),
|
|
1556
|
+
"redEvidence": None,
|
|
1557
|
+
"greenEvidence": None,
|
|
1558
|
+
"changedFiles": [],
|
|
1559
|
+
"resolvedAt": None,
|
|
1560
|
+
}
|
|
1561
|
+
batch["issues"].append(issue)
|
|
1562
|
+
batch["requiredVerifications"] = _required_verifications(batch["issues"])
|
|
1563
|
+
batch["updatedAt"] = now_iso()
|
|
1564
|
+
_write_json(_batch_path(change_dir, batch_id), batch)
|
|
1565
|
+
return issue
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def resolve_issue(
|
|
1569
|
+
change_dir: Path,
|
|
1570
|
+
*,
|
|
1571
|
+
batch_id: str,
|
|
1572
|
+
issue_id: str,
|
|
1573
|
+
red_evidence: str,
|
|
1574
|
+
green_evidence: str,
|
|
1575
|
+
changed_files: list[str],
|
|
1576
|
+
) -> dict[str, Any]:
|
|
1577
|
+
if not red_evidence.strip() or not green_evidence.strip():
|
|
1578
|
+
raise ValueError(f"FIXBACK_RED_GREEN_REQUIRED: {issue_id}")
|
|
1579
|
+
batch = _load(change_dir, batch_id)
|
|
1580
|
+
if batch.get("status") != "OPEN":
|
|
1581
|
+
raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
|
|
1582
|
+
issue = next(
|
|
1583
|
+
(item for item in batch["issues"] if item.get("issueId") == issue_id),
|
|
1584
|
+
None,
|
|
1585
|
+
)
|
|
1586
|
+
if issue is None:
|
|
1587
|
+
raise ValueError(f"FIXBACK_ISSUE_NOT_FOUND: {issue_id}")
|
|
1588
|
+
red_record = _evidence_record(
|
|
1589
|
+
change_dir,
|
|
1590
|
+
red_evidence,
|
|
1591
|
+
kind="red",
|
|
1592
|
+
statuses={"FAIL"},
|
|
1593
|
+
product_identity=str(batch["baseProductIdentity"]),
|
|
1594
|
+
)
|
|
1595
|
+
green_record = _evidence_record(
|
|
1596
|
+
change_dir,
|
|
1597
|
+
green_evidence,
|
|
1598
|
+
kind="green",
|
|
1599
|
+
statuses={"PASS", "OK"},
|
|
1600
|
+
)
|
|
1601
|
+
if red_record["evidenceId"] == green_record["evidenceId"]:
|
|
1602
|
+
raise ValueError(f"FIXBACK_EVIDENCE_ID_REUSED: {issue_id}")
|
|
1603
|
+
changed_records = [
|
|
1604
|
+
_changed_file_record(change_dir, raw_path)
|
|
1605
|
+
for raw_path in sorted(set(changed_files))
|
|
1606
|
+
]
|
|
1607
|
+
resolved_at = now_iso()
|
|
1608
|
+
issue.update(
|
|
1609
|
+
{
|
|
1610
|
+
"status": "RESOLVED",
|
|
1611
|
+
"redEvidence": red_record,
|
|
1612
|
+
"greenEvidence": green_record,
|
|
1613
|
+
"changedFiles": changed_records,
|
|
1614
|
+
"resolvedAt": resolved_at,
|
|
1615
|
+
}
|
|
1616
|
+
)
|
|
1617
|
+
batch["changedFiles"] = sorted(
|
|
1618
|
+
{
|
|
1619
|
+
record["path"]
|
|
1620
|
+
for item in batch["issues"]
|
|
1621
|
+
for record in item.get("changedFiles", [])
|
|
1622
|
+
}
|
|
1623
|
+
)
|
|
1624
|
+
batch["updatedAt"] = resolved_at
|
|
1625
|
+
batch["invalidation"] = invalidate_affected_evidence(
|
|
1626
|
+
change_dir,
|
|
1627
|
+
changed_files=[record["path"] for record in changed_records],
|
|
1628
|
+
batch_id=batch_id,
|
|
1629
|
+
)
|
|
1630
|
+
_write_json(_batch_path(change_dir, batch_id), batch)
|
|
1631
|
+
return issue
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
def close_batch(
|
|
1635
|
+
change_dir: Path,
|
|
1636
|
+
*,
|
|
1637
|
+
batch_id: str,
|
|
1638
|
+
final_product_identity: str,
|
|
1639
|
+
affected_receipt: str,
|
|
1640
|
+
review_receipt: str,
|
|
1641
|
+
) -> dict[str, Any]:
|
|
1642
|
+
batch = _load(change_dir, batch_id)
|
|
1643
|
+
if batch.get("status") != "OPEN":
|
|
1644
|
+
raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
|
|
1645
|
+
unresolved = [
|
|
1646
|
+
str(item.get("issueId"))
|
|
1647
|
+
for item in batch["issues"]
|
|
1648
|
+
if item.get("status") != "RESOLVED"
|
|
1649
|
+
or not item.get("redEvidence")
|
|
1650
|
+
or not item.get("greenEvidence")
|
|
1651
|
+
]
|
|
1652
|
+
if unresolved:
|
|
1653
|
+
raise ValueError(
|
|
1654
|
+
"FIXBACK_ISSUES_UNRESOLVED: " + ",".join(unresolved)
|
|
1655
|
+
)
|
|
1656
|
+
if not affected_receipt.strip() or not review_receipt.strip():
|
|
1657
|
+
raise ValueError("FIXBACK_BATCH_RECEIPTS_REQUIRED")
|
|
1658
|
+
if not final_product_identity.strip():
|
|
1659
|
+
raise ValueError("FIXBACK_FINAL_IDENTITY_REQUIRED")
|
|
1660
|
+
for issue in batch["issues"]:
|
|
1661
|
+
green = issue.get("greenEvidence")
|
|
1662
|
+
if not isinstance(green, dict) or (
|
|
1663
|
+
green.get("productIdentity") != final_product_identity
|
|
1664
|
+
):
|
|
1665
|
+
raise ValueError(
|
|
1666
|
+
"FIXBACK_GREEN_IDENTITY_MISMATCH: "
|
|
1667
|
+
+ str(issue.get("issueId"))
|
|
1668
|
+
)
|
|
1669
|
+
affected_record = _evidence_record(
|
|
1670
|
+
change_dir,
|
|
1671
|
+
affected_receipt,
|
|
1672
|
+
kind="verification",
|
|
1673
|
+
statuses={"PASS", "OK"},
|
|
1674
|
+
product_identity=final_product_identity,
|
|
1675
|
+
)
|
|
1676
|
+
review_record = _evidence_record(
|
|
1677
|
+
change_dir,
|
|
1678
|
+
review_receipt,
|
|
1679
|
+
kind="review",
|
|
1680
|
+
statuses={"PASS", "OK"},
|
|
1681
|
+
product_identity=final_product_identity,
|
|
1682
|
+
)
|
|
1683
|
+
passed_gates = set(affected_record["passedGates"]) | set(
|
|
1684
|
+
review_record["passedGates"]
|
|
1685
|
+
)
|
|
1686
|
+
missing_gates = sorted(
|
|
1687
|
+
set(batch["requiredVerifications"]) - passed_gates
|
|
1688
|
+
)
|
|
1689
|
+
if missing_gates:
|
|
1690
|
+
raise ValueError(
|
|
1691
|
+
"FIXBACK_REQUIRED_GATES_MISSING: " + ",".join(missing_gates)
|
|
1692
|
+
)
|
|
1693
|
+
closed_at = now_iso()
|
|
1694
|
+
batch.update(
|
|
1695
|
+
{
|
|
1696
|
+
"status": "CLOSED",
|
|
1697
|
+
"closedAt": closed_at,
|
|
1698
|
+
"updatedAt": closed_at,
|
|
1699
|
+
"finalProductIdentity": final_product_identity,
|
|
1700
|
+
"receipts": {
|
|
1701
|
+
"affected": affected_record,
|
|
1702
|
+
"review": review_record,
|
|
1703
|
+
},
|
|
1704
|
+
"verificationRuns": {
|
|
1705
|
+
"affected": 1,
|
|
1706
|
+
"review": 1,
|
|
1707
|
+
},
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
_write_json(_batch_path(change_dir, batch_id), batch)
|
|
1711
|
+
return batch
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def batch_status(change_dir: Path, batch_id: str) -> dict[str, Any]:
|
|
1715
|
+
return _load(change_dir, batch_id)
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
def freeze_readiness(
|
|
1719
|
+
change_dir: Path,
|
|
1720
|
+
*,
|
|
1721
|
+
product_identity: str,
|
|
1722
|
+
) -> dict[str, Any]:
|
|
1723
|
+
batches = _all_batches(change_dir)
|
|
1724
|
+
opened = [item for item in batches if item.get("status") == "OPEN"]
|
|
1725
|
+
if opened:
|
|
1726
|
+
return {
|
|
1727
|
+
"ok": False,
|
|
1728
|
+
"code": "FIXBACK_BATCH_OPEN",
|
|
1729
|
+
"batchIds": [item.get("batchId") for item in opened],
|
|
1730
|
+
}
|
|
1731
|
+
closed = sorted(
|
|
1732
|
+
(
|
|
1733
|
+
item for item in batches
|
|
1734
|
+
if item.get("status") == "CLOSED"
|
|
1735
|
+
),
|
|
1736
|
+
key=lambda item: str(item.get("closedAt") or ""),
|
|
1737
|
+
)
|
|
1738
|
+
if closed and closed[-1].get("finalProductIdentity") != product_identity:
|
|
1739
|
+
return {
|
|
1740
|
+
"ok": False,
|
|
1741
|
+
"code": "FIXBACK_IDENTITY_CHANGED_AFTER_CLOSE",
|
|
1742
|
+
"batchId": closed[-1].get("batchId"),
|
|
1743
|
+
"expected": closed[-1].get("finalProductIdentity"),
|
|
1744
|
+
"actual": product_identity,
|
|
1745
|
+
}
|
|
1746
|
+
for batch in closed:
|
|
1747
|
+
records = [
|
|
1748
|
+
batch.get("receipts", {}).get("affected"),
|
|
1749
|
+
batch.get("receipts", {}).get("review"),
|
|
1750
|
+
]
|
|
1751
|
+
for issue in batch.get("issues", []):
|
|
1752
|
+
records.extend(
|
|
1753
|
+
[issue.get("redEvidence"), issue.get("greenEvidence")]
|
|
1754
|
+
)
|
|
1755
|
+
if any(not _evidence_digest_matches(record) for record in records):
|
|
1756
|
+
return {
|
|
1757
|
+
"ok": False,
|
|
1758
|
+
"code": "FIXBACK_EVIDENCE_CHANGED",
|
|
1759
|
+
"batchId": batch.get("batchId"),
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
"ok": True,
|
|
1763
|
+
"code": "FIXBACK_READY_FOR_FREEZE",
|
|
1764
|
+
"closedBatchCount": len(closed),
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _emit(value: dict[str, Any]) -> int:
|
|
1769
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
1770
|
+
return 0 if value.get("ok", True) else 1
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1774
|
+
parser = argparse.ArgumentParser(prog="harness_fixback.py")
|
|
1775
|
+
parser.add_argument("--json", action="store_true")
|
|
1776
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
1777
|
+
resume = sub.add_parser("resume")
|
|
1778
|
+
resume.add_argument("--json", action="store_true")
|
|
1779
|
+
resume.add_argument("--change-dir", required=True)
|
|
1780
|
+
resume.add_argument("--batch-id")
|
|
1781
|
+
resume.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1782
|
+
resume.add_argument("--root-cause")
|
|
1783
|
+
resume.add_argument("--run-id")
|
|
1784
|
+
resume.add_argument("--attempt", type=int)
|
|
1785
|
+
opened = sub.add_parser("open")
|
|
1786
|
+
opened.add_argument("--change-dir", required=True)
|
|
1787
|
+
opened.add_argument("--batch-id", required=True)
|
|
1788
|
+
opened.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1789
|
+
opened.add_argument("--root-cause", required=True)
|
|
1790
|
+
issue = sub.add_parser("add-issue")
|
|
1791
|
+
issue.add_argument("--change-dir", required=True)
|
|
1792
|
+
issue.add_argument("--batch-id", required=True)
|
|
1793
|
+
issue.add_argument("--issue-id", required=True)
|
|
1794
|
+
issue.add_argument("--summary", required=True)
|
|
1795
|
+
issue.add_argument("--risk-tag", action="append", default=[])
|
|
1796
|
+
resolve = sub.add_parser("resolve-issue")
|
|
1797
|
+
resolve.add_argument("--change-dir", required=True)
|
|
1798
|
+
resolve.add_argument("--batch-id", required=True)
|
|
1799
|
+
resolve.add_argument("--issue-id", required=True)
|
|
1800
|
+
resolve.add_argument("--red-evidence", required=True)
|
|
1801
|
+
resolve.add_argument("--green-evidence", required=True)
|
|
1802
|
+
resolve.add_argument("--changed-file", action="append", default=[])
|
|
1803
|
+
close = sub.add_parser("close")
|
|
1804
|
+
close.add_argument("--change-dir", required=True)
|
|
1805
|
+
close.add_argument("--batch-id", required=True)
|
|
1806
|
+
close.add_argument(
|
|
1807
|
+
"--final-product-identity",
|
|
1808
|
+
required=True,
|
|
1809
|
+
help=(
|
|
1810
|
+
"关批次时的产品身份,必须与每条 GREEN 证据的 productIdentity 相同。"
|
|
1811
|
+
"取 launch-review 回显的 resolvedProductIdentity,不要另行推导。"
|
|
1812
|
+
),
|
|
1813
|
+
)
|
|
1814
|
+
close.add_argument("--affected-receipt", required=True)
|
|
1815
|
+
close.add_argument("--review-receipt", required=True)
|
|
1816
|
+
status = sub.add_parser("status")
|
|
1817
|
+
status.add_argument("--change-dir", required=True)
|
|
1818
|
+
status.add_argument("--batch-id", required=True)
|
|
1819
|
+
freeze = sub.add_parser("freeze-readiness")
|
|
1820
|
+
freeze.add_argument("--change-dir", required=True)
|
|
1821
|
+
freeze.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1822
|
+
register = sub.add_parser("register-evidence")
|
|
1823
|
+
register.add_argument("--change-dir", required=True)
|
|
1824
|
+
register.add_argument("--evidence", required=True)
|
|
1825
|
+
template = sub.add_parser(
|
|
1826
|
+
"evidence-template",
|
|
1827
|
+
help="从托管会话/审查 sidecar 生成可直接注册的证据 JSON",
|
|
1828
|
+
)
|
|
1829
|
+
template.add_argument("--change-dir", required=True)
|
|
1830
|
+
template.add_argument(
|
|
1831
|
+
"--kind",
|
|
1832
|
+
required=True,
|
|
1833
|
+
choices=list(_EVIDENCE_KINDS),
|
|
1834
|
+
help="red=修复前失败会话;green=修复后成功会话;verification=受影响链;review=审查收据",
|
|
1835
|
+
)
|
|
1836
|
+
template.add_argument(
|
|
1837
|
+
"--session",
|
|
1838
|
+
help="run-start 产出的 sessionId(kind=red/green/verification 必填)",
|
|
1839
|
+
)
|
|
1840
|
+
template.add_argument("--out", help="输出路径,默认 runtime/fixback-<evidenceId>.json")
|
|
1841
|
+
template.add_argument("--evidence-id")
|
|
1842
|
+
template.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1843
|
+
template.add_argument("--passed-gate", action="append", default=[])
|
|
1844
|
+
template.add_argument("--json", action="store_true")
|
|
1845
|
+
review_plan = sub.add_parser("review-plan")
|
|
1846
|
+
review_plan.add_argument("--change-dir", required=True)
|
|
1847
|
+
review_plan.add_argument("--json", action="store_true")
|
|
1848
|
+
review_start = sub.add_parser("start-review")
|
|
1849
|
+
review_start.add_argument("--change-dir", required=True)
|
|
1850
|
+
review_start.add_argument("--batch-id", required=True)
|
|
1851
|
+
review_start.add_argument("--product-identity", required=True, help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1852
|
+
review_start.add_argument("--run-id", required=True)
|
|
1853
|
+
review_start.add_argument("--attempt", type=int, required=True)
|
|
1854
|
+
review_start.add_argument("--json", action="store_true")
|
|
1855
|
+
launch_review = sub.add_parser("launch-review")
|
|
1856
|
+
launch_review.add_argument("--project", required=True)
|
|
1857
|
+
launch_review.add_argument("--change", required=True)
|
|
1858
|
+
launch_review.add_argument("--change-dir", required=True)
|
|
1859
|
+
launch_review.add_argument("--executor", required=True)
|
|
1860
|
+
launch_review.add_argument("--skills-root", required=True)
|
|
1861
|
+
launch_review.add_argument("--product-identity", help="产品身份指纹,用于把 RED/GREEN 证据绑定到同一产品状态。规范取值是当前 git HEAD;省略则自动推导并在返回体的 resolvedProductIdentity 里回显,close 时原样引用即可。")
|
|
1862
|
+
launch_review.add_argument("--run-id")
|
|
1863
|
+
launch_review.add_argument("--attempt", type=int, default=2)
|
|
1864
|
+
launch_review.add_argument("--batch-id")
|
|
1865
|
+
launch_review.add_argument("--task", type=int)
|
|
1866
|
+
launch_review.add_argument("--executor-tool")
|
|
1867
|
+
launch_review.add_argument("--executor-agent")
|
|
1868
|
+
launch_review.add_argument("--executor-model")
|
|
1869
|
+
launch_review.add_argument("--json", action="store_true")
|
|
1870
|
+
return parser
|
|
1871
|
+
|
|
1872
|
+
|
|
1873
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1874
|
+
args = build_parser().parse_args(argv)
|
|
1875
|
+
change_dir = Path(args.change_dir)
|
|
1876
|
+
try:
|
|
1877
|
+
if args.command == "review-plan":
|
|
1878
|
+
value = review_fixback_plan(change_dir)
|
|
1879
|
+
elif args.command == "start-review":
|
|
1880
|
+
value = open_review_batch(
|
|
1881
|
+
change_dir,
|
|
1882
|
+
plan=review_fixback_plan(change_dir),
|
|
1883
|
+
batch_id=args.batch_id,
|
|
1884
|
+
product_identity=args.product_identity,
|
|
1885
|
+
run_id=args.run_id,
|
|
1886
|
+
attempt=args.attempt,
|
|
1887
|
+
)
|
|
1888
|
+
elif args.command == "launch-review":
|
|
1889
|
+
value = launch_review_fixback(
|
|
1890
|
+
project=Path(args.project),
|
|
1891
|
+
change=args.change,
|
|
1892
|
+
change_dir=change_dir,
|
|
1893
|
+
executor=args.executor,
|
|
1894
|
+
skills_root=Path(args.skills_root),
|
|
1895
|
+
# 省略时推导当前 HEAD:这个值全仓文档零命中,硬性必填只会
|
|
1896
|
+
# 逼调用方去 grep 源码猜(2026-08-19 那轮猜了 8 次)。
|
|
1897
|
+
product_identity=(
|
|
1898
|
+
args.product_identity
|
|
1899
|
+
or resolve_product_identity(change_dir)
|
|
1900
|
+
),
|
|
1901
|
+
run_id=args.run_id,
|
|
1902
|
+
attempt=args.attempt,
|
|
1903
|
+
batch_id=args.batch_id,
|
|
1904
|
+
task=args.task,
|
|
1905
|
+
executor_tool=args.executor_tool,
|
|
1906
|
+
executor_agent=args.executor_agent,
|
|
1907
|
+
executor_model=args.executor_model,
|
|
1908
|
+
)
|
|
1909
|
+
elif args.command == "resume":
|
|
1910
|
+
value = resume_batch(
|
|
1911
|
+
change_dir,
|
|
1912
|
+
batch_id=args.batch_id,
|
|
1913
|
+
product_identity=args.product_identity,
|
|
1914
|
+
root_cause=args.root_cause,
|
|
1915
|
+
run_id=args.run_id,
|
|
1916
|
+
attempt=args.attempt,
|
|
1917
|
+
)
|
|
1918
|
+
elif args.command == "open":
|
|
1919
|
+
value = open_batch(
|
|
1920
|
+
change_dir,
|
|
1921
|
+
batch_id=args.batch_id,
|
|
1922
|
+
product_identity=args.product_identity,
|
|
1923
|
+
root_cause=args.root_cause,
|
|
1924
|
+
)
|
|
1925
|
+
elif args.command == "add-issue":
|
|
1926
|
+
value = add_issue(
|
|
1927
|
+
change_dir,
|
|
1928
|
+
batch_id=args.batch_id,
|
|
1929
|
+
issue_id=args.issue_id,
|
|
1930
|
+
summary=args.summary,
|
|
1931
|
+
risk_tags=args.risk_tag,
|
|
1932
|
+
)
|
|
1933
|
+
elif args.command == "resolve-issue":
|
|
1934
|
+
value = resolve_issue(
|
|
1935
|
+
change_dir,
|
|
1936
|
+
batch_id=args.batch_id,
|
|
1937
|
+
issue_id=args.issue_id,
|
|
1938
|
+
red_evidence=args.red_evidence,
|
|
1939
|
+
green_evidence=args.green_evidence,
|
|
1940
|
+
changed_files=args.changed_file,
|
|
1941
|
+
)
|
|
1942
|
+
elif args.command == "close":
|
|
1943
|
+
value = close_batch(
|
|
1944
|
+
change_dir,
|
|
1945
|
+
batch_id=args.batch_id,
|
|
1946
|
+
final_product_identity=args.final_product_identity,
|
|
1947
|
+
affected_receipt=args.affected_receipt,
|
|
1948
|
+
review_receipt=args.review_receipt,
|
|
1949
|
+
)
|
|
1950
|
+
elif args.command == "status":
|
|
1951
|
+
value = batch_status(change_dir, args.batch_id)
|
|
1952
|
+
elif args.command == "register-evidence":
|
|
1953
|
+
value = register_evidence(change_dir, args.evidence)
|
|
1954
|
+
elif args.command == "evidence-template":
|
|
1955
|
+
value = evidence_template(
|
|
1956
|
+
change_dir,
|
|
1957
|
+
kind=args.kind,
|
|
1958
|
+
session_id=args.session,
|
|
1959
|
+
out=args.out,
|
|
1960
|
+
evidence_id=args.evidence_id,
|
|
1961
|
+
product_identity=args.product_identity,
|
|
1962
|
+
passed_gates=args.passed_gate or None,
|
|
1963
|
+
)
|
|
1964
|
+
else:
|
|
1965
|
+
value = freeze_readiness(
|
|
1966
|
+
change_dir,
|
|
1967
|
+
product_identity=args.product_identity,
|
|
1968
|
+
)
|
|
1969
|
+
except (OSError, ValueError) as exc:
|
|
1970
|
+
return _emit({"ok": False, "code": str(exc).split(":", 1)[0], "error": str(exc)})
|
|
1971
|
+
return _emit(value)
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
if __name__ == "__main__":
|
|
1975
|
+
raise SystemExit(main())
|