@hunter-harness/workflow-harness 0.3.1 → 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-archive/reference.md +30 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +538 -4
- 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-archive/reference.md +30 -1
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +1 -0
- 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 +146 -8
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
- package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +538 -4
- 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 +19 -19
- package/harness/manifests/general/codebuddy.json +19 -19
- package/harness/manifests/general/codex.json +19 -19
- package/harness/manifests/general/cursor.json +19 -19
- package/harness/manifests/general/pi.json +475 -0
- package/harness/manifests/java/claude-code.json +21 -21
- package/harness/manifests/java/codebuddy.json +21 -21
- package/harness/manifests/java/codex.json +21 -21
- package/harness/manifests/java/cursor.json +21 -21
- package/harness/manifests/java/pi.json +499 -0
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""已发布计划的只读验收与 v2 清单解包(read-side only)。
|
|
3
|
+
|
|
4
|
+
legacy staging/finalize/republish 写侧已于 2026-08 移除(roadmap 14;采纳度
|
|
5
|
+
gating 由用户决策解除)。新 change 的发布走 `hunter-harness plan evidence-pack`
|
|
6
|
+
+ `plan finalize`(v2);计划修订走 evidence-pack 重跑(expected_baseline
|
|
7
|
+
= present + 新 attempt)。本模块保留:
|
|
8
|
+
- `verify_plan`:已发布计划的只读验收(legacy receipt 与 v2 事务/journal 两种
|
|
9
|
+
证据形状),供 `harness_gate.validate_plan_handoff` 与 `verify` 子命令使用;
|
|
10
|
+
- v2 manifest / checkpoints 解包与 plan/scenario 表解析:供 gate 与 ledger
|
|
11
|
+
在只读侧消费(哈希校验后)。
|
|
12
|
+
|
|
13
|
+
历史 change 的 legacy 产物必须保持可读;删除的是写入路径,不是读路径。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import stat
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path, PurePosixPath
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
29
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
31
|
+
|
|
32
|
+
import harness_events # noqa: E402
|
|
33
|
+
|
|
34
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
36
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
37
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
SCHEMA_VERSION = 1
|
|
40
|
+
_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
|
41
|
+
_BACKTICK = re.compile(r"`([^`\r\n]+)`")
|
|
42
|
+
_CODE_PATH_SUFFIXES = {
|
|
43
|
+
".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp", ".html",
|
|
44
|
+
".java", ".js", ".jsx", ".kt", ".kts", ".mjs", ".php", ".py", ".rb",
|
|
45
|
+
".rs", ".scss", ".sh", ".svelte", ".swift", ".ts", ".tsx", ".vue",
|
|
46
|
+
}
|
|
47
|
+
_BUILD_FILE_NAMES = {
|
|
48
|
+
"build.gradle", "build.gradle.kts", "cargo.toml", "composer.json", "go.mod",
|
|
49
|
+
"gradle.properties", "package-lock.json", "package.json", "pnpm-lock.yaml",
|
|
50
|
+
"pom.xml", "pyproject.toml", "requirements.txt", "settings.gradle",
|
|
51
|
+
"settings.gradle.kts", "tsconfig.json", "yarn.lock",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# C8: valid ownerPhase values (lifecycle phases that can own tasks).
|
|
55
|
+
VALID_OWNER_PHASES = {"plan", "execute", "review", "submit"}
|
|
56
|
+
SLICE_CANDIDATE_TYPES = {
|
|
57
|
+
"independent-release-candidate",
|
|
58
|
+
"child-of-aggregate",
|
|
59
|
+
"fixback-of",
|
|
60
|
+
"evidence-only",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# C9: map scenario priority to required evidence kind.
|
|
64
|
+
PRIORITY_EVIDENCE_KIND = {
|
|
65
|
+
"P0": "ledger",
|
|
66
|
+
"P1": "ledger",
|
|
67
|
+
"P2": "advisory",
|
|
68
|
+
}
|
|
69
|
+
_FILE_ATTRIBUTE_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _result_error(code: str, message: str) -> dict[str, Any]:
|
|
73
|
+
return {"ok": False, "code": code, "error": message}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PlanParseError(ValueError):
|
|
77
|
+
def __init__(self, code: str, message: str) -> None:
|
|
78
|
+
super().__init__(message)
|
|
79
|
+
self.code = code
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalize_header(value: str) -> str:
|
|
83
|
+
return re.sub(r"[\s_-]+", "", value).lower()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _column_index(
|
|
87
|
+
headers: list[str],
|
|
88
|
+
*aliases: str,
|
|
89
|
+
) -> int | None:
|
|
90
|
+
normalized = {
|
|
91
|
+
_normalize_header(header): index for index, header in enumerate(headers)
|
|
92
|
+
}
|
|
93
|
+
for alias in aliases:
|
|
94
|
+
index = normalized.get(_normalize_header(alias))
|
|
95
|
+
if index is not None:
|
|
96
|
+
return index
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _table_cells(line: str) -> list[str]:
|
|
101
|
+
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, Any]]:
|
|
105
|
+
"""C9: parse test-scenarios.md tables, extracting scenario rows.
|
|
106
|
+
|
|
107
|
+
Returns a list of dicts with keys: id / priority / scenario / verification /
|
|
108
|
+
ownerPhase / requiredEvidenceKind (last one derived from priority).
|
|
109
|
+
"""
|
|
110
|
+
text = Path(scenarios_path).read_text(encoding="utf-8-sig")
|
|
111
|
+
lines = text.splitlines()
|
|
112
|
+
scenarios: list[dict[str, Any]] = []
|
|
113
|
+
i = 0
|
|
114
|
+
while i < len(lines):
|
|
115
|
+
line = lines[i].strip()
|
|
116
|
+
if line.startswith("|"):
|
|
117
|
+
headers = _table_cells(line)
|
|
118
|
+
id_index = _column_index(headers, "ID", "#", "编号", "场景 ID")
|
|
119
|
+
scenario_index = _column_index(
|
|
120
|
+
headers,
|
|
121
|
+
"场景",
|
|
122
|
+
"场景描述",
|
|
123
|
+
"scenario",
|
|
124
|
+
"description",
|
|
125
|
+
)
|
|
126
|
+
if id_index is None or scenario_index is None:
|
|
127
|
+
i += 1
|
|
128
|
+
continue
|
|
129
|
+
priority_index = _column_index(headers, "优先级", "priority")
|
|
130
|
+
category_index = _column_index(headers, "分类", "category")
|
|
131
|
+
verification_index = _column_index(
|
|
132
|
+
headers,
|
|
133
|
+
"验证方式",
|
|
134
|
+
"verification",
|
|
135
|
+
"可复用证据",
|
|
136
|
+
)
|
|
137
|
+
owner_phase_index = _column_index(
|
|
138
|
+
headers,
|
|
139
|
+
"owner phase",
|
|
140
|
+
"ownerPhase",
|
|
141
|
+
"负责阶段",
|
|
142
|
+
)
|
|
143
|
+
execution_tier_index = _column_index(
|
|
144
|
+
headers,
|
|
145
|
+
"执行层级",
|
|
146
|
+
"execution tier",
|
|
147
|
+
"tier",
|
|
148
|
+
)
|
|
149
|
+
expected_index = _column_index(headers, "预期", "expected")
|
|
150
|
+
executable_id_index = _column_index(
|
|
151
|
+
headers,
|
|
152
|
+
"executable test ID",
|
|
153
|
+
"executableTestId",
|
|
154
|
+
"可执行测试 ID",
|
|
155
|
+
"测试 ID",
|
|
156
|
+
)
|
|
157
|
+
test_file_index = _column_index(
|
|
158
|
+
headers,
|
|
159
|
+
"test file",
|
|
160
|
+
"testFile",
|
|
161
|
+
"测试文件",
|
|
162
|
+
"spec path",
|
|
163
|
+
)
|
|
164
|
+
test_title_index = _column_index(
|
|
165
|
+
headers,
|
|
166
|
+
"test title",
|
|
167
|
+
"testTitle",
|
|
168
|
+
"测试标题",
|
|
169
|
+
"用例标题",
|
|
170
|
+
)
|
|
171
|
+
executable_mapping_declared = any(
|
|
172
|
+
index is not None
|
|
173
|
+
for index in (
|
|
174
|
+
executable_id_index,
|
|
175
|
+
test_file_index,
|
|
176
|
+
test_title_index,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
row_index = i + 2
|
|
181
|
+
while row_index < len(lines):
|
|
182
|
+
row = lines[row_index].strip()
|
|
183
|
+
if not row.startswith("|"):
|
|
184
|
+
break
|
|
185
|
+
cells = _table_cells(row)
|
|
186
|
+
if max(id_index, scenario_index) >= len(cells):
|
|
187
|
+
raise PlanParseError(
|
|
188
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
189
|
+
f"{scenarios_path.name}: line {row_index + 1} is missing scenario ID or description",
|
|
190
|
+
)
|
|
191
|
+
complete_row = len(cells) == len(headers)
|
|
192
|
+
scenario_id = cells[id_index].strip()
|
|
193
|
+
if not scenario_id or set(scenario_id) <= {"-", ":"}:
|
|
194
|
+
raise PlanParseError(
|
|
195
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
196
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario ID",
|
|
197
|
+
)
|
|
198
|
+
scenario_text = cells[scenario_index].strip()
|
|
199
|
+
if not scenario_text or set(scenario_text) <= {"-", ":"}:
|
|
200
|
+
raise PlanParseError(
|
|
201
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
202
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario description",
|
|
203
|
+
)
|
|
204
|
+
priority = (
|
|
205
|
+
cells[priority_index].strip().upper()
|
|
206
|
+
if priority_index is not None
|
|
207
|
+
and priority_index < len(cells)
|
|
208
|
+
and cells[priority_index].strip()
|
|
209
|
+
else "P1"
|
|
210
|
+
)
|
|
211
|
+
scenario = {
|
|
212
|
+
"id": scenario_id,
|
|
213
|
+
"priority": priority,
|
|
214
|
+
"scenario": scenario_text,
|
|
215
|
+
"ownerPhase": (
|
|
216
|
+
cells[owner_phase_index].strip()
|
|
217
|
+
if owner_phase_index is not None
|
|
218
|
+
and complete_row
|
|
219
|
+
and owner_phase_index < len(cells)
|
|
220
|
+
and cells[owner_phase_index].strip()
|
|
221
|
+
else "execute"
|
|
222
|
+
),
|
|
223
|
+
"requiredEvidenceKind": PRIORITY_EVIDENCE_KIND.get(
|
|
224
|
+
priority,
|
|
225
|
+
"advisory",
|
|
226
|
+
),
|
|
227
|
+
"executableMappingDeclared": executable_mapping_declared,
|
|
228
|
+
}
|
|
229
|
+
if category_index is not None and category_index < len(cells):
|
|
230
|
+
if cells[category_index].strip():
|
|
231
|
+
scenario["category"] = cells[category_index].strip()
|
|
232
|
+
for key, index in (
|
|
233
|
+
("verification", verification_index),
|
|
234
|
+
("executionTier", execution_tier_index),
|
|
235
|
+
("expected", expected_index),
|
|
236
|
+
("executableTestId", executable_id_index),
|
|
237
|
+
("testFile", test_file_index),
|
|
238
|
+
("testTitle", test_title_index),
|
|
239
|
+
):
|
|
240
|
+
if (
|
|
241
|
+
complete_row
|
|
242
|
+
and index is not None
|
|
243
|
+
and index < len(cells)
|
|
244
|
+
and cells[index].strip()
|
|
245
|
+
):
|
|
246
|
+
scenario[key] = cells[index].strip()
|
|
247
|
+
scenarios.append(scenario)
|
|
248
|
+
row_index += 1
|
|
249
|
+
i = row_index
|
|
250
|
+
continue
|
|
251
|
+
i += 1
|
|
252
|
+
return scenarios
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def scenario_manifest_schema_version(scenarios: list[dict[str, Any]]) -> int:
|
|
256
|
+
required = [
|
|
257
|
+
item
|
|
258
|
+
for item in scenarios
|
|
259
|
+
if str(item.get("requiredEvidenceKind") or "") == "ledger"
|
|
260
|
+
]
|
|
261
|
+
if required and all(
|
|
262
|
+
all(
|
|
263
|
+
str(item.get(field) or "").strip()
|
|
264
|
+
for field in ("executableTestId", "testFile", "testTitle")
|
|
265
|
+
)
|
|
266
|
+
for item in required
|
|
267
|
+
):
|
|
268
|
+
return 2
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# v2 场景键 → 门禁与 ledger 消费的 legacy 键。可执行三元是可选的:缺了只是把
|
|
273
|
+
# manifest 降到 schemaVersion 1(绑不上结构化执行收据),不算缺口。
|
|
274
|
+
_V2_SCENARIO_FIELD_MAP = (
|
|
275
|
+
("scenario_id", "id"),
|
|
276
|
+
("priority", "priority"),
|
|
277
|
+
("required_evidence_kind", "requiredEvidenceKind"),
|
|
278
|
+
("owner_phase", "ownerPhase"),
|
|
279
|
+
)
|
|
280
|
+
_V2_SCENARIO_OPTIONAL_FIELD_MAP = (
|
|
281
|
+
("executable_test_id", "executableTestId"),
|
|
282
|
+
("test_file", "testFile"),
|
|
283
|
+
("test_title", "testTitle"),
|
|
284
|
+
)
|
|
285
|
+
V2_MANIFEST_UNSUPPORTED = "SCENARIO_MANIFEST_V2_UNSUPPORTED"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def is_v2_scenario_manifest(manifest: Any) -> bool:
|
|
289
|
+
"""v2 plan finalize 派生的 scenario_manifest artifact 包装体。
|
|
290
|
+
|
|
291
|
+
它没有顶层 schemaVersion,场景字段全在 content.scenarios 下且改了名。
|
|
292
|
+
"""
|
|
293
|
+
return (
|
|
294
|
+
isinstance(manifest, dict)
|
|
295
|
+
and manifest.get("artifact_type") == "scenario_manifest"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def unpack_v2_scenario_manifest(manifest: Any) -> dict[str, Any] | None:
|
|
300
|
+
"""v2 artifact 包装体 → 消费端的 legacy 形状;字段不齐时 fail-closed。
|
|
301
|
+
|
|
302
|
+
返回 None 表示"这不是 v2 包装体",调用方按 legacy 原样处理。否则返回
|
|
303
|
+
``{"ok": True, "manifest": {...}}``(已是 legacy 形状,后续判定逻辑对
|
|
304
|
+
v2 与 legacy 完全一致)或 ``{"ok": False, "code": V2_MANIFEST_UNSUPPORTED}``。
|
|
305
|
+
|
|
306
|
+
这个函数住在 finalizer 里,是因为 legacy manifest 的 schema 本来就由本模块
|
|
307
|
+
定义(``scenario_manifest_schema_version`` / ``PRIORITY_EVIDENCE_KIND``)。
|
|
308
|
+
门禁与 ledger 都 import 它,两边不会各推一套解包规则。
|
|
309
|
+
|
|
310
|
+
为什么必须逐场景校验、不能取键的并集:并集只要有**一条**场景带了
|
|
311
|
+
priority 就算"present",其余缺 priority 的场景会静默落进非必需集,
|
|
312
|
+
``required_ids`` 随之缩水——那正是把证据门禁悄悄关掉的老路子。
|
|
313
|
+
"""
|
|
314
|
+
if not is_v2_scenario_manifest(manifest):
|
|
315
|
+
return None
|
|
316
|
+
content = manifest.get("content")
|
|
317
|
+
raw = content.get("scenarios") if isinstance(content, dict) else None
|
|
318
|
+
if not isinstance(raw, list):
|
|
319
|
+
return {
|
|
320
|
+
"ok": False,
|
|
321
|
+
"code": V2_MANIFEST_UNSUPPORTED,
|
|
322
|
+
"message": "v2 scenario-manifest 的 content.scenarios 必须是数组",
|
|
323
|
+
"missingFields": [legacy for _, legacy in _V2_SCENARIO_FIELD_MAP],
|
|
324
|
+
"artifactType": "scenario_manifest",
|
|
325
|
+
}
|
|
326
|
+
scenarios: list[dict[str, Any]] = []
|
|
327
|
+
missing: set[str] = set()
|
|
328
|
+
for item in raw:
|
|
329
|
+
if not isinstance(item, dict):
|
|
330
|
+
missing.update(legacy for _, legacy in _V2_SCENARIO_FIELD_MAP)
|
|
331
|
+
continue
|
|
332
|
+
mapped: dict[str, Any] = {}
|
|
333
|
+
for source, legacy in _V2_SCENARIO_FIELD_MAP:
|
|
334
|
+
value = item.get(source)
|
|
335
|
+
if not str(value or "").strip():
|
|
336
|
+
missing.add(legacy)
|
|
337
|
+
continue
|
|
338
|
+
mapped[legacy] = value
|
|
339
|
+
for source, legacy in _V2_SCENARIO_OPTIONAL_FIELD_MAP:
|
|
340
|
+
value = item.get(source)
|
|
341
|
+
if str(value or "").strip():
|
|
342
|
+
mapped[legacy] = value
|
|
343
|
+
scenarios.append(mapped)
|
|
344
|
+
if missing:
|
|
345
|
+
return {
|
|
346
|
+
"ok": False,
|
|
347
|
+
"code": V2_MANIFEST_UNSUPPORTED,
|
|
348
|
+
"message": (
|
|
349
|
+
"meta/scenario-manifest.json 是 v2 plan artifact 包装体,"
|
|
350
|
+
"部分场景缺少消费端需要的字段;请在规划阶段补齐后重新发布,"
|
|
351
|
+
"不要手改派生产物"
|
|
352
|
+
),
|
|
353
|
+
"missingFields": sorted(missing),
|
|
354
|
+
"artifactType": "scenario_manifest",
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
"ok": True,
|
|
358
|
+
"manifest": {
|
|
359
|
+
# 与 legacy 同一条规则判版本,不另立一套。
|
|
360
|
+
"schemaVersion": scenario_manifest_schema_version(scenarios),
|
|
361
|
+
"scenarios": scenarios,
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def unpack_v2_implementation_checkpoints(document: Any) -> dict[str, Any] | None:
|
|
367
|
+
"""v2 的 implementation_checkpoints artifact 包装体 → 门禁消费的形状。
|
|
368
|
+
|
|
369
|
+
返回 None 表示"这不是 v2 包装体",调用方按原样处理。
|
|
370
|
+
|
|
371
|
+
v2 的 content 是 ``{tasks, foundation_gate}``,而 ``checkpoint_status`` 找的是
|
|
372
|
+
``checkpoints: [{id, status}]``——找不到就返回 "missing",``foundation_gate_blocks``
|
|
373
|
+
随即放行。也就是说 foundation-gate 对**所有 v2 计划**是静默关闭的。信息本身是够
|
|
374
|
+
的(``foundation_gate`` 就是那个状态),只是形状不同,所以这里解包而不是改文件名
|
|
375
|
+
——与 scenario-manifest 同一模式。
|
|
376
|
+
"""
|
|
377
|
+
if (
|
|
378
|
+
not isinstance(document, dict)
|
|
379
|
+
or document.get("artifact_type") != "implementation_checkpoints"
|
|
380
|
+
):
|
|
381
|
+
return None
|
|
382
|
+
content = document.get("content")
|
|
383
|
+
content = content if isinstance(content, dict) else {}
|
|
384
|
+
status = str(content.get("foundation_gate") or "").strip() or "pending"
|
|
385
|
+
tasks = content.get("tasks")
|
|
386
|
+
return {
|
|
387
|
+
"schemaVersion": 1,
|
|
388
|
+
"checkpoints": [{"id": "foundation-gate", "status": status, "blocking": True}],
|
|
389
|
+
"tasks": tasks if isinstance(tasks, list) else [],
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
394
|
+
"""C8: parse plan.md task table rows, extracting optional ownerPhase/implementationDoneWhen/verificationPhase columns.
|
|
395
|
+
|
|
396
|
+
Returns a list of dicts with keys: # / 簇 / 任务 / ownerPhase / implementationDoneWhen /
|
|
397
|
+
verificationPhase / requiresExplicitAuthority (last four optional, only present when
|
|
398
|
+
the column header includes them).
|
|
399
|
+
"""
|
|
400
|
+
text = Path(plan_path).read_text(encoding="utf-8-sig")
|
|
401
|
+
lines = text.splitlines()
|
|
402
|
+
tasks: list[dict[str, str]] = []
|
|
403
|
+
i = 0
|
|
404
|
+
while i < len(lines):
|
|
405
|
+
line = lines[i].strip()
|
|
406
|
+
if not line.startswith("|"):
|
|
407
|
+
i += 1
|
|
408
|
+
continue
|
|
409
|
+
headers = _table_cells(line)
|
|
410
|
+
number_index = _column_index(headers, "#", "任务编号", "task ID")
|
|
411
|
+
task_index = _column_index(headers, "任务", "task")
|
|
412
|
+
if number_index is None or task_index is None:
|
|
413
|
+
i += 1
|
|
414
|
+
continue
|
|
415
|
+
cluster_index = _column_index(headers, "簇", "cluster")
|
|
416
|
+
optional_indices = {
|
|
417
|
+
name: _column_index(headers, name)
|
|
418
|
+
for name in (
|
|
419
|
+
"ownerPhase",
|
|
420
|
+
"implementationDoneWhen",
|
|
421
|
+
"verificationPhase",
|
|
422
|
+
"requiresExplicitAuthority",
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
row_index = i + 2
|
|
426
|
+
while row_index < len(lines):
|
|
427
|
+
row = lines[row_index].strip()
|
|
428
|
+
if not row.startswith("|"):
|
|
429
|
+
break
|
|
430
|
+
cells = _table_cells(row)
|
|
431
|
+
if max(number_index, task_index) >= len(cells):
|
|
432
|
+
raise PlanParseError(
|
|
433
|
+
"PLAN_TASK_ROW_INVALID",
|
|
434
|
+
f"{plan_path.name}: line {row_index + 1} is missing task ID or description",
|
|
435
|
+
)
|
|
436
|
+
complete_row = len(cells) == len(headers)
|
|
437
|
+
number = cells[number_index].strip()
|
|
438
|
+
task_text = cells[task_index].strip()
|
|
439
|
+
if (
|
|
440
|
+
not number
|
|
441
|
+
or not task_text
|
|
442
|
+
or set(number) <= {"-", ":"}
|
|
443
|
+
or set(task_text) <= {"-", ":"}
|
|
444
|
+
):
|
|
445
|
+
raise PlanParseError(
|
|
446
|
+
"PLAN_TASK_ROW_INVALID",
|
|
447
|
+
f"{plan_path.name}: line {row_index + 1} has an empty task ID or description",
|
|
448
|
+
)
|
|
449
|
+
task: dict[str, str] = {"num": number, "task": task_text}
|
|
450
|
+
if cluster_index is not None and cells[cluster_index].strip():
|
|
451
|
+
task["cluster"] = cells[cluster_index].strip()
|
|
452
|
+
for name, index in optional_indices.items():
|
|
453
|
+
if (
|
|
454
|
+
complete_row
|
|
455
|
+
and index is not None
|
|
456
|
+
and index < len(cells)
|
|
457
|
+
and cells[index].strip()
|
|
458
|
+
):
|
|
459
|
+
task[name] = cells[index].strip()
|
|
460
|
+
tasks.append(task)
|
|
461
|
+
row_index += 1
|
|
462
|
+
i = row_index
|
|
463
|
+
return tasks
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _frontmatter(text: str) -> dict[str, str] | None:
|
|
469
|
+
lines = text.splitlines()
|
|
470
|
+
if not lines or lines[0].strip() != "---":
|
|
471
|
+
return None
|
|
472
|
+
data: dict[str, str] = {}
|
|
473
|
+
for line in lines[1:]:
|
|
474
|
+
if line.strip() == "---":
|
|
475
|
+
return data
|
|
476
|
+
if ":" not in line:
|
|
477
|
+
return None
|
|
478
|
+
key, value = line.split(":", 1)
|
|
479
|
+
data[key.strip()] = value.strip()
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _required_artifact_names(change_name: str) -> set[str]:
|
|
484
|
+
return {
|
|
485
|
+
f"spec/{change_name}-design.md",
|
|
486
|
+
f"plans/{change_name}-plan.md",
|
|
487
|
+
f"plans/{change_name}-implementation-detail.md",
|
|
488
|
+
f"plans/{change_name}-test-scenarios.md",
|
|
489
|
+
"meta/gate-policy.json",
|
|
490
|
+
"meta/worktree.json",
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _is_link_or_reparse(path: Path) -> bool:
|
|
495
|
+
"""Detect symlinks and Windows reparse points on every supported Python."""
|
|
496
|
+
try:
|
|
497
|
+
if path.is_symlink():
|
|
498
|
+
return True
|
|
499
|
+
attributes = getattr(os.lstat(path), "st_file_attributes", 0)
|
|
500
|
+
except OSError:
|
|
501
|
+
return False
|
|
502
|
+
return bool(attributes & _FILE_ATTRIBUTE_REPARSE_POINT)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _receipt_artifact_targets(
|
|
518
|
+
change_dir: Path,
|
|
519
|
+
files_list: list[Any],
|
|
520
|
+
) -> tuple[list[tuple[str, Path]] | None, dict[str, Any] | None]:
|
|
521
|
+
targets: list[tuple[str, Path]] = []
|
|
522
|
+
seen: set[str] = set()
|
|
523
|
+
for index, value in enumerate(files_list):
|
|
524
|
+
if not isinstance(value, str):
|
|
525
|
+
return None, _result_error(
|
|
526
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
527
|
+
f"receipt files[{index}] must be a string",
|
|
528
|
+
)
|
|
529
|
+
raw = value
|
|
530
|
+
segments = raw.split("/")
|
|
531
|
+
rel = PurePosixPath(raw)
|
|
532
|
+
invalid = (
|
|
533
|
+
not raw
|
|
534
|
+
or raw != raw.strip()
|
|
535
|
+
or "\\" in raw
|
|
536
|
+
or ":" in raw
|
|
537
|
+
or rel.is_absolute()
|
|
538
|
+
or any(segment in {"", ".", ".."} for segment in segments)
|
|
539
|
+
or any(segment.endswith((".", " ")) for segment in segments)
|
|
540
|
+
or not rel.parts
|
|
541
|
+
or rel.parts[0] not in {"spec", "plans", "meta"}
|
|
542
|
+
)
|
|
543
|
+
if invalid:
|
|
544
|
+
return None, _result_error(
|
|
545
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
546
|
+
f"receipt files[{index}] is not a safe artifact-relative path: {raw!r}",
|
|
547
|
+
)
|
|
548
|
+
normalized = rel.as_posix()
|
|
549
|
+
if normalized in seen:
|
|
550
|
+
return None, _result_error(
|
|
551
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
552
|
+
f"receipt contains duplicate artifact path: {normalized}",
|
|
553
|
+
)
|
|
554
|
+
seen.add(normalized)
|
|
555
|
+
target = change_dir.joinpath(*rel.parts)
|
|
556
|
+
cursor = change_dir
|
|
557
|
+
for part in rel.parts:
|
|
558
|
+
cursor = cursor / part
|
|
559
|
+
if _is_link_or_reparse(cursor):
|
|
560
|
+
return None, _result_error(
|
|
561
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
562
|
+
f"receipt artifact path traverses a link: {normalized}",
|
|
563
|
+
)
|
|
564
|
+
try:
|
|
565
|
+
target.resolve(strict=False).relative_to(change_dir)
|
|
566
|
+
except (OSError, RuntimeError, ValueError):
|
|
567
|
+
return None, _result_error(
|
|
568
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
569
|
+
f"receipt artifact path resolves outside the change directory: {normalized}",
|
|
570
|
+
)
|
|
571
|
+
targets.append((normalized, target))
|
|
572
|
+
return targets, None
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _verify_plan_v2(change_dir: Path) -> dict[str, Any] | None:
|
|
576
|
+
"""v2 finalizer(TS)证据的只读验收。
|
|
577
|
+
|
|
578
|
+
v2 不产生 meta/plan-finalization.json;canonical 证据是
|
|
579
|
+
meta/plan-finalization-transactions/ + meta/publication-journals/。
|
|
580
|
+
本路径只做结构验收:最新事务终态、匹配 journal 的 committed/readback、
|
|
581
|
+
durable targets 存在且字节哈希与 binding 一致。内容质量由 TS 质量层裁决,
|
|
582
|
+
Python 不重复审判。无 v2 证据时返回 None(调用方回退 RECEIPT_MISSING)。
|
|
583
|
+
"""
|
|
584
|
+
transactions_dir = change_dir / "meta" / "plan-finalization-transactions"
|
|
585
|
+
if not transactions_dir.is_dir():
|
|
586
|
+
return None
|
|
587
|
+
transactions = sorted(
|
|
588
|
+
transactions_dir.glob("*.json"),
|
|
589
|
+
key=lambda path: path.stat().st_mtime,
|
|
590
|
+
)
|
|
591
|
+
if not transactions:
|
|
592
|
+
return None
|
|
593
|
+
latest: dict[str, Any] | None = None
|
|
594
|
+
for candidate in reversed(transactions):
|
|
595
|
+
try:
|
|
596
|
+
payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
597
|
+
except (OSError, json.JSONDecodeError):
|
|
598
|
+
continue
|
|
599
|
+
if isinstance(payload, dict):
|
|
600
|
+
latest = payload
|
|
601
|
+
break
|
|
602
|
+
if latest is None:
|
|
603
|
+
return None
|
|
604
|
+
if latest.get("change_key") != change_dir.name:
|
|
605
|
+
return _result_error(
|
|
606
|
+
"RECEIPT_CHANGE_NAME_INVALID",
|
|
607
|
+
"v2 transaction change_key does not match the change directory",
|
|
608
|
+
)
|
|
609
|
+
if latest.get("status") != "publication_committed_event_complete":
|
|
610
|
+
return _result_error(
|
|
611
|
+
"RECEIPT_NOT_FINALIZED",
|
|
612
|
+
f"v2 transaction status is {latest.get('status')!r}, "
|
|
613
|
+
"expected 'publication_committed_event_complete'",
|
|
614
|
+
)
|
|
615
|
+
operation_id = str(latest.get("operation_id") or "")
|
|
616
|
+
journals_dir = change_dir / "meta" / "publication-journals"
|
|
617
|
+
journal: dict[str, Any] | None = None
|
|
618
|
+
if journals_dir.is_dir():
|
|
619
|
+
for candidate in sorted(journals_dir.glob("*.json")):
|
|
620
|
+
try:
|
|
621
|
+
payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
622
|
+
except (OSError, json.JSONDecodeError):
|
|
623
|
+
continue
|
|
624
|
+
if isinstance(payload, dict) and payload.get("operation_id") == operation_id:
|
|
625
|
+
journal = payload
|
|
626
|
+
break
|
|
627
|
+
if journal is None:
|
|
628
|
+
return _result_error(
|
|
629
|
+
"PUBLICATION_JOURNAL_MISSING",
|
|
630
|
+
f"no publication journal matches v2 operation {operation_id!r}",
|
|
631
|
+
)
|
|
632
|
+
if journal.get("state") != "committed" or journal.get("readback") != "verified":
|
|
633
|
+
return _result_error(
|
|
634
|
+
"PUBLICATION_JOURNAL_NOT_COMMITTED",
|
|
635
|
+
"v2 publication journal is not committed with verified readback",
|
|
636
|
+
)
|
|
637
|
+
binding = journal.get("binding")
|
|
638
|
+
if not isinstance(binding, dict):
|
|
639
|
+
return _result_error("PUBLICATION_BINDING_INVALID", "v2 journal binding missing")
|
|
640
|
+
payload_hashes = binding.get("expected_payload_hashes")
|
|
641
|
+
ownership_paths = binding.get("ownership_paths")
|
|
642
|
+
if not isinstance(payload_hashes, dict) or not isinstance(ownership_paths, list):
|
|
643
|
+
return _result_error("PUBLICATION_BINDING_INVALID", "v2 binding fields malformed")
|
|
644
|
+
required = {
|
|
645
|
+
f"plans/{change_dir.name}-design.md",
|
|
646
|
+
f"plans/{change_dir.name}-plan.md",
|
|
647
|
+
f"plans/{change_dir.name}-implementation-detail.md",
|
|
648
|
+
f"plans/{change_dir.name}-test-scenarios.md",
|
|
649
|
+
# v2 发布的是派生视图 meta/plan-profile.json,不是 Python classify 写的
|
|
650
|
+
# meta/gate-policy.json——后者是 run/test 门禁的权威输入,不能被发布覆盖。
|
|
651
|
+
"meta/plan-profile.json",
|
|
652
|
+
"meta/worktree.json",
|
|
653
|
+
}
|
|
654
|
+
missing_required = sorted(required - set(ownership_paths))
|
|
655
|
+
if missing_required:
|
|
656
|
+
return _result_error(
|
|
657
|
+
"RECEIPT_FILES_INCOMPLETE",
|
|
658
|
+
"v2 journal omits required artifacts: " + ", ".join(missing_required),
|
|
659
|
+
)
|
|
660
|
+
for rel, expected_hash in payload_hashes.items():
|
|
661
|
+
if not isinstance(expected_hash, str) or not expected_hash.startswith("sha256:"):
|
|
662
|
+
return _result_error("PUBLICATION_BINDING_INVALID", f"{rel}: malformed hash")
|
|
663
|
+
target = change_dir / rel
|
|
664
|
+
try:
|
|
665
|
+
resolved = target.resolve()
|
|
666
|
+
resolved.relative_to(change_dir.resolve())
|
|
667
|
+
except (OSError, ValueError):
|
|
668
|
+
return _result_error(
|
|
669
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
670
|
+
f"v2 artifact path resolves outside the change directory: {rel}",
|
|
671
|
+
)
|
|
672
|
+
if not target.is_file():
|
|
673
|
+
return _result_error("ARTIFACT_MISSING", f"published artifact missing: {rel}")
|
|
674
|
+
actual_hash = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
|
|
675
|
+
if actual_hash != expected_hash:
|
|
676
|
+
return _result_error(
|
|
677
|
+
"ARTIFACT_HASH_DRIFT",
|
|
678
|
+
f"{rel}: content hash {actual_hash} != v2 binding {expected_hash}",
|
|
679
|
+
)
|
|
680
|
+
return {
|
|
681
|
+
"ok": True,
|
|
682
|
+
"code": "PLAN_V2_VERIFIED",
|
|
683
|
+
"v2": True,
|
|
684
|
+
"operationId": operation_id,
|
|
685
|
+
"runId": latest.get("run_id"),
|
|
686
|
+
"attempt": latest.get("attempt"),
|
|
687
|
+
"artifactCount": len(payload_hashes),
|
|
688
|
+
"status": latest.get("status"),
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
693
|
+
"""Read-only verification of a finalized plan (retro §5.8).
|
|
694
|
+
|
|
695
|
+
Validates the published artifacts, receipt and events stream without
|
|
696
|
+
requiring the original staging directory. Returns the artifact hash,
|
|
697
|
+
phase.end count/status, frontmatter, and consistency checks. Any parse
|
|
698
|
+
error or inconsistency results in ok=false with a non-zero exit code.
|
|
699
|
+
"""
|
|
700
|
+
change_dir = change_dir.resolve()
|
|
701
|
+
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
702
|
+
if not receipt_path.is_file():
|
|
703
|
+
# v2 finalizer 证据路径(canonical 事实源是 transactions + journals)
|
|
704
|
+
v2_result = _verify_plan_v2(change_dir)
|
|
705
|
+
if v2_result is not None:
|
|
706
|
+
return v2_result
|
|
707
|
+
return _result_error("RECEIPT_MISSING", f"receipt not found: {receipt_path}")
|
|
708
|
+
|
|
709
|
+
try:
|
|
710
|
+
receipt = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
711
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
712
|
+
return _result_error("RECEIPT_INVALID", str(exc))
|
|
713
|
+
if not isinstance(receipt, dict):
|
|
714
|
+
return _result_error("RECEIPT_INVALID", "receipt top-level must be an object")
|
|
715
|
+
|
|
716
|
+
change_name = str(receipt.get("changeName") or "").strip()
|
|
717
|
+
if not change_name:
|
|
718
|
+
return _result_error("RECEIPT_INVALID", "receipt missing changeName")
|
|
719
|
+
if change_name != change_dir.name:
|
|
720
|
+
return _result_error(
|
|
721
|
+
"RECEIPT_CHANGE_NAME_INVALID",
|
|
722
|
+
f"receipt changeName {change_name!r} does not match {change_dir.name!r}",
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
expected_hash = str(receipt.get("artifactsHash") or "").strip()
|
|
726
|
+
if not expected_hash.startswith("sha256:"):
|
|
727
|
+
return _result_error("RECEIPT_INVALID", "receipt artifactsHash missing or malformed")
|
|
728
|
+
|
|
729
|
+
files_list = receipt.get("files")
|
|
730
|
+
if not isinstance(files_list, list) or not files_list:
|
|
731
|
+
return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
|
|
732
|
+
artifact_targets, path_error = _receipt_artifact_targets(change_dir, files_list)
|
|
733
|
+
if path_error is not None:
|
|
734
|
+
return path_error
|
|
735
|
+
assert artifact_targets is not None
|
|
736
|
+
artifact_name_set = {rel for rel, _ in artifact_targets}
|
|
737
|
+
missing_required = sorted(
|
|
738
|
+
_required_artifact_names(change_name) - artifact_name_set
|
|
739
|
+
)
|
|
740
|
+
if missing_required:
|
|
741
|
+
return _result_error(
|
|
742
|
+
"RECEIPT_FILES_INCOMPLETE",
|
|
743
|
+
"receipt omits required artifacts: " + ", ".join(missing_required),
|
|
744
|
+
)
|
|
745
|
+
if receipt.get("status") != "finalized":
|
|
746
|
+
return _result_error(
|
|
747
|
+
"RECEIPT_NOT_FINALIZED",
|
|
748
|
+
f"receipt status is {receipt.get('status')!r}, expected 'finalized'",
|
|
749
|
+
)
|
|
750
|
+
receipt_run_id = str(receipt.get("runId") or "").strip()
|
|
751
|
+
receipt_attempt = receipt.get("attempt")
|
|
752
|
+
if not receipt_run_id or not isinstance(receipt_attempt, int):
|
|
753
|
+
return _result_error(
|
|
754
|
+
"RECEIPT_INVALID",
|
|
755
|
+
"receipt runId/attempt identity is missing or malformed",
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Recompute artifacts hash from published files.
|
|
759
|
+
digest = hashlib.sha256()
|
|
760
|
+
artifact_names: list[str] = []
|
|
761
|
+
for rel, target in artifact_targets:
|
|
762
|
+
if not target.is_file():
|
|
763
|
+
return _result_error(
|
|
764
|
+
"ARTIFACT_MISSING", f"published artifact missing: {rel}"
|
|
765
|
+
)
|
|
766
|
+
artifact_names.append(rel)
|
|
767
|
+
digest.update(rel.encode("utf-8"))
|
|
768
|
+
digest.update(b"\0")
|
|
769
|
+
digest.update(target.read_bytes())
|
|
770
|
+
digest.update(b"\0")
|
|
771
|
+
actual_hash = "sha256:" + digest.hexdigest()
|
|
772
|
+
if actual_hash != expected_hash:
|
|
773
|
+
return _result_error(
|
|
774
|
+
"ARTIFACT_HASH_DRIFT",
|
|
775
|
+
f"published artifacts hash {actual_hash} != receipt {expected_hash}",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# Validate frontmatter on markdown artifacts.
|
|
779
|
+
frontmatter_results: dict[str, Any] = {}
|
|
780
|
+
for rel in artifact_names:
|
|
781
|
+
if not rel.endswith(".md"):
|
|
782
|
+
continue
|
|
783
|
+
target = change_dir / rel
|
|
784
|
+
try:
|
|
785
|
+
text = target.read_text(encoding="utf-8-sig")
|
|
786
|
+
except OSError as exc:
|
|
787
|
+
return _result_error("ARTIFACT_UNREADABLE", f"{rel}: {exc}")
|
|
788
|
+
fm = _frontmatter(text)
|
|
789
|
+
if fm is None:
|
|
790
|
+
return _result_error(
|
|
791
|
+
"ARTIFACT_FRONTMATTER_MISSING", f"{rel}: no frontmatter"
|
|
792
|
+
)
|
|
793
|
+
if fm.get("change-name") != change_name:
|
|
794
|
+
return _result_error(
|
|
795
|
+
"ARTIFACT_FRONTMATTER_INVALID",
|
|
796
|
+
f"{rel}: change-name '{fm.get('change-name')}' != '{change_name}'",
|
|
797
|
+
)
|
|
798
|
+
frontmatter_results[rel] = {
|
|
799
|
+
"change-name": fm.get("change-name"),
|
|
800
|
+
"status": fm.get("status"),
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
plan_path = change_dir / "plans" / f"{change_name}-plan.md"
|
|
804
|
+
try:
|
|
805
|
+
expected_tasks = parse_plan_tasks(plan_path)
|
|
806
|
+
except PlanParseError as exc:
|
|
807
|
+
return _result_error(exc.code, str(exc))
|
|
808
|
+
if not expected_tasks:
|
|
809
|
+
return _result_error(
|
|
810
|
+
"PLAN_TASKS_EMPTY",
|
|
811
|
+
f"no task rows parsed from {plan_path.relative_to(change_dir).as_posix()}",
|
|
812
|
+
)
|
|
813
|
+
checkpoints_path = change_dir / "meta" / "implementation-checkpoints.json"
|
|
814
|
+
if not checkpoints_path.is_file():
|
|
815
|
+
return _result_error(
|
|
816
|
+
"IMPLEMENTATION_CHECKPOINTS_MISSING",
|
|
817
|
+
f"derived task metadata missing: {checkpoints_path}",
|
|
818
|
+
)
|
|
819
|
+
try:
|
|
820
|
+
checkpoints = json.loads(
|
|
821
|
+
checkpoints_path.read_text(encoding="utf-8-sig")
|
|
822
|
+
)
|
|
823
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
824
|
+
return _result_error("IMPLEMENTATION_CHECKPOINTS_INVALID", str(exc))
|
|
825
|
+
expected_checkpoints = {
|
|
826
|
+
"schemaVersion": 1,
|
|
827
|
+
"changeName": change_name,
|
|
828
|
+
"tasks": expected_tasks,
|
|
829
|
+
"foundationGate": "approved",
|
|
830
|
+
}
|
|
831
|
+
if checkpoints != expected_checkpoints:
|
|
832
|
+
return _result_error(
|
|
833
|
+
"IMPLEMENTATION_CHECKPOINTS_DRIFT",
|
|
834
|
+
"derived implementation checkpoints do not match the finalized plan",
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
scenarios_path = (
|
|
838
|
+
change_dir / "plans" / f"{change_name}-test-scenarios.md"
|
|
839
|
+
)
|
|
840
|
+
try:
|
|
841
|
+
expected_scenarios = parse_test_scenarios(scenarios_path)
|
|
842
|
+
except PlanParseError as exc:
|
|
843
|
+
return _result_error(exc.code, str(exc))
|
|
844
|
+
if not expected_scenarios:
|
|
845
|
+
return _result_error(
|
|
846
|
+
"PLAN_SCENARIOS_EMPTY",
|
|
847
|
+
f"no scenario rows parsed from {scenarios_path.relative_to(change_dir).as_posix()}",
|
|
848
|
+
)
|
|
849
|
+
scenario_manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
850
|
+
if not scenario_manifest_path.is_file():
|
|
851
|
+
return _result_error(
|
|
852
|
+
"SCENARIO_MANIFEST_MISSING",
|
|
853
|
+
f"derived scenario metadata missing: {scenario_manifest_path}",
|
|
854
|
+
)
|
|
855
|
+
try:
|
|
856
|
+
manifest = json.loads(
|
|
857
|
+
scenario_manifest_path.read_text(encoding="utf-8-sig")
|
|
858
|
+
)
|
|
859
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
860
|
+
return _result_error("SCENARIO_MANIFEST_INVALID", str(exc))
|
|
861
|
+
actual_scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
|
|
862
|
+
if not isinstance(actual_scenarios, list) or not actual_scenarios:
|
|
863
|
+
return _result_error(
|
|
864
|
+
"SCENARIO_MANIFEST_EMPTY",
|
|
865
|
+
"scenario-manifest.json must contain at least one scenario",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
expected_manifest = {
|
|
869
|
+
"schemaVersion": scenario_manifest_schema_version(expected_scenarios),
|
|
870
|
+
"changeName": change_name,
|
|
871
|
+
"scenarios": [
|
|
872
|
+
{
|
|
873
|
+
key: value
|
|
874
|
+
for key, value in scenario.items()
|
|
875
|
+
if key != "executableMappingDeclared"
|
|
876
|
+
}
|
|
877
|
+
for scenario in expected_scenarios
|
|
878
|
+
],
|
|
879
|
+
}
|
|
880
|
+
if manifest != expected_manifest:
|
|
881
|
+
return _result_error(
|
|
882
|
+
"SCENARIO_MANIFEST_DRIFT",
|
|
883
|
+
"derived scenario manifest does not match the finalized scenario table",
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Validate gate-policy.json is parseable JSON.
|
|
887
|
+
gate_policy_path = change_dir / "meta" / "gate-policy.json"
|
|
888
|
+
if not gate_policy_path.is_file():
|
|
889
|
+
return _result_error(
|
|
890
|
+
"GATE_POLICY_MISSING",
|
|
891
|
+
f"gate policy missing: {gate_policy_path}",
|
|
892
|
+
)
|
|
893
|
+
try:
|
|
894
|
+
json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
|
|
895
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
896
|
+
return _result_error("GATE_POLICY_INVALID", str(exc))
|
|
897
|
+
gate_policy_consistent = True
|
|
898
|
+
|
|
899
|
+
# Validate events.ndjson: require one matching start/end lifecycle.
|
|
900
|
+
events_path = harness_events.events_path(change_dir)
|
|
901
|
+
phase_start_count = 0
|
|
902
|
+
phase_end_count = 0
|
|
903
|
+
phase_end_status: str | None = None
|
|
904
|
+
events_parse_errors: list[str] = []
|
|
905
|
+
if events_path.is_file():
|
|
906
|
+
for line_no, line in enumerate(events_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
907
|
+
if not line.strip():
|
|
908
|
+
continue
|
|
909
|
+
try:
|
|
910
|
+
event = json.loads(line)
|
|
911
|
+
except json.JSONDecodeError as exc:
|
|
912
|
+
events_parse_errors.append(f"line {line_no}: {exc}")
|
|
913
|
+
continue
|
|
914
|
+
if not isinstance(event, dict):
|
|
915
|
+
events_parse_errors.append(f"line {line_no}: not an object")
|
|
916
|
+
continue
|
|
917
|
+
if event.get("phase") != "plan":
|
|
918
|
+
continue
|
|
919
|
+
if (
|
|
920
|
+
str(event.get("run_id") or "") != receipt_run_id
|
|
921
|
+
or event.get("attempt") != receipt_attempt
|
|
922
|
+
):
|
|
923
|
+
continue
|
|
924
|
+
if event.get("type") == "phase.start":
|
|
925
|
+
phase_start_count += 1
|
|
926
|
+
elif event.get("type") == "phase.end":
|
|
927
|
+
phase_end_count += 1
|
|
928
|
+
phase_end_status = str(event.get("status") or "").upper()
|
|
929
|
+
else:
|
|
930
|
+
events_parse_errors.append("events.ndjson missing")
|
|
931
|
+
|
|
932
|
+
if events_parse_errors:
|
|
933
|
+
return _result_error(
|
|
934
|
+
"EVENTS_PARSE_ERROR", "; ".join(events_parse_errors[:5])
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
if phase_start_count == 0:
|
|
938
|
+
return _result_error(
|
|
939
|
+
"PHASE_START_MISSING",
|
|
940
|
+
"no matching plan phase.start event found",
|
|
941
|
+
)
|
|
942
|
+
if phase_start_count > 1:
|
|
943
|
+
return _result_error(
|
|
944
|
+
"PHASE_START_DUPLICATE",
|
|
945
|
+
f"found {phase_start_count} matching plan phase.start events",
|
|
946
|
+
)
|
|
947
|
+
if phase_end_count == 0:
|
|
948
|
+
return _result_error("PHASE_END_MISSING", "no plan phase.end event found")
|
|
949
|
+
if phase_end_count > 1:
|
|
950
|
+
return _result_error(
|
|
951
|
+
"PHASE_END_DUPLICATE", f"found {phase_end_count} phase.end events"
|
|
952
|
+
)
|
|
953
|
+
if phase_end_status != "OK":
|
|
954
|
+
return _result_error(
|
|
955
|
+
"PHASE_END_NOT_OK",
|
|
956
|
+
f"plan phase.end status is {phase_end_status!r}, expected 'OK'",
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
"ok": True,
|
|
961
|
+
"action": "verify",
|
|
962
|
+
"changeDir": str(change_dir),
|
|
963
|
+
"changeName": change_name,
|
|
964
|
+
"artifactsHash": actual_hash,
|
|
965
|
+
"phaseStartCount": phase_start_count,
|
|
966
|
+
"phaseEndCount": phase_end_count,
|
|
967
|
+
"phaseEndStatus": phase_end_status,
|
|
968
|
+
"frontmatter": frontmatter_results,
|
|
969
|
+
"gatePolicyConsistent": gate_policy_consistent,
|
|
970
|
+
"receiptConsistent": True,
|
|
971
|
+
"taskCount": len(expected_tasks),
|
|
972
|
+
"scenarioCount": len(expected_scenarios),
|
|
973
|
+
"files": artifact_names,
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
996
|
+
parser = argparse.ArgumentParser(prog="harness_plan_finalize.py")
|
|
997
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
998
|
+
verify = sub.add_parser("verify")
|
|
999
|
+
verify.add_argument("--change-dir", required=True)
|
|
1000
|
+
verify.add_argument("--json", action="store_true")
|
|
1001
|
+
return parser
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1005
|
+
args = build_parser().parse_args(argv)
|
|
1006
|
+
result = verify_plan(Path(args.change_dir))
|
|
1007
|
+
stream = sys.stdout if result["ok"] else sys.stderr
|
|
1008
|
+
if args.json:
|
|
1009
|
+
stream.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
|
1010
|
+
else:
|
|
1011
|
+
stream.write((result.get("action") or result.get("code") or "error") + "\n")
|
|
1012
|
+
return 0 if result["ok"] else 1
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
if __name__ == "__main__":
|
|
1016
|
+
raise SystemExit(main())
|