hunter-harness 0.2.0 → 0.2.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/dist/bin.js +659 -230
- package/package.json +9 -7
- package/resources/harness/bundles/general/claude-code/.harness-build.json +0 -6
- package/resources/harness/bundles/general/claude-code/CONTEXT.md +0 -65
- package/resources/harness/bundles/general/claude-code/README.md +0 -478
- package/resources/harness/bundles/general/claude-code/agents/harness-evaluator.md +0 -96
- package/resources/harness/bundles/general/claude-code/agents/harness-explorer.md +0 -72
- package/resources/harness/bundles/general/claude-code/agents/harness-reviewer.md +0 -69
- package/resources/harness/bundles/general/claude-code/harness-archive/SKILL.md +0 -215
- package/resources/harness/bundles/general/claude-code/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/general/claude-code/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/general/claude-code/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +0 -112
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +0 -246
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +0 -164
- package/resources/harness/bundles/general/claude-code/harness-plan/SKILL.md +0 -127
- package/resources/harness/bundles/general/claude-code/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/general/claude-code/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/general/claude-code/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/general/claude-code/harness-review/SKILL.md +0 -160
- package/resources/harness/bundles/general/claude-code/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/general/claude-code/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/general/claude-code/harness-review/reference.md +0 -86
- package/resources/harness/bundles/general/claude-code/harness-run/SKILL.md +0 -132
- package/resources/harness/bundles/general/claude-code/harness-run/checklist.md +0 -259
- package/resources/harness/bundles/general/claude-code/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/general/claude-code/harness-run/reference.md +0 -834
- package/resources/harness/bundles/general/claude-code/harness-submit/SKILL.md +0 -159
- package/resources/harness/bundles/general/claude-code/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/general/claude-code/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/general/claude-code/harness-sync/SKILL.md +0 -82
- package/resources/harness/bundles/general/claude-code/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/general/claude-code/harness-test/SKILL.md +0 -180
- package/resources/harness/bundles/general/claude-code/harness-test/checklist.md +0 -319
- package/resources/harness/bundles/general/claude-code/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/general/claude-code/harness-test/reference.md +0 -791
- package/resources/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/general/claude-code/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/general/claude-code/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/general/claude-code/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/general/claude-code/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/general/claude-code/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/general/claude-code/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/general/claude-code/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/general/claude-code/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/general/claude-code/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/general/claude-code/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/general/codebuddy/.harness-build.json +0 -6
- package/resources/harness/bundles/general/codebuddy/CONTEXT.md +0 -65
- package/resources/harness/bundles/general/codebuddy/README.md +0 -478
- package/resources/harness/bundles/general/codebuddy/agents/harness-evaluator.md +0 -94
- package/resources/harness/bundles/general/codebuddy/agents/harness-explorer.md +0 -53
- package/resources/harness/bundles/general/codebuddy/agents/harness-reviewer.md +0 -52
- package/resources/harness/bundles/general/codebuddy/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/general/codebuddy/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/general/codebuddy/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/general/codebuddy/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/general/codebuddy/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/general/codebuddy/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/general/codebuddy/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/general/codebuddy/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/general/codebuddy/harness-review/SKILL.md +0 -142
- package/resources/harness/bundles/general/codebuddy/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/general/codebuddy/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/general/codebuddy/harness-review/reference.md +0 -86
- package/resources/harness/bundles/general/codebuddy/harness-run/SKILL.md +0 -113
- package/resources/harness/bundles/general/codebuddy/harness-run/checklist.md +0 -259
- package/resources/harness/bundles/general/codebuddy/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/general/codebuddy/harness-run/reference.md +0 -834
- package/resources/harness/bundles/general/codebuddy/harness-submit/SKILL.md +0 -142
- package/resources/harness/bundles/general/codebuddy/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/general/codebuddy/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/general/codebuddy/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/general/codebuddy/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/general/codebuddy/harness-test/SKILL.md +0 -160
- package/resources/harness/bundles/general/codebuddy/harness-test/checklist.md +0 -319
- package/resources/harness/bundles/general/codebuddy/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/general/codebuddy/harness-test/reference.md +0 -791
- package/resources/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/general/codebuddy/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/general/codebuddy/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/general/codebuddy/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/general/codebuddy/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/general/codebuddy/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/general/codebuddy/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/general/codebuddy/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/general/codebuddy/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/general/codex/.harness-build.json +0 -6
- package/resources/harness/bundles/general/codex/CONTEXT.md +0 -65
- package/resources/harness/bundles/general/codex/README.md +0 -478
- package/resources/harness/bundles/general/codex/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/general/codex/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/general/codex/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/general/codex/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/general/codex/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/general/codex/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/general/codex/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/general/codex/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/general/codex/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/general/codex/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/general/codex/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/general/codex/harness-review/SKILL.md +0 -137
- package/resources/harness/bundles/general/codex/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/general/codex/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/general/codex/harness-review/reference.md +0 -86
- package/resources/harness/bundles/general/codex/harness-run/SKILL.md +0 -113
- package/resources/harness/bundles/general/codex/harness-run/checklist.md +0 -259
- package/resources/harness/bundles/general/codex/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/general/codex/harness-run/reference.md +0 -834
- package/resources/harness/bundles/general/codex/harness-submit/SKILL.md +0 -142
- package/resources/harness/bundles/general/codex/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/general/codex/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/general/codex/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/general/codex/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/general/codex/harness-test/SKILL.md +0 -160
- package/resources/harness/bundles/general/codex/harness-test/checklist.md +0 -319
- package/resources/harness/bundles/general/codex/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/general/codex/harness-test/reference.md +0 -791
- package/resources/harness/bundles/general/codex/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/general/codex/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/general/codex/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/general/codex/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/general/codex/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/general/codex/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/general/codex/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/general/codex/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/general/codex/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/general/codex/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/general/codex/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/general/codex/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/general/cursor/.harness-build.json +0 -6
- package/resources/harness/bundles/general/cursor/CONTEXT.md +0 -65
- package/resources/harness/bundles/general/cursor/README.md +0 -478
- package/resources/harness/bundles/general/cursor/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/general/cursor/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/general/cursor/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/general/cursor/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/general/cursor/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/general/cursor/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/general/cursor/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/general/cursor/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/general/cursor/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/general/cursor/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/general/cursor/harness-review/SKILL.md +0 -137
- package/resources/harness/bundles/general/cursor/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/general/cursor/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/general/cursor/harness-review/reference.md +0 -86
- package/resources/harness/bundles/general/cursor/harness-run/SKILL.md +0 -113
- package/resources/harness/bundles/general/cursor/harness-run/checklist.md +0 -259
- package/resources/harness/bundles/general/cursor/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/general/cursor/harness-run/reference.md +0 -834
- package/resources/harness/bundles/general/cursor/harness-submit/SKILL.md +0 -142
- package/resources/harness/bundles/general/cursor/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/general/cursor/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/general/cursor/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/general/cursor/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/general/cursor/harness-test/SKILL.md +0 -160
- package/resources/harness/bundles/general/cursor/harness-test/checklist.md +0 -319
- package/resources/harness/bundles/general/cursor/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/general/cursor/harness-test/reference.md +0 -791
- package/resources/harness/bundles/general/cursor/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/general/cursor/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/general/cursor/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/general/cursor/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/general/cursor/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/general/cursor/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/general/cursor/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/general/cursor/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/general/cursor/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/general/cursor/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/general/cursor/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/general/cursor/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/java/claude-code/.harness-build.json +0 -6
- package/resources/harness/bundles/java/claude-code/CONTEXT.md +0 -65
- package/resources/harness/bundles/java/claude-code/README.md +0 -478
- package/resources/harness/bundles/java/claude-code/agents/harness-evaluator.md +0 -96
- package/resources/harness/bundles/java/claude-code/agents/harness-explorer.md +0 -72
- package/resources/harness/bundles/java/claude-code/agents/harness-reviewer.md +0 -69
- package/resources/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +0 -86
- package/resources/harness/bundles/java/claude-code/harness-apidoc/checklist.md +0 -142
- package/resources/harness/bundles/java/claude-code/harness-apidoc/reference.md +0 -233
- package/resources/harness/bundles/java/claude-code/harness-archive/SKILL.md +0 -215
- package/resources/harness/bundles/java/claude-code/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/java/claude-code/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/java/claude-code/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +0 -112
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +0 -246
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +0 -164
- package/resources/harness/bundles/java/claude-code/harness-package/SKILL.md +0 -87
- package/resources/harness/bundles/java/claude-code/harness-package/checklist.md +0 -322
- package/resources/harness/bundles/java/claude-code/harness-package/reference.md +0 -210
- package/resources/harness/bundles/java/claude-code/harness-plan/SKILL.md +0 -127
- package/resources/harness/bundles/java/claude-code/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/java/claude-code/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/java/claude-code/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/java/claude-code/harness-review/SKILL.md +0 -160
- package/resources/harness/bundles/java/claude-code/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/java/claude-code/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/java/claude-code/harness-review/reference.md +0 -86
- package/resources/harness/bundles/java/claude-code/harness-run/SKILL.md +0 -148
- package/resources/harness/bundles/java/claude-code/harness-run/checklist.md +0 -250
- package/resources/harness/bundles/java/claude-code/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/java/claude-code/harness-run/reference.md +0 -814
- package/resources/harness/bundles/java/claude-code/harness-submit/SKILL.md +0 -166
- package/resources/harness/bundles/java/claude-code/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/java/claude-code/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/java/claude-code/harness-sync/SKILL.md +0 -82
- package/resources/harness/bundles/java/claude-code/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/java/claude-code/harness-test/SKILL.md +0 -192
- package/resources/harness/bundles/java/claude-code/harness-test/checklist.md +0 -317
- package/resources/harness/bundles/java/claude-code/harness-test/pitfalls-java.md +0 -16
- package/resources/harness/bundles/java/claude-code/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/java/claude-code/harness-test/reference.md +0 -788
- package/resources/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/java/claude-code/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/java/claude-code/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/java/claude-code/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/java/claude-code/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/java/claude-code/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/java/claude-code/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/java/claude-code/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/java/claude-code/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/java/claude-code/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/java/claude-code/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/java/codebuddy/.harness-build.json +0 -6
- package/resources/harness/bundles/java/codebuddy/CONTEXT.md +0 -65
- package/resources/harness/bundles/java/codebuddy/README.md +0 -478
- package/resources/harness/bundles/java/codebuddy/agents/harness-evaluator.md +0 -94
- package/resources/harness/bundles/java/codebuddy/agents/harness-explorer.md +0 -53
- package/resources/harness/bundles/java/codebuddy/agents/harness-reviewer.md +0 -52
- package/resources/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +0 -68
- package/resources/harness/bundles/java/codebuddy/harness-apidoc/checklist.md +0 -142
- package/resources/harness/bundles/java/codebuddy/harness-apidoc/reference.md +0 -233
- package/resources/harness/bundles/java/codebuddy/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/java/codebuddy/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/java/codebuddy/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/java/codebuddy/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/java/codebuddy/harness-package/SKILL.md +0 -69
- package/resources/harness/bundles/java/codebuddy/harness-package/checklist.md +0 -322
- package/resources/harness/bundles/java/codebuddy/harness-package/reference.md +0 -210
- package/resources/harness/bundles/java/codebuddy/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/java/codebuddy/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/java/codebuddy/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/java/codebuddy/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/java/codebuddy/harness-review/SKILL.md +0 -142
- package/resources/harness/bundles/java/codebuddy/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/java/codebuddy/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/java/codebuddy/harness-review/reference.md +0 -86
- package/resources/harness/bundles/java/codebuddy/harness-run/SKILL.md +0 -129
- package/resources/harness/bundles/java/codebuddy/harness-run/checklist.md +0 -250
- package/resources/harness/bundles/java/codebuddy/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/java/codebuddy/harness-run/reference.md +0 -814
- package/resources/harness/bundles/java/codebuddy/harness-submit/SKILL.md +0 -149
- package/resources/harness/bundles/java/codebuddy/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/java/codebuddy/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/java/codebuddy/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/java/codebuddy/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/java/codebuddy/harness-test/SKILL.md +0 -172
- package/resources/harness/bundles/java/codebuddy/harness-test/checklist.md +0 -317
- package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls-java.md +0 -16
- package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/java/codebuddy/harness-test/reference.md +0 -788
- package/resources/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/java/codebuddy/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/java/codebuddy/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/java/codebuddy/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/java/codebuddy/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/java/codebuddy/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/java/codebuddy/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/java/codebuddy/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/java/codebuddy/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/java/codex/.harness-build.json +0 -6
- package/resources/harness/bundles/java/codex/CONTEXT.md +0 -65
- package/resources/harness/bundles/java/codex/README.md +0 -478
- package/resources/harness/bundles/java/codex/harness-apidoc/SKILL.md +0 -68
- package/resources/harness/bundles/java/codex/harness-apidoc/checklist.md +0 -142
- package/resources/harness/bundles/java/codex/harness-apidoc/reference.md +0 -233
- package/resources/harness/bundles/java/codex/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/java/codex/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/java/codex/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/java/codex/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/java/codex/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/java/codex/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/java/codex/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/java/codex/harness-package/SKILL.md +0 -69
- package/resources/harness/bundles/java/codex/harness-package/checklist.md +0 -322
- package/resources/harness/bundles/java/codex/harness-package/reference.md +0 -210
- package/resources/harness/bundles/java/codex/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/java/codex/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/java/codex/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/java/codex/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/java/codex/harness-review/SKILL.md +0 -137
- package/resources/harness/bundles/java/codex/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/java/codex/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/java/codex/harness-review/reference.md +0 -86
- package/resources/harness/bundles/java/codex/harness-run/SKILL.md +0 -129
- package/resources/harness/bundles/java/codex/harness-run/checklist.md +0 -250
- package/resources/harness/bundles/java/codex/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/java/codex/harness-run/reference.md +0 -814
- package/resources/harness/bundles/java/codex/harness-submit/SKILL.md +0 -149
- package/resources/harness/bundles/java/codex/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/java/codex/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/java/codex/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/java/codex/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/java/codex/harness-test/SKILL.md +0 -172
- package/resources/harness/bundles/java/codex/harness-test/checklist.md +0 -317
- package/resources/harness/bundles/java/codex/harness-test/pitfalls-java.md +0 -16
- package/resources/harness/bundles/java/codex/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/java/codex/harness-test/reference.md +0 -788
- package/resources/harness/bundles/java/codex/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/java/codex/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/java/codex/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/java/codex/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/java/codex/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/java/codex/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/java/codex/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/java/codex/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/java/codex/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/java/codex/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/java/codex/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/java/codex/scripts/harness_service.py +0 -1331
- package/resources/harness/bundles/java/cursor/.harness-build.json +0 -6
- package/resources/harness/bundles/java/cursor/CONTEXT.md +0 -65
- package/resources/harness/bundles/java/cursor/README.md +0 -478
- package/resources/harness/bundles/java/cursor/harness-apidoc/SKILL.md +0 -68
- package/resources/harness/bundles/java/cursor/harness-apidoc/checklist.md +0 -142
- package/resources/harness/bundles/java/cursor/harness-apidoc/reference.md +0 -233
- package/resources/harness/bundles/java/cursor/harness-archive/SKILL.md +0 -198
- package/resources/harness/bundles/java/cursor/harness-archive/checklist.md +0 -52
- package/resources/harness/bundles/java/cursor/harness-archive/reference.md +0 -103
- package/resources/harness/bundles/java/cursor/harness-archive/scripts/gen-manifest.ps1 +0 -31
- package/resources/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +0 -171
- package/resources/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +0 -128
- package/resources/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +0 -95
- package/resources/harness/bundles/java/cursor/harness-codebase-map/checklist.md +0 -126
- package/resources/harness/bundles/java/cursor/harness-codebase-map/reference.md +0 -364
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONCERNS.md +0 -49
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONVENTIONS.md +0 -57
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STACK.md +0 -55
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STRUCTURE.md +0 -69
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/TESTING.md +0 -64
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-manifest.schema.json +0 -73
- package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-summary.md +0 -64
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +0 -229
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/design.md +0 -842
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +0 -309
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
- package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
- package/resources/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +0 -147
- package/resources/harness/bundles/java/cursor/harness-package/SKILL.md +0 -69
- package/resources/harness/bundles/java/cursor/harness-package/checklist.md +0 -322
- package/resources/harness/bundles/java/cursor/harness-package/reference.md +0 -210
- package/resources/harness/bundles/java/cursor/harness-plan/SKILL.md +0 -109
- package/resources/harness/bundles/java/cursor/harness-plan/checklist.md +0 -243
- package/resources/harness/bundles/java/cursor/harness-plan/protocols.md +0 -124
- package/resources/harness/bundles/java/cursor/harness-plan/reference.md +0 -376
- package/resources/harness/bundles/java/cursor/harness-review/SKILL.md +0 -137
- package/resources/harness/bundles/java/cursor/harness-review/checklist.md +0 -124
- package/resources/harness/bundles/java/cursor/harness-review/protocols.md +0 -68
- package/resources/harness/bundles/java/cursor/harness-review/reference.md +0 -86
- package/resources/harness/bundles/java/cursor/harness-run/SKILL.md +0 -129
- package/resources/harness/bundles/java/cursor/harness-run/checklist.md +0 -250
- package/resources/harness/bundles/java/cursor/harness-run/protocols.md +0 -78
- package/resources/harness/bundles/java/cursor/harness-run/reference.md +0 -814
- package/resources/harness/bundles/java/cursor/harness-submit/SKILL.md +0 -149
- package/resources/harness/bundles/java/cursor/harness-submit/checklist.md +0 -407
- package/resources/harness/bundles/java/cursor/harness-submit/reference.md +0 -152
- package/resources/harness/bundles/java/cursor/harness-sync/SKILL.md +0 -65
- package/resources/harness/bundles/java/cursor/harness-sync/reference.md +0 -153
- package/resources/harness/bundles/java/cursor/harness-test/SKILL.md +0 -172
- package/resources/harness/bundles/java/cursor/harness-test/checklist.md +0 -317
- package/resources/harness/bundles/java/cursor/harness-test/pitfalls-java.md +0 -16
- package/resources/harness/bundles/java/cursor/harness-test/pitfalls.md +0 -260
- package/resources/harness/bundles/java/cursor/harness-test/reference.md +0 -788
- package/resources/harness/bundles/java/cursor/protocols/archive-report-protocol.md +0 -175
- package/resources/harness/bundles/java/cursor/protocols/evidence-based-reporting-protocol.md +0 -134
- package/resources/harness/bundles/java/cursor/protocols/ledger-protocol.md +0 -270
- package/resources/harness/bundles/java/cursor/protocols/powershell-protocol.md +0 -181
- package/resources/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +0 -150
- package/resources/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +0 -92
- package/resources/harness/bundles/java/cursor/protocols/state-layout-protocol.md +0 -131
- package/resources/harness/bundles/java/cursor/protocols/submit-protocol.md +0 -78
- package/resources/harness/bundles/java/cursor/scripts/harness_archive.py +0 -2319
- package/resources/harness/bundles/java/cursor/scripts/harness_deploy.py +0 -674
- package/resources/harness/bundles/java/cursor/scripts/harness_events.py +0 -680
- package/resources/harness/bundles/java/cursor/scripts/harness_ledger.py +0 -671
- package/resources/harness/bundles/java/cursor/scripts/harness_preflight.py +0 -780
- package/resources/harness/bundles/java/cursor/scripts/harness_service.py +0 -1331
- package/resources/harness/manifests/general/claude-code.json +0 -277
- package/resources/harness/manifests/general/codebuddy.json +0 -277
- package/resources/harness/manifests/general/codex.json +0 -265
- package/resources/harness/manifests/general/cursor.json +0 -265
- package/resources/harness/manifests/java/claude-code.json +0 -305
- package/resources/harness/manifests/java/codebuddy.json +0 -305
- package/resources/harness/manifests/java/codex.json +0 -293
- package/resources/harness/manifests/java/cursor.json +0 -293
- package/resources/harness/migrations/0.1.1/general.json +0 -358
- package/resources/harness/migrations/0.1.1/java.json +0 -398
|
@@ -1,2319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Harness archive finalize / status / replay (D3).
|
|
3
|
-
|
|
4
|
-
Subcommands:
|
|
5
|
-
status — pre-archive gate checks (read-only)
|
|
6
|
-
finalize — single-process archive: manifest → move → collect → render →
|
|
7
|
-
validate → after-manifest → delete-or-keep → knowledge/service
|
|
8
|
-
replay — read-only re-collect + validate for historical archives
|
|
9
|
-
|
|
10
|
-
Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
|
|
11
|
-
Depends on P0-1 harness_events.py; optionally P0-3 harness_knowledge.py.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import datetime as dt
|
|
18
|
-
import hashlib
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import re
|
|
22
|
-
import shutil
|
|
23
|
-
import subprocess
|
|
24
|
-
import sys
|
|
25
|
-
import tempfile
|
|
26
|
-
import uuid
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if hasattr(sys.stdout, "reconfigure"):
|
|
32
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
33
|
-
if hasattr(sys.stderr, "reconfigure"):
|
|
34
|
-
sys.stderr.reconfigure(encoding="utf-8")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
38
|
-
SKILLS_ROOT = SCRIPTS_DIR.parent
|
|
39
|
-
RENDER_SCRIPT = SKILLS_ROOT / "harness-archive" / "templates" / "render-summary.mjs"
|
|
40
|
-
SUMMARY_TEMPLATE = (
|
|
41
|
-
SKILLS_ROOT / "harness-archive" / "templates" / "summary-data-template.json"
|
|
42
|
-
)
|
|
43
|
-
KNOWLEDGE_SCRIPT = (
|
|
44
|
-
SKILLS_ROOT / "harness-knowledge-ingest" / "scripts" / "harness_knowledge.py"
|
|
45
|
-
)
|
|
46
|
-
SERVICE_SCRIPT = SCRIPTS_DIR / "harness_service.py"
|
|
47
|
-
|
|
48
|
-
# Manifest compare must ignore self-mutating log files appended during finalize.
|
|
49
|
-
MANIFEST_COMPARE_EXCLUDE = frozenset(
|
|
50
|
-
{
|
|
51
|
-
"logs/execution-log.md",
|
|
52
|
-
"execution-log.md",
|
|
53
|
-
"events.ndjson",
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
SCHEMA_VERSION = "2.2"
|
|
58
|
-
NOT_AVAILABLE = "not_available"
|
|
59
|
-
|
|
60
|
-
# Ensure sibling harness_events is importable.
|
|
61
|
-
if str(SCRIPTS_DIR) not in sys.path:
|
|
62
|
-
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
63
|
-
|
|
64
|
-
import harness_events as he # noqa: E402
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# ---------------------------------------------------------------------------
|
|
68
|
-
# I/O helpers
|
|
69
|
-
# ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def now_iso() -> str:
|
|
73
|
-
return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def today_date() -> str:
|
|
77
|
-
return dt.date.today().isoformat()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def emit_json(payload: dict[str, Any]) -> None:
|
|
81
|
-
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def emit_error(message: str, *, as_json: bool, extra: dict[str, Any] | None = None) -> int:
|
|
85
|
-
payload: dict[str, Any] = {"ok": False, "error": message}
|
|
86
|
-
if extra:
|
|
87
|
-
payload.update(extra)
|
|
88
|
-
if as_json:
|
|
89
|
-
sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
90
|
-
else:
|
|
91
|
-
sys.stderr.write(f"error: {message}\n")
|
|
92
|
-
return 1
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def resolve_path(raw: str) -> Path:
|
|
96
|
-
return Path(raw).expanduser().resolve()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def read_json(path: Path) -> Any:
|
|
100
|
-
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def write_json(path: Path, data: Any) -> None:
|
|
104
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
-
path.write_text(
|
|
106
|
-
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
|
107
|
-
encoding="utf-8",
|
|
108
|
-
newline="\n",
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def sha256_file(path: Path) -> str:
|
|
113
|
-
h = hashlib.sha256()
|
|
114
|
-
with path.open("rb") as f:
|
|
115
|
-
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
116
|
-
h.update(chunk)
|
|
117
|
-
return h.hexdigest()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def find_project_root(change_or_archive_dir: Path) -> Path:
|
|
121
|
-
"""Resolve project root from .harness/changes|archive/<name>."""
|
|
122
|
-
p = change_or_archive_dir.resolve()
|
|
123
|
-
if p.parent.name in {"changes", "archive"} and p.parent.parent.name == ".harness":
|
|
124
|
-
return p.parent.parent.parent
|
|
125
|
-
# Fallback: walk up looking for .harness
|
|
126
|
-
for parent in p.parents:
|
|
127
|
-
if (parent / ".harness").is_dir():
|
|
128
|
-
return parent
|
|
129
|
-
return p.parent
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def infer_change_name(dir_path: Path) -> str:
|
|
133
|
-
name = dir_path.name
|
|
134
|
-
m = re.match(r"^(\d{4}-\d{2}-\d{2})-(.+)$", name)
|
|
135
|
-
if m:
|
|
136
|
-
return m.group(2)
|
|
137
|
-
return name
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def load_template() -> dict[str, Any]:
|
|
141
|
-
if SUMMARY_TEMPLATE.is_file():
|
|
142
|
-
data = read_json(SUMMARY_TEMPLATE)
|
|
143
|
-
if isinstance(data, dict):
|
|
144
|
-
return data
|
|
145
|
-
return {"schemaVersion": SCHEMA_VERSION}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# ---------------------------------------------------------------------------
|
|
149
|
-
# Events append (reuse P0-1)
|
|
150
|
-
# ---------------------------------------------------------------------------
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def append_event(
|
|
154
|
-
change_dir: Path,
|
|
155
|
-
*,
|
|
156
|
-
phase: str,
|
|
157
|
-
type_: str,
|
|
158
|
-
**fields: Any,
|
|
159
|
-
) -> dict[str, Any]:
|
|
160
|
-
"""Append one event via harness_events primitives and re-render execution-log."""
|
|
161
|
-
path = he.events_path(change_dir)
|
|
162
|
-
existing = he.load_events(path) if path.exists() else []
|
|
163
|
-
event: dict[str, Any] = {
|
|
164
|
-
"schema_version": he.SCHEMA_VERSION,
|
|
165
|
-
"id": he.new_event_id(existing),
|
|
166
|
-
"timestamp": now_iso(),
|
|
167
|
-
"phase": phase,
|
|
168
|
-
"type": type_,
|
|
169
|
-
"note": "",
|
|
170
|
-
}
|
|
171
|
-
for key, value in fields.items():
|
|
172
|
-
if value is not None:
|
|
173
|
-
event[key] = value
|
|
174
|
-
line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
175
|
-
he.atomic_append_line(path, line)
|
|
176
|
-
all_events = existing + [he.normalize_event(event)]
|
|
177
|
-
he.write_execution_log(change_dir, he.render_execution_log(all_events))
|
|
178
|
-
return event
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# ---------------------------------------------------------------------------
|
|
182
|
-
# Manifest (Python port of gen-manifest.ps1)
|
|
183
|
-
# ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
|
|
187
|
-
"""Build path/size/sha256 manifest; exclude the output file itself."""
|
|
188
|
-
root = root.resolve()
|
|
189
|
-
exclude: Path | None = None
|
|
190
|
-
if output_path.exists():
|
|
191
|
-
exclude = output_path.resolve()
|
|
192
|
-
|
|
193
|
-
files: list[dict[str, Any]] = []
|
|
194
|
-
total_bytes = 0
|
|
195
|
-
for path in sorted(root.rglob("*")):
|
|
196
|
-
if not path.is_file():
|
|
197
|
-
continue
|
|
198
|
-
if exclude is not None and path.resolve() == exclude:
|
|
199
|
-
continue
|
|
200
|
-
rel = path.relative_to(root).as_posix()
|
|
201
|
-
size = path.stat().st_size
|
|
202
|
-
total_bytes += size
|
|
203
|
-
files.append(
|
|
204
|
-
{
|
|
205
|
-
"path": rel,
|
|
206
|
-
"sizeBytes": size,
|
|
207
|
-
"sha256": sha256_file(path),
|
|
208
|
-
}
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
result = {
|
|
212
|
-
"root": str(root),
|
|
213
|
-
"generatedAt": dt.datetime.now().isoformat(timespec="seconds"),
|
|
214
|
-
"fileCount": len(files),
|
|
215
|
-
"totalBytes": total_bytes,
|
|
216
|
-
"files": files,
|
|
217
|
-
}
|
|
218
|
-
write_json(output_path, result)
|
|
219
|
-
return result
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _manifest_path_excluded(rel: str) -> bool:
|
|
223
|
-
norm = rel.replace("\\", "/")
|
|
224
|
-
if norm in MANIFEST_COMPARE_EXCLUDE:
|
|
225
|
-
return True
|
|
226
|
-
if norm == "events.ndjson" or norm.endswith("/events.ndjson"):
|
|
227
|
-
return True
|
|
228
|
-
if norm.endswith("execution-log.md"):
|
|
229
|
-
return True
|
|
230
|
-
return False
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _manifest_index(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
234
|
-
out: dict[str, dict[str, Any]] = {}
|
|
235
|
-
for item in manifest.get("files") or []:
|
|
236
|
-
if not isinstance(item, dict):
|
|
237
|
-
continue
|
|
238
|
-
rel = str(item.get("path") or "").replace("\\", "/")
|
|
239
|
-
if not rel or _manifest_path_excluded(rel):
|
|
240
|
-
continue
|
|
241
|
-
out[rel] = item
|
|
242
|
-
return out
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def compare_manifests(
|
|
246
|
-
before: dict[str, Any],
|
|
247
|
-
after: dict[str, Any],
|
|
248
|
-
) -> dict[str, Any]:
|
|
249
|
-
"""Compare before/after; exclude execution-log / events self-appends.
|
|
250
|
-
|
|
251
|
-
Files only in after are treated as generated (OK).
|
|
252
|
-
Files in before missing or hash-mismatched in after are errors.
|
|
253
|
-
"""
|
|
254
|
-
b_idx = _manifest_index(before)
|
|
255
|
-
a_idx = _manifest_index(after)
|
|
256
|
-
|
|
257
|
-
missing: list[str] = []
|
|
258
|
-
mismatched: list[dict[str, str]] = []
|
|
259
|
-
for rel, b_item in b_idx.items():
|
|
260
|
-
a_item = a_idx.get(rel)
|
|
261
|
-
if a_item is None:
|
|
262
|
-
missing.append(rel)
|
|
263
|
-
continue
|
|
264
|
-
if str(a_item.get("sha256")) != str(b_item.get("sha256")):
|
|
265
|
-
mismatched.append(
|
|
266
|
-
{
|
|
267
|
-
"path": rel,
|
|
268
|
-
"before": str(b_item.get("sha256")),
|
|
269
|
-
"after": str(a_item.get("sha256")),
|
|
270
|
-
}
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
generated = sorted(set(a_idx) - set(b_idx))
|
|
274
|
-
moved_ok = len(b_idx) - len(missing) - len(mismatched)
|
|
275
|
-
ok = not missing and not mismatched
|
|
276
|
-
return {
|
|
277
|
-
"ok": ok,
|
|
278
|
-
"movedFiles": moved_ok,
|
|
279
|
-
"generatedFiles": len(generated),
|
|
280
|
-
"totalArchiveFiles": int(after.get("fileCount") or len(a_idx)),
|
|
281
|
-
"missing": missing,
|
|
282
|
-
"mismatched": mismatched,
|
|
283
|
-
"generated": generated,
|
|
284
|
-
"checksumStatus": "OK" if ok else "FAIL",
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# ---------------------------------------------------------------------------
|
|
289
|
-
# Evidence loaders
|
|
290
|
-
# ---------------------------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def load_ledger(change_dir: Path) -> dict[str, Any] | None:
|
|
294
|
-
for rel in (
|
|
295
|
-
"evidence/verification-ledger.json",
|
|
296
|
-
"verification-ledger.json",
|
|
297
|
-
):
|
|
298
|
-
path = change_dir / rel
|
|
299
|
-
if path.is_file():
|
|
300
|
-
try:
|
|
301
|
-
data = read_json(path)
|
|
302
|
-
return data if isinstance(data, dict) else None
|
|
303
|
-
except (OSError, json.JSONDecodeError):
|
|
304
|
-
return None
|
|
305
|
-
return None
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def load_execution_log(change_dir: Path) -> str:
|
|
309
|
-
for rel in ("logs/execution-log.md", "execution-log.md"):
|
|
310
|
-
path = change_dir / rel
|
|
311
|
-
if path.is_file():
|
|
312
|
-
try:
|
|
313
|
-
return path.read_text(encoding="utf-8-sig")
|
|
314
|
-
except OSError:
|
|
315
|
-
return ""
|
|
316
|
-
return ""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def load_existing_summary(change_dir: Path) -> dict[str, Any] | None:
|
|
320
|
-
for rel in (
|
|
321
|
-
"reports/final/summary-data.json",
|
|
322
|
-
"summary-data.json",
|
|
323
|
-
):
|
|
324
|
-
path = change_dir / rel
|
|
325
|
-
if path.is_file():
|
|
326
|
-
try:
|
|
327
|
-
data = read_json(path)
|
|
328
|
-
return data if isinstance(data, dict) else None
|
|
329
|
-
except (OSError, json.JSONDecodeError):
|
|
330
|
-
return None
|
|
331
|
-
return None
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def find_test_reports(change_dir: Path) -> list[Path]:
|
|
335
|
-
patterns = [
|
|
336
|
-
"tests/test-report-*.md",
|
|
337
|
-
"reports/test/test-report-*.md",
|
|
338
|
-
"reports/test/*.md",
|
|
339
|
-
]
|
|
340
|
-
found: list[Path] = []
|
|
341
|
-
for pattern in patterns:
|
|
342
|
-
found.extend(sorted(change_dir.glob(pattern)))
|
|
343
|
-
return found
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def find_review_reports(change_dir: Path) -> list[Path]:
|
|
347
|
-
patterns = [
|
|
348
|
-
"reports/review/review-report-*.md",
|
|
349
|
-
"reviews/review-report-*.md",
|
|
350
|
-
"reports/review/fixback-*.md",
|
|
351
|
-
]
|
|
352
|
-
found: list[Path] = []
|
|
353
|
-
for pattern in patterns:
|
|
354
|
-
found.extend(sorted(change_dir.glob(pattern)))
|
|
355
|
-
return found
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def git_run(project: Path, *args: str) -> tuple[int, str, str]:
|
|
359
|
-
try:
|
|
360
|
-
proc = subprocess.run(
|
|
361
|
-
["git", "-C", str(project), *args],
|
|
362
|
-
capture_output=True,
|
|
363
|
-
text=True,
|
|
364
|
-
encoding="utf-8",
|
|
365
|
-
errors="replace",
|
|
366
|
-
timeout=30,
|
|
367
|
-
check=False,
|
|
368
|
-
)
|
|
369
|
-
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
|
370
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
371
|
-
return 1, "", str(exc)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def extract_final_pushed_hash(execution_log: str) -> str | None:
|
|
375
|
-
"""Parse 'final pushed hash' from execution-log (submit phase)."""
|
|
376
|
-
patterns = [
|
|
377
|
-
r"final pushed hash[:\s`]+([0-9a-f]{7,40})",
|
|
378
|
-
r"finalPushedHash[:\s\"']+([0-9a-f]{7,40})",
|
|
379
|
-
r"pushed hash[:\s`]+([0-9a-f]{7,40})",
|
|
380
|
-
]
|
|
381
|
-
for pat in patterns:
|
|
382
|
-
m = re.search(pat, execution_log, re.IGNORECASE)
|
|
383
|
-
if m:
|
|
384
|
-
return m.group(1)
|
|
385
|
-
return None
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def worktree_requested(change_dir: Path) -> bool:
|
|
389
|
-
for rel in ("meta/worktree.json", "worktree.json"):
|
|
390
|
-
path = change_dir / rel
|
|
391
|
-
if path.is_file():
|
|
392
|
-
try:
|
|
393
|
-
data = read_json(path)
|
|
394
|
-
if isinstance(data, dict):
|
|
395
|
-
return bool(data.get("requested"))
|
|
396
|
-
except (OSError, json.JSONDecodeError):
|
|
397
|
-
pass
|
|
398
|
-
return False
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
# ---------------------------------------------------------------------------
|
|
402
|
-
# status
|
|
403
|
-
# ---------------------------------------------------------------------------
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def check_status(change_dir: Path) -> dict[str, Any]:
|
|
407
|
-
"""Read-only archive preconditions. Never mutates."""
|
|
408
|
-
blockers: list[dict[str, str]] = []
|
|
409
|
-
warnings: list[dict[str, str]] = []
|
|
410
|
-
checks: dict[str, Any] = {}
|
|
411
|
-
|
|
412
|
-
if not change_dir.is_dir():
|
|
413
|
-
return {
|
|
414
|
-
"ok": False,
|
|
415
|
-
"archivable": False,
|
|
416
|
-
"change_dir": str(change_dir),
|
|
417
|
-
"blockers": [{"code": "missing-change-dir", "message": f"not found: {change_dir}"}],
|
|
418
|
-
"warnings": [],
|
|
419
|
-
"checks": {},
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
project = find_project_root(change_dir)
|
|
423
|
-
checks["project_root"] = str(project)
|
|
424
|
-
|
|
425
|
-
# --- commit pushed ---
|
|
426
|
-
code, out, err = git_run(project, "rev-parse", "--is-inside-work-tree")
|
|
427
|
-
if code != 0:
|
|
428
|
-
warnings.append(
|
|
429
|
-
{
|
|
430
|
-
"code": "git-unavailable",
|
|
431
|
-
"message": f"not a git work tree or git missing: {err or out}",
|
|
432
|
-
}
|
|
433
|
-
)
|
|
434
|
-
checks["commit_pushed"] = None
|
|
435
|
-
else:
|
|
436
|
-
code, out, err = git_run(project, "log", "@{u}..HEAD", "--oneline")
|
|
437
|
-
if code != 0:
|
|
438
|
-
# No upstream configured
|
|
439
|
-
warnings.append(
|
|
440
|
-
{
|
|
441
|
-
"code": "no-upstream",
|
|
442
|
-
"message": err or "no upstream configured; cannot verify push",
|
|
443
|
-
}
|
|
444
|
-
)
|
|
445
|
-
checks["commit_pushed"] = None
|
|
446
|
-
elif out.strip():
|
|
447
|
-
blockers.append(
|
|
448
|
-
{
|
|
449
|
-
"code": "unpushed-commits",
|
|
450
|
-
"message": f"unpushed commits:\n{out}",
|
|
451
|
-
}
|
|
452
|
-
)
|
|
453
|
-
checks["commit_pushed"] = False
|
|
454
|
-
else:
|
|
455
|
-
checks["commit_pushed"] = True
|
|
456
|
-
|
|
457
|
-
# --- final hash ---
|
|
458
|
-
code, head, _ = git_run(project, "rev-parse", "HEAD")
|
|
459
|
-
head_hash = head if code == 0 else None
|
|
460
|
-
checks["head"] = head_hash
|
|
461
|
-
|
|
462
|
-
expected_hash: str | None = None
|
|
463
|
-
hash_source: str | None = None
|
|
464
|
-
ledger = load_ledger(change_dir)
|
|
465
|
-
if worktree_requested(change_dir) and ledger:
|
|
466
|
-
merge_hash = ledger.get("mergeFinalHash") or (
|
|
467
|
-
ledger.get("merge") or {}
|
|
468
|
-
).get("finalHash")
|
|
469
|
-
if merge_hash:
|
|
470
|
-
expected_hash = str(merge_hash)
|
|
471
|
-
hash_source = "verification-ledger.mergeFinalHash"
|
|
472
|
-
if expected_hash is None:
|
|
473
|
-
log_text = load_execution_log(change_dir)
|
|
474
|
-
pushed = extract_final_pushed_hash(log_text)
|
|
475
|
-
if pushed:
|
|
476
|
-
expected_hash = pushed
|
|
477
|
-
hash_source = "execution-log.final-pushed-hash"
|
|
478
|
-
if expected_hash is None and ledger:
|
|
479
|
-
for key in ("finalCommit", "finalHash", "headCommit"):
|
|
480
|
-
if ledger.get(key):
|
|
481
|
-
expected_hash = str(ledger[key])
|
|
482
|
-
hash_source = f"verification-ledger.{key}"
|
|
483
|
-
break
|
|
484
|
-
|
|
485
|
-
checks["expected_final_hash"] = expected_hash
|
|
486
|
-
checks["hash_source"] = hash_source
|
|
487
|
-
|
|
488
|
-
if head_hash and expected_hash:
|
|
489
|
-
# Allow short-hash prefix match
|
|
490
|
-
if head_hash.startswith(expected_hash) or expected_hash.startswith(head_hash[:7]):
|
|
491
|
-
checks["final_hash_match"] = True
|
|
492
|
-
else:
|
|
493
|
-
checks["final_hash_match"] = False
|
|
494
|
-
blockers.append(
|
|
495
|
-
{
|
|
496
|
-
"code": "final-hash-mismatch",
|
|
497
|
-
"message": (
|
|
498
|
-
f"HEAD={head_hash} != expected={expected_hash} "
|
|
499
|
-
f"(source={hash_source})"
|
|
500
|
-
),
|
|
501
|
-
}
|
|
502
|
-
)
|
|
503
|
-
elif head_hash and expected_hash is None:
|
|
504
|
-
warnings.append(
|
|
505
|
-
{
|
|
506
|
-
"code": "final-hash-unknown",
|
|
507
|
-
"message": "could not determine expected final hash from ledger/log",
|
|
508
|
-
}
|
|
509
|
-
)
|
|
510
|
-
checks["final_hash_match"] = None
|
|
511
|
-
else:
|
|
512
|
-
checks["final_hash_match"] = None
|
|
513
|
-
|
|
514
|
-
# --- test / review reports ---
|
|
515
|
-
test_reports = find_test_reports(change_dir)
|
|
516
|
-
review_reports = find_review_reports(change_dir)
|
|
517
|
-
log_text = load_execution_log(change_dir)
|
|
518
|
-
has_review_section = bool(
|
|
519
|
-
re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE)
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
checks["test_reports"] = [str(p.relative_to(change_dir)) for p in test_reports]
|
|
523
|
-
checks["review_reports"] = [str(p.relative_to(change_dir)) for p in review_reports]
|
|
524
|
-
|
|
525
|
-
if test_reports:
|
|
526
|
-
checks["test_report_status"] = "present"
|
|
527
|
-
else:
|
|
528
|
-
checks["test_report_status"] = "missing-mark-skipped"
|
|
529
|
-
warnings.append(
|
|
530
|
-
{
|
|
531
|
-
"code": "test-report-missing",
|
|
532
|
-
"message": (
|
|
533
|
-
"no test-report-*.md; archive must mark verification as "
|
|
534
|
-
"NOT_RUN/USER_SKIPPED (not fabricated pass rates)"
|
|
535
|
-
),
|
|
536
|
-
}
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
if review_reports:
|
|
540
|
-
checks["review_report_status"] = "present"
|
|
541
|
-
elif has_review_section:
|
|
542
|
-
checks["review_report_status"] = "ran-but-not-persisted"
|
|
543
|
-
warnings.append(
|
|
544
|
-
{
|
|
545
|
-
"code": "review-not-persisted",
|
|
546
|
-
"message": (
|
|
547
|
-
"execution-log has review section but no review-report file; "
|
|
548
|
-
"prefer persisting report before archive"
|
|
549
|
-
),
|
|
550
|
-
}
|
|
551
|
-
)
|
|
552
|
-
else:
|
|
553
|
-
checks["review_report_status"] = "not-run"
|
|
554
|
-
warnings.append(
|
|
555
|
-
{
|
|
556
|
-
"code": "review-not-run",
|
|
557
|
-
"message": "no review report; mark reviewSummary as ADVISORY_NOT_RUN",
|
|
558
|
-
}
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
archivable = len(blockers) == 0
|
|
562
|
-
return {
|
|
563
|
-
"ok": True,
|
|
564
|
-
"archivable": archivable,
|
|
565
|
-
"change_dir": str(change_dir),
|
|
566
|
-
"change_name": infer_change_name(change_dir),
|
|
567
|
-
"blockers": blockers,
|
|
568
|
-
"warnings": warnings,
|
|
569
|
-
"checks": checks,
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
# ---------------------------------------------------------------------------
|
|
574
|
-
# collect
|
|
575
|
-
# ---------------------------------------------------------------------------
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def _deepcopy_json(obj: Any) -> Any:
|
|
579
|
-
return json.loads(json.dumps(obj))
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
def _na_if_missing(value: Any, *, allow_empty: bool = False) -> Any:
|
|
583
|
-
if value is None:
|
|
584
|
-
return NOT_AVAILABLE
|
|
585
|
-
if not allow_empty and value == "":
|
|
586
|
-
return NOT_AVAILABLE
|
|
587
|
-
return value
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
591
|
-
empty = {
|
|
592
|
-
"run": 0,
|
|
593
|
-
"failures": 0,
|
|
594
|
-
"errors": 0,
|
|
595
|
-
"skipped": 0,
|
|
596
|
-
"passRate": NOT_AVAILABLE,
|
|
597
|
-
"source": "not-run",
|
|
598
|
-
}
|
|
599
|
-
if not ledger:
|
|
600
|
-
return empty
|
|
601
|
-
validations = ledger.get("validations") or ledger.get("verification") or {}
|
|
602
|
-
unit = validations.get("unitTest") or validations.get("unitTests") or {}
|
|
603
|
-
if not isinstance(unit, dict):
|
|
604
|
-
return empty
|
|
605
|
-
status = str(unit.get("status") or "").upper()
|
|
606
|
-
evidence = unit.get("evidence") or {}
|
|
607
|
-
if isinstance(evidence, dict):
|
|
608
|
-
run = evidence.get("run", unit.get("run", 0))
|
|
609
|
-
failures = evidence.get("failures", unit.get("failures", 0))
|
|
610
|
-
errors = evidence.get("errors", unit.get("errors", 0))
|
|
611
|
-
skipped = evidence.get("skipped", unit.get("skipped", 0))
|
|
612
|
-
pass_rate = evidence.get("passRate") or unit.get("passRate")
|
|
613
|
-
else:
|
|
614
|
-
run = unit.get("run", 0)
|
|
615
|
-
failures = unit.get("failures", 0)
|
|
616
|
-
errors = unit.get("errors", 0)
|
|
617
|
-
skipped = unit.get("skipped", 0)
|
|
618
|
-
pass_rate = unit.get("passRate")
|
|
619
|
-
|
|
620
|
-
source = "committed"
|
|
621
|
-
if status in {"NOT_RUN", "SKIPPED", "USER_SKIPPED"}:
|
|
622
|
-
source = "not-run"
|
|
623
|
-
elif str(unit.get("reused") or "").lower() in {"true", "1"} or "REUSED" in status:
|
|
624
|
-
source = "committed"
|
|
625
|
-
|
|
626
|
-
result = {
|
|
627
|
-
"run": int(run or 0),
|
|
628
|
-
"failures": int(failures or 0),
|
|
629
|
-
"errors": int(errors or 0),
|
|
630
|
-
"skipped": int(skipped or 0),
|
|
631
|
-
"passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
|
|
632
|
-
"source": source,
|
|
633
|
-
}
|
|
634
|
-
if status in {"NOT_RUN", "USER_SKIPPED", "SKIPPED"}:
|
|
635
|
-
result["status"] = status if status != "SKIPPED" else "USER_SKIPPED"
|
|
636
|
-
return result
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _ledger_api_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
|
|
640
|
-
empty = {
|
|
641
|
-
"status": "NOT_RUN",
|
|
642
|
-
"total": 0,
|
|
643
|
-
"passed": 0,
|
|
644
|
-
"failed": 0,
|
|
645
|
-
"blocked": 0,
|
|
646
|
-
"passRate": NOT_AVAILABLE,
|
|
647
|
-
}
|
|
648
|
-
if not ledger:
|
|
649
|
-
return empty
|
|
650
|
-
validations = ledger.get("validations") or ledger.get("verification") or {}
|
|
651
|
-
api = validations.get("apiTest") or validations.get("apiTests") or {}
|
|
652
|
-
if not isinstance(api, dict):
|
|
653
|
-
return empty
|
|
654
|
-
status = str(api.get("status") or "NOT_RUN").upper()
|
|
655
|
-
if status in {"OK", "PASS", "PASSED", "SUCCESS"}:
|
|
656
|
-
status = "OK"
|
|
657
|
-
elif status in {"SKIP", "SKIPPED"}:
|
|
658
|
-
status = "USER_SKIPPED"
|
|
659
|
-
evidence = api.get("evidence") if isinstance(api.get("evidence"), dict) else {}
|
|
660
|
-
total = evidence.get("total", api.get("total", 0))
|
|
661
|
-
passed = evidence.get("passed", api.get("passed", 0))
|
|
662
|
-
failed = evidence.get("failed", api.get("failed", 0))
|
|
663
|
-
blocked = evidence.get("blocked", api.get("blocked", 0))
|
|
664
|
-
pass_rate = evidence.get("passRate") or api.get("passRate")
|
|
665
|
-
return {
|
|
666
|
-
"status": status,
|
|
667
|
-
"total": int(total or 0),
|
|
668
|
-
"passed": int(passed or 0),
|
|
669
|
-
"failed": int(failed or 0),
|
|
670
|
-
"blocked": int(blocked or 0),
|
|
671
|
-
"passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def _ledger_db_compat(ledger: dict[str, Any] | None) -> str:
|
|
676
|
-
if not ledger:
|
|
677
|
-
return "NOT_RUN"
|
|
678
|
-
validations = ledger.get("validations") or {}
|
|
679
|
-
db = validations.get("dbCompatibility") or validations.get("db") or {}
|
|
680
|
-
if isinstance(db, dict):
|
|
681
|
-
return str(db.get("status") or "NOT_RUN").upper()
|
|
682
|
-
if isinstance(db, str) and db.strip():
|
|
683
|
-
return db.strip().upper()
|
|
684
|
-
top = ledger.get("dbCompatibility")
|
|
685
|
-
if isinstance(top, str) and top.strip():
|
|
686
|
-
return top.strip().upper()
|
|
687
|
-
return "NOT_RUN"
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
def _parse_durations_from_log(log_text: str) -> dict[str, Any]:
|
|
691
|
-
"""Best-effort parse of harness-* sections from execution-log."""
|
|
692
|
-
stages: list[dict[str, Any]] = []
|
|
693
|
-
# Match both old hand-written and events-rendered phase headers.
|
|
694
|
-
section_re = re.compile(
|
|
695
|
-
r"(?:###\s*\[\d+\]\s*)?harness-(\w+)|##\s+(plan|run|test|review|submit|merge|archive)\b",
|
|
696
|
-
re.IGNORECASE,
|
|
697
|
-
)
|
|
698
|
-
start_re = re.compile(r"\*\*开始\*\*:\s*(.+)")
|
|
699
|
-
end_re = re.compile(r"\*\*结束\*\*:\s*(.+)")
|
|
700
|
-
dur_re = re.compile(r"\*\*耗时\*\*:\s*(.+)")
|
|
701
|
-
result_re = re.compile(r"\*\*结果\*\*:\s*(.+)")
|
|
702
|
-
|
|
703
|
-
lines = log_text.splitlines()
|
|
704
|
-
i = 0
|
|
705
|
-
while i < len(lines):
|
|
706
|
-
m = section_re.search(lines[i])
|
|
707
|
-
if not m:
|
|
708
|
-
i += 1
|
|
709
|
-
continue
|
|
710
|
-
stage = (m.group(1) or m.group(2) or "").lower()
|
|
711
|
-
skill = f"harness-{stage}" if not stage.startswith("harness") else stage
|
|
712
|
-
started = ended = None
|
|
713
|
-
minutes = 0.0
|
|
714
|
-
result = "OK"
|
|
715
|
-
j = i + 1
|
|
716
|
-
while j < len(lines) and not section_re.search(lines[j]):
|
|
717
|
-
sm = start_re.search(lines[j])
|
|
718
|
-
if sm:
|
|
719
|
-
started = sm.group(1).strip()
|
|
720
|
-
em = end_re.search(lines[j])
|
|
721
|
-
if em:
|
|
722
|
-
ended = em.group(1).strip()
|
|
723
|
-
dm = dur_re.search(lines[j])
|
|
724
|
-
if dm:
|
|
725
|
-
raw = dm.group(1).strip()
|
|
726
|
-
mm = re.search(r"(\d+)\s*分", raw)
|
|
727
|
-
ss = re.search(r"(\d+)\s*秒", raw)
|
|
728
|
-
minutes = (int(mm.group(1)) if mm else 0) + (
|
|
729
|
-
(int(ss.group(1)) if ss else 0) / 60.0
|
|
730
|
-
)
|
|
731
|
-
if minutes == 0:
|
|
732
|
-
ms = re.search(r"([\d.]+)\s*s", raw, re.I)
|
|
733
|
-
if ms:
|
|
734
|
-
minutes = float(ms.group(1)) / 60.0
|
|
735
|
-
rm = result_re.search(lines[j])
|
|
736
|
-
if rm:
|
|
737
|
-
raw_r = rm.group(1)
|
|
738
|
-
if "FAIL" in raw_r or "❌" in raw_r:
|
|
739
|
-
result = "FAIL"
|
|
740
|
-
elif "WARN" in raw_r or "🟡" in raw_r:
|
|
741
|
-
result = "WARN"
|
|
742
|
-
else:
|
|
743
|
-
result = "OK"
|
|
744
|
-
j += 1
|
|
745
|
-
stages.append(
|
|
746
|
-
{
|
|
747
|
-
"stage": stage.replace("harness-", ""),
|
|
748
|
-
"skill": skill if skill.startswith("harness-") else f"harness-{stage}",
|
|
749
|
-
"startedAt": started or NOT_AVAILABLE,
|
|
750
|
-
"endedAt": ended or NOT_AVAILABLE,
|
|
751
|
-
"minutes": round(minutes, 2),
|
|
752
|
-
"result": result,
|
|
753
|
-
}
|
|
754
|
-
)
|
|
755
|
-
i = j
|
|
756
|
-
|
|
757
|
-
total = round(sum(float(s["minutes"]) for s in stages), 2)
|
|
758
|
-
return {
|
|
759
|
-
"totalLabel": f"约 {int(round(total))} 分" if total else "约 0 分",
|
|
760
|
-
"totalMinutes": total,
|
|
761
|
-
"stages": stages,
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
def _skill_calls_from_stages(stages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
766
|
-
counts: dict[str, dict[str, Any]] = {}
|
|
767
|
-
order: list[str] = []
|
|
768
|
-
for s in stages:
|
|
769
|
-
skill = str(s.get("skill") or s.get("stage") or "unknown")
|
|
770
|
-
if skill not in counts:
|
|
771
|
-
counts[skill] = {"skill": skill, "count": 0, "result": s.get("result") or "OK"}
|
|
772
|
-
order.append(skill)
|
|
773
|
-
counts[skill]["count"] += 1
|
|
774
|
-
counts[skill]["result"] = s.get("result") or counts[skill]["result"]
|
|
775
|
-
return [counts[k] for k in order]
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
def _phases_from_events_summary(summary: dict[str, Any]) -> dict[str, Any]:
|
|
779
|
-
phases_out: dict[str, Any] = {}
|
|
780
|
-
for name in ("plan", "run", "test", "review", "submit", "archive"):
|
|
781
|
-
phases_out[name] = {"duration_ms": None, "event_count": 0}
|
|
782
|
-
for name, info in (summary.get("phases") or {}).items():
|
|
783
|
-
key = str(name).lower()
|
|
784
|
-
if key not in phases_out:
|
|
785
|
-
phases_out[key] = {"duration_ms": None, "event_count": 0}
|
|
786
|
-
phases_out[key] = {
|
|
787
|
-
"duration_ms": info.get("duration_ms"),
|
|
788
|
-
"event_count": int(info.get("event_count") or 0),
|
|
789
|
-
}
|
|
790
|
-
return phases_out
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
def _commands_from_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
794
|
-
out: list[dict[str, Any]] = []
|
|
795
|
-
for e in events:
|
|
796
|
-
if e.get("type") != "command":
|
|
797
|
-
continue
|
|
798
|
-
out.append(
|
|
799
|
-
{
|
|
800
|
-
"command": e.get("command") or "",
|
|
801
|
-
"exit_code": e.get("exit_code"),
|
|
802
|
-
"duration_ms": e.get("duration_ms"),
|
|
803
|
-
"phase": e.get("phase"),
|
|
804
|
-
"timestamp": e.get("timestamp"),
|
|
805
|
-
}
|
|
806
|
-
)
|
|
807
|
-
return out
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
def _verification_checks_from_events(
|
|
811
|
-
events: list[dict[str, Any]],
|
|
812
|
-
ledger: dict[str, Any] | None,
|
|
813
|
-
) -> list[dict[str, Any]]:
|
|
814
|
-
checks: list[dict[str, Any]] = []
|
|
815
|
-
seen: set[str] = set()
|
|
816
|
-
for e in events:
|
|
817
|
-
if e.get("type") != "verification":
|
|
818
|
-
continue
|
|
819
|
-
name = str(e.get("name") or "unnamed")
|
|
820
|
-
status = str(e.get("status") or "unknown").lower()
|
|
821
|
-
checks.append(
|
|
822
|
-
{
|
|
823
|
-
"name": name,
|
|
824
|
-
"status": status,
|
|
825
|
-
"command": e.get("command") or "",
|
|
826
|
-
"source": "events.ndjson",
|
|
827
|
-
}
|
|
828
|
-
)
|
|
829
|
-
seen.add(name.lower())
|
|
830
|
-
if ledger:
|
|
831
|
-
validations = ledger.get("validations") or {}
|
|
832
|
-
if isinstance(validations, dict):
|
|
833
|
-
for name, info in validations.items():
|
|
834
|
-
if name.lower() in seen:
|
|
835
|
-
continue
|
|
836
|
-
if not isinstance(info, dict):
|
|
837
|
-
continue
|
|
838
|
-
checks.append(
|
|
839
|
-
{
|
|
840
|
-
"name": name,
|
|
841
|
-
"status": str(info.get("status") or "unknown").lower(),
|
|
842
|
-
"command": str(info.get("command") or ""),
|
|
843
|
-
"source": "evidence/verification-ledger.json",
|
|
844
|
-
}
|
|
845
|
-
)
|
|
846
|
-
return checks
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
def _artifacts_from_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
850
|
-
out: list[dict[str, Any]] = []
|
|
851
|
-
for e in events:
|
|
852
|
-
if e.get("type") != "artifact":
|
|
853
|
-
continue
|
|
854
|
-
out.append(
|
|
855
|
-
{
|
|
856
|
-
"path": e.get("path") or "",
|
|
857
|
-
"kind": e.get("kind") or "",
|
|
858
|
-
"phase": e.get("phase") or "",
|
|
859
|
-
}
|
|
860
|
-
)
|
|
861
|
-
return out
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
def _durations_from_event_phases(event_summary: dict[str, Any]) -> dict[str, Any]:
|
|
865
|
-
stages: list[dict[str, Any]] = []
|
|
866
|
-
total_ms = 0
|
|
867
|
-
for name, info in (event_summary.get("phases") or {}).items():
|
|
868
|
-
dur = info.get("duration_ms")
|
|
869
|
-
minutes = round((dur or 0) / 60000, 2) if dur is not None else 0
|
|
870
|
-
if dur:
|
|
871
|
-
total_ms += int(dur)
|
|
872
|
-
stages.append(
|
|
873
|
-
{
|
|
874
|
-
"stage": str(name),
|
|
875
|
-
"skill": f"harness-{name}",
|
|
876
|
-
"startedAt": info.get("started_at") or NOT_AVAILABLE,
|
|
877
|
-
"endedAt": info.get("ended_at") or NOT_AVAILABLE,
|
|
878
|
-
"minutes": minutes,
|
|
879
|
-
"result": "OK",
|
|
880
|
-
}
|
|
881
|
-
)
|
|
882
|
-
total_min = round(total_ms / 60000, 2)
|
|
883
|
-
return {
|
|
884
|
-
"totalLabel": f"约 {int(round(total_min))} 分",
|
|
885
|
-
"totalMinutes": total_min,
|
|
886
|
-
"stages": stages,
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
def _stage_status_from_sources(
|
|
891
|
-
events: list[dict[str, Any]],
|
|
892
|
-
ledger: dict[str, Any] | None,
|
|
893
|
-
change_dir: Path,
|
|
894
|
-
) -> dict[str, str]:
|
|
895
|
-
status = {
|
|
896
|
-
"plan": "OK",
|
|
897
|
-
"run": "OK",
|
|
898
|
-
"test": "OK",
|
|
899
|
-
"review": "ADVISORY",
|
|
900
|
-
"submit": "OK",
|
|
901
|
-
"archive": "OK",
|
|
902
|
-
}
|
|
903
|
-
# Issues in events can downgrade
|
|
904
|
-
for e in events:
|
|
905
|
-
if e.get("type") != "issue":
|
|
906
|
-
continue
|
|
907
|
-
sev = str(e.get("severity") or "").lower()
|
|
908
|
-
phase = str(e.get("phase") or "").lower()
|
|
909
|
-
if phase in status and sev in {"error", "fail", "failed", "critical"}:
|
|
910
|
-
status[phase] = "FAIL"
|
|
911
|
-
elif phase in status and sev in {"warn", "warning"} and status[phase] == "OK":
|
|
912
|
-
status[phase] = "WARN"
|
|
913
|
-
|
|
914
|
-
api = _ledger_api_tests(ledger)
|
|
915
|
-
db = _ledger_db_compat(ledger)
|
|
916
|
-
if api.get("status") == "USER_SKIPPED":
|
|
917
|
-
status["test"] = "USER_SKIPPED"
|
|
918
|
-
elif db == "BLOCKED_BY_DBA":
|
|
919
|
-
status["test"] = "BLOCKED_BY_DBA"
|
|
920
|
-
elif api.get("status") == "NOT_RUN" and not find_test_reports(change_dir):
|
|
921
|
-
unit = _ledger_unit_tests(ledger)
|
|
922
|
-
if unit.get("source") == "not-run" and unit.get("run", 0) == 0:
|
|
923
|
-
status["test"] = "USER_SKIPPED"
|
|
924
|
-
|
|
925
|
-
if not find_review_reports(change_dir):
|
|
926
|
-
log_text = load_execution_log(change_dir)
|
|
927
|
-
if not re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE):
|
|
928
|
-
status["review"] = "ADVISORY"
|
|
929
|
-
|
|
930
|
-
return status
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
def _compute_final_status(
|
|
934
|
-
stage_status: dict[str, str],
|
|
935
|
-
verification: dict[str, Any],
|
|
936
|
-
) -> str:
|
|
937
|
-
api = verification.get("apiTests") or {}
|
|
938
|
-
db = str(verification.get("dbCompatibility") or "")
|
|
939
|
-
api_status = str(api.get("status") or "")
|
|
940
|
-
if api_status == "USER_SKIPPED" or db == "BLOCKED_BY_DBA":
|
|
941
|
-
return "CONDITIONAL_OK"
|
|
942
|
-
for v in stage_status.values():
|
|
943
|
-
if v == "FAIL":
|
|
944
|
-
return "FAIL"
|
|
945
|
-
unit = verification.get("unitTests") or {}
|
|
946
|
-
if int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0:
|
|
947
|
-
return "FAIL"
|
|
948
|
-
if int(api.get("failed") or 0) > 0:
|
|
949
|
-
return "FAIL"
|
|
950
|
-
for v in stage_status.values():
|
|
951
|
-
if v in {"WARN", "PARTIAL", "USER_SKIPPED", "BLOCKED_BY_DBA"}:
|
|
952
|
-
return "WARN" if v == "WARN" else "CONDITIONAL_OK"
|
|
953
|
-
return "OK"
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
def _changed_files_from_git(
|
|
957
|
-
project: Path,
|
|
958
|
-
base: str | None,
|
|
959
|
-
head: str | None,
|
|
960
|
-
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
961
|
-
diff_stat = {
|
|
962
|
-
"filesChanged": 0,
|
|
963
|
-
"insertions": 0,
|
|
964
|
-
"deletions": 0,
|
|
965
|
-
"range": NOT_AVAILABLE,
|
|
966
|
-
}
|
|
967
|
-
changed: list[dict[str, Any]] = []
|
|
968
|
-
if not base or not head:
|
|
969
|
-
return diff_stat, changed
|
|
970
|
-
rng = f"{base}..{head}"
|
|
971
|
-
code, out, _ = git_run(project, "diff", "--numstat", rng)
|
|
972
|
-
if code != 0 or not out:
|
|
973
|
-
diff_stat["range"] = rng
|
|
974
|
-
return diff_stat, changed
|
|
975
|
-
insertions = deletions = 0
|
|
976
|
-
files = 0
|
|
977
|
-
for line in out.splitlines():
|
|
978
|
-
parts = line.split("\t")
|
|
979
|
-
if len(parts) < 3:
|
|
980
|
-
continue
|
|
981
|
-
ins_s, del_s, path = parts[0], parts[1], parts[2]
|
|
982
|
-
ins = int(ins_s) if ins_s.isdigit() else 0
|
|
983
|
-
dele = int(del_s) if del_s.isdigit() else 0
|
|
984
|
-
insertions += ins
|
|
985
|
-
deletions += dele
|
|
986
|
-
files += 1
|
|
987
|
-
changed.append(
|
|
988
|
-
{
|
|
989
|
-
"path": path,
|
|
990
|
-
"summary": "",
|
|
991
|
-
"insertions": ins,
|
|
992
|
-
"deletions": dele,
|
|
993
|
-
}
|
|
994
|
-
)
|
|
995
|
-
diff_stat = {
|
|
996
|
-
"filesChanged": files,
|
|
997
|
-
"insertions": insertions,
|
|
998
|
-
"deletions": deletions,
|
|
999
|
-
"range": rng,
|
|
1000
|
-
}
|
|
1001
|
-
return diff_stat, changed
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
def _review_summary(change_dir: Path, existing: dict[str, Any] | None) -> dict[str, Any]:
|
|
1005
|
-
base = {
|
|
1006
|
-
"status": "ADVISORY",
|
|
1007
|
-
"red": 0,
|
|
1008
|
-
"yellow": 0,
|
|
1009
|
-
"redFixed": 0,
|
|
1010
|
-
"redConfirmed": 0,
|
|
1011
|
-
"yellowFixed": 0,
|
|
1012
|
-
"yellowDeferred": 0,
|
|
1013
|
-
"summary": "",
|
|
1014
|
-
}
|
|
1015
|
-
if existing and isinstance(existing.get("reviewSummary"), dict):
|
|
1016
|
-
merged = dict(base)
|
|
1017
|
-
merged.update(existing["reviewSummary"])
|
|
1018
|
-
return merged
|
|
1019
|
-
if not find_review_reports(change_dir):
|
|
1020
|
-
log_text = load_execution_log(change_dir)
|
|
1021
|
-
if not re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE):
|
|
1022
|
-
base["status"] = "ADVISORY_NOT_RUN"
|
|
1023
|
-
return base
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
def collect_summary_data(
|
|
1027
|
-
change_dir: Path,
|
|
1028
|
-
*,
|
|
1029
|
-
before_manifest: dict[str, Any] | None = None,
|
|
1030
|
-
after_manifest: dict[str, Any] | None = None,
|
|
1031
|
-
compare_result: dict[str, Any] | None = None,
|
|
1032
|
-
write: bool = True,
|
|
1033
|
-
for_replay: bool = False,
|
|
1034
|
-
) -> dict[str, Any]:
|
|
1035
|
-
"""Build schema 2.2 summary-data from events/ledger/log/manifest/reports."""
|
|
1036
|
-
template = load_template()
|
|
1037
|
-
data = _deepcopy_json(template)
|
|
1038
|
-
# Clear placeholder strings from template
|
|
1039
|
-
data["schemaVersion"] = SCHEMA_VERSION
|
|
1040
|
-
change_name = infer_change_name(change_dir)
|
|
1041
|
-
data["changeName"] = change_name
|
|
1042
|
-
|
|
1043
|
-
sources: list[str] = []
|
|
1044
|
-
events: list[dict[str, Any]] = []
|
|
1045
|
-
events_file = he.events_path(change_dir)
|
|
1046
|
-
has_events = events_file.is_file() and events_file.stat().st_size > 0
|
|
1047
|
-
if has_events:
|
|
1048
|
-
try:
|
|
1049
|
-
events = he.load_events(events_file)
|
|
1050
|
-
sources.append("events.ndjson")
|
|
1051
|
-
except ValueError:
|
|
1052
|
-
events = []
|
|
1053
|
-
|
|
1054
|
-
ledger = load_ledger(change_dir)
|
|
1055
|
-
if ledger:
|
|
1056
|
-
sources.append("evidence/verification-ledger.json")
|
|
1057
|
-
|
|
1058
|
-
log_text = load_execution_log(change_dir)
|
|
1059
|
-
if log_text:
|
|
1060
|
-
sources.append("logs/execution-log.md")
|
|
1061
|
-
|
|
1062
|
-
existing = load_existing_summary(change_dir)
|
|
1063
|
-
if existing and for_replay:
|
|
1064
|
-
sources.append("reports/final/summary-data.json")
|
|
1065
|
-
|
|
1066
|
-
# Prefer existing summary fields when replaying old archives (golden-stable).
|
|
1067
|
-
if for_replay and existing:
|
|
1068
|
-
for key in (
|
|
1069
|
-
"businessGoal",
|
|
1070
|
-
"finalStatus",
|
|
1071
|
-
"finalCommit",
|
|
1072
|
-
"finalCommitBranch",
|
|
1073
|
-
"baseCommit",
|
|
1074
|
-
"diffStat",
|
|
1075
|
-
"stageStatus",
|
|
1076
|
-
"durations",
|
|
1077
|
-
"skillCalls",
|
|
1078
|
-
"verification",
|
|
1079
|
-
"timeline",
|
|
1080
|
-
"changedFiles",
|
|
1081
|
-
"artifacts",
|
|
1082
|
-
"reviewSummary",
|
|
1083
|
-
"archiveManifest",
|
|
1084
|
-
"uncommittedTestEvidence",
|
|
1085
|
-
"maintenanceNotes",
|
|
1086
|
-
"knownRisks",
|
|
1087
|
-
"manualActions",
|
|
1088
|
-
):
|
|
1089
|
-
if key in existing:
|
|
1090
|
-
data[key] = _deepcopy_json(existing[key])
|
|
1091
|
-
|
|
1092
|
-
event_summary = he.build_summary(change_dir, events) if events else {
|
|
1093
|
-
"ok": True,
|
|
1094
|
-
"event_count": 0,
|
|
1095
|
-
"phases": {},
|
|
1096
|
-
"issues": [],
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
# businessGoal
|
|
1100
|
-
if not data.get("businessGoal") or str(data.get("businessGoal")).startswith("本次"):
|
|
1101
|
-
if existing and existing.get("businessGoal"):
|
|
1102
|
-
data["businessGoal"] = existing["businessGoal"]
|
|
1103
|
-
else:
|
|
1104
|
-
data["businessGoal"] = NOT_AVAILABLE if for_replay else ""
|
|
1105
|
-
|
|
1106
|
-
# commits
|
|
1107
|
-
project = find_project_root(change_dir)
|
|
1108
|
-
if not data.get("finalCommit") or str(data.get("finalCommit")).startswith("<"):
|
|
1109
|
-
code, head, _ = git_run(project, "rev-parse", "HEAD")
|
|
1110
|
-
if code == 0 and head:
|
|
1111
|
-
data["finalCommit"] = head
|
|
1112
|
-
elif existing and existing.get("finalCommit"):
|
|
1113
|
-
data["finalCommit"] = existing["finalCommit"]
|
|
1114
|
-
else:
|
|
1115
|
-
data["finalCommit"] = NOT_AVAILABLE if for_replay else ""
|
|
1116
|
-
|
|
1117
|
-
if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
|
|
1118
|
-
if ledger and ledger.get("baseCommit"):
|
|
1119
|
-
data["baseCommit"] = ledger["baseCommit"]
|
|
1120
|
-
elif existing and existing.get("baseCommit"):
|
|
1121
|
-
data["baseCommit"] = existing["baseCommit"]
|
|
1122
|
-
else:
|
|
1123
|
-
data["baseCommit"] = NOT_AVAILABLE if for_replay else ""
|
|
1124
|
-
|
|
1125
|
-
if not data.get("finalCommitBranch") or str(data.get("finalCommitBranch")).startswith("<"):
|
|
1126
|
-
code, branch, _ = git_run(project, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
|
1127
|
-
if code == 0 and branch:
|
|
1128
|
-
data["finalCommitBranch"] = branch
|
|
1129
|
-
elif existing and existing.get("finalCommitBranch"):
|
|
1130
|
-
data["finalCommitBranch"] = existing["finalCommitBranch"]
|
|
1131
|
-
else:
|
|
1132
|
-
data["finalCommitBranch"] = NOT_AVAILABLE if for_replay else ""
|
|
1133
|
-
|
|
1134
|
-
# verification
|
|
1135
|
-
if not for_replay or not isinstance(data.get("verification"), dict):
|
|
1136
|
-
unit = _ledger_unit_tests(ledger)
|
|
1137
|
-
api = _ledger_api_tests(ledger)
|
|
1138
|
-
db = _ledger_db_compat(ledger)
|
|
1139
|
-
if not find_test_reports(change_dir) and unit.get("run", 0) == 0:
|
|
1140
|
-
if "status" not in unit:
|
|
1141
|
-
unit["status"] = "NOT_RUN"
|
|
1142
|
-
if api.get("status") in {"", "OK"} and api.get("total", 0) == 0:
|
|
1143
|
-
api["status"] = "NOT_RUN"
|
|
1144
|
-
coverage = NOT_AVAILABLE
|
|
1145
|
-
if existing and isinstance(existing.get("verification"), dict):
|
|
1146
|
-
coverage = existing["verification"].get("coverageDisplay", coverage)
|
|
1147
|
-
data["verification"] = {
|
|
1148
|
-
"unitTests": unit,
|
|
1149
|
-
"apiTests": api,
|
|
1150
|
-
"dbCompatibility": db,
|
|
1151
|
-
"coverageDisplay": coverage,
|
|
1152
|
-
}
|
|
1153
|
-
else:
|
|
1154
|
-
# Ensure nested keys exist for old schema 2.1
|
|
1155
|
-
ver = data.setdefault("verification", {})
|
|
1156
|
-
ver.setdefault("unitTests", {})
|
|
1157
|
-
ver.setdefault("apiTests", {})
|
|
1158
|
-
ver.setdefault("dbCompatibility", ver.get("dbCompatibility", NOT_AVAILABLE))
|
|
1159
|
-
ver.setdefault("coverageDisplay", ver.get("coverageDisplay", NOT_AVAILABLE))
|
|
1160
|
-
|
|
1161
|
-
# stageStatus / finalStatus
|
|
1162
|
-
if not for_replay or not isinstance(data.get("stageStatus"), dict):
|
|
1163
|
-
data["stageStatus"] = _stage_status_from_sources(events, ledger, change_dir)
|
|
1164
|
-
if not for_replay or not data.get("finalStatus") or str(data.get("finalStatus")).startswith("OK |"):
|
|
1165
|
-
data["finalStatus"] = _compute_final_status(
|
|
1166
|
-
data.get("stageStatus") or {},
|
|
1167
|
-
data.get("verification") or {},
|
|
1168
|
-
)
|
|
1169
|
-
|
|
1170
|
-
# durations / skillCalls
|
|
1171
|
-
if events:
|
|
1172
|
-
data["durations"] = _durations_from_event_phases(event_summary)
|
|
1173
|
-
data["skillCalls"] = _skill_calls_from_stages(data["durations"].get("stages") or [])
|
|
1174
|
-
elif log_text and (not for_replay or not data.get("durations")):
|
|
1175
|
-
data["durations"] = _parse_durations_from_log(log_text)
|
|
1176
|
-
data["skillCalls"] = _skill_calls_from_stages(data["durations"].get("stages") or [])
|
|
1177
|
-
elif not data.get("durations"):
|
|
1178
|
-
data["durations"] = {
|
|
1179
|
-
"totalLabel": NOT_AVAILABLE,
|
|
1180
|
-
"totalMinutes": 0,
|
|
1181
|
-
"stages": [],
|
|
1182
|
-
}
|
|
1183
|
-
data["skillCalls"] = []
|
|
1184
|
-
|
|
1185
|
-
# diffStat / changedFiles
|
|
1186
|
-
if not for_replay or not data.get("changedFiles"):
|
|
1187
|
-
base = data.get("baseCommit")
|
|
1188
|
-
head = data.get("finalCommit")
|
|
1189
|
-
if base and head and base != NOT_AVAILABLE and head != NOT_AVAILABLE:
|
|
1190
|
-
diff_stat, changed = _changed_files_from_git(project, str(base), str(head))
|
|
1191
|
-
if changed:
|
|
1192
|
-
data["diffStat"] = diff_stat
|
|
1193
|
-
data["changedFiles"] = changed
|
|
1194
|
-
elif existing and existing.get("changedFiles"):
|
|
1195
|
-
data["changedFiles"] = existing["changedFiles"]
|
|
1196
|
-
data["diffStat"] = existing.get("diffStat") or diff_stat
|
|
1197
|
-
else:
|
|
1198
|
-
data["diffStat"] = diff_stat
|
|
1199
|
-
data["changedFiles"] = []
|
|
1200
|
-
elif existing and existing.get("changedFiles"):
|
|
1201
|
-
data["changedFiles"] = existing["changedFiles"]
|
|
1202
|
-
data["diffStat"] = existing.get("diffStat") or {
|
|
1203
|
-
"filesChanged": len(existing["changedFiles"]),
|
|
1204
|
-
"insertions": 0,
|
|
1205
|
-
"deletions": 0,
|
|
1206
|
-
"range": NOT_AVAILABLE,
|
|
1207
|
-
}
|
|
1208
|
-
else:
|
|
1209
|
-
data["diffStat"] = {
|
|
1210
|
-
"filesChanged": 0,
|
|
1211
|
-
"insertions": 0,
|
|
1212
|
-
"deletions": 0,
|
|
1213
|
-
"range": NOT_AVAILABLE if for_replay else "",
|
|
1214
|
-
}
|
|
1215
|
-
data["changedFiles"] = []
|
|
1216
|
-
|
|
1217
|
-
# artifacts (build products stay empty unless already known; reportPipeline has event artifacts)
|
|
1218
|
-
if not isinstance(data.get("artifacts"), list):
|
|
1219
|
-
data["artifacts"] = []
|
|
1220
|
-
if not for_replay:
|
|
1221
|
-
# Keep empty placeholder for build artifacts; event artifacts go to reportPipeline
|
|
1222
|
-
pass
|
|
1223
|
-
|
|
1224
|
-
data["reviewSummary"] = _review_summary(change_dir, existing if for_replay else None)
|
|
1225
|
-
data.setdefault("timeline", [])
|
|
1226
|
-
data.setdefault("uncommittedTestEvidence", [])
|
|
1227
|
-
|
|
1228
|
-
# Model-owned fields: leave empty placeholders on finalize; preserve on replay
|
|
1229
|
-
if not for_replay:
|
|
1230
|
-
data["maintenanceNotes"] = []
|
|
1231
|
-
data["knownRisks"] = []
|
|
1232
|
-
data["manualActions"] = []
|
|
1233
|
-
else:
|
|
1234
|
-
data.setdefault("maintenanceNotes", [])
|
|
1235
|
-
data.setdefault("knownRisks", [])
|
|
1236
|
-
data.setdefault("manualActions", [])
|
|
1237
|
-
|
|
1238
|
-
# archiveManifest
|
|
1239
|
-
am = {
|
|
1240
|
-
"movedFiles": 0,
|
|
1241
|
-
"generatedFiles": 0,
|
|
1242
|
-
"totalArchiveFiles": 0,
|
|
1243
|
-
"checksumStatus": "OK",
|
|
1244
|
-
}
|
|
1245
|
-
if compare_result:
|
|
1246
|
-
am["movedFiles"] = compare_result.get("movedFiles", 0)
|
|
1247
|
-
am["generatedFiles"] = compare_result.get("generatedFiles", 0)
|
|
1248
|
-
am["totalArchiveFiles"] = compare_result.get("totalArchiveFiles", 0)
|
|
1249
|
-
am["checksumStatus"] = compare_result.get("checksumStatus", "OK")
|
|
1250
|
-
elif after_manifest:
|
|
1251
|
-
am["totalArchiveFiles"] = int(after_manifest.get("fileCount") or 0)
|
|
1252
|
-
elif before_manifest:
|
|
1253
|
-
am["totalArchiveFiles"] = int(before_manifest.get("fileCount") or 0)
|
|
1254
|
-
elif for_replay and isinstance(data.get("archiveManifest"), dict):
|
|
1255
|
-
am = {**am, **data["archiveManifest"]}
|
|
1256
|
-
data["archiveManifest"] = am
|
|
1257
|
-
|
|
1258
|
-
# reportPipeline
|
|
1259
|
-
commands = _commands_from_events(events)
|
|
1260
|
-
if not commands and for_replay:
|
|
1261
|
-
# Cannot invent commands
|
|
1262
|
-
pass
|
|
1263
|
-
verification_checks = _verification_checks_from_events(events, ledger)
|
|
1264
|
-
pipeline_artifacts = _artifacts_from_events(events)
|
|
1265
|
-
if not sources:
|
|
1266
|
-
sources = [NOT_AVAILABLE]
|
|
1267
|
-
|
|
1268
|
-
data["reportPipeline"] = {
|
|
1269
|
-
"schema_version": 1,
|
|
1270
|
-
"generated_at": now_iso(),
|
|
1271
|
-
"event_count": len(events),
|
|
1272
|
-
"sources": sources,
|
|
1273
|
-
"phases": _phases_from_events_summary(event_summary),
|
|
1274
|
-
"commands": commands,
|
|
1275
|
-
"verificationChecks": verification_checks,
|
|
1276
|
-
"artifacts": pipeline_artifacts,
|
|
1277
|
-
"validationIssues": [],
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
# Fill any remaining template placeholders that look like instructions
|
|
1281
|
-
for key in list(data.keys()):
|
|
1282
|
-
val = data[key]
|
|
1283
|
-
if isinstance(val, str) and ("|" in val and "OK" in val and len(val) < 80):
|
|
1284
|
-
# template enum hint left behind
|
|
1285
|
-
if for_replay:
|
|
1286
|
-
data[key] = existing.get(key, NOT_AVAILABLE) if existing else NOT_AVAILABLE
|
|
1287
|
-
|
|
1288
|
-
if write:
|
|
1289
|
-
out_path = change_dir / "reports" / "final" / "summary-data.json"
|
|
1290
|
-
write_json(out_path, data)
|
|
1291
|
-
|
|
1292
|
-
return data
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
# ---------------------------------------------------------------------------
|
|
1296
|
-
# render
|
|
1297
|
-
# ---------------------------------------------------------------------------
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
def resolve_node_path(project_root: Path) -> str | None:
|
|
1301
|
-
profile = project_root / ".harness" / "config" / "build-profile.json"
|
|
1302
|
-
if profile.is_file():
|
|
1303
|
-
try:
|
|
1304
|
-
data = read_json(profile)
|
|
1305
|
-
node = (data.get("toolPaths") or {}).get("node")
|
|
1306
|
-
if node and Path(str(node)).exists():
|
|
1307
|
-
return str(node)
|
|
1308
|
-
except (OSError, json.JSONDecodeError, TypeError):
|
|
1309
|
-
pass
|
|
1310
|
-
return shutil.which("node")
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
def render_fallback_html(summary: dict[str, Any]) -> str:
|
|
1314
|
-
"""Render escaped, deterministic HTML with every validate-required fact.
|
|
1315
|
-
|
|
1316
|
-
Used when the Node renderer is unavailable or fails. No timestamps / random
|
|
1317
|
-
data; all dynamic values are HTML-escaped via _html_escape.
|
|
1318
|
-
"""
|
|
1319
|
-
def esc(v: Any) -> str:
|
|
1320
|
-
return _html_escape("" if v is None else str(v))
|
|
1321
|
-
|
|
1322
|
-
parts: list[str] = [
|
|
1323
|
-
"<!DOCTYPE html>",
|
|
1324
|
-
'<html lang="zh-CN"><head><meta charset="utf-8">',
|
|
1325
|
-
"<title>harness final-summary (python fallback)</title>",
|
|
1326
|
-
"</head><body>",
|
|
1327
|
-
"<h1>变更最终报告(Python fallback 渲染)</h1>",
|
|
1328
|
-
f'<h2 id="changeName">{esc(summary.get("changeName"))}</h2>',
|
|
1329
|
-
f'<p><strong>finalStatus</strong>: '
|
|
1330
|
-
f'<span id="finalStatus">{esc(summary.get("finalStatus"))}</span></p>',
|
|
1331
|
-
]
|
|
1332
|
-
|
|
1333
|
-
pipeline = summary.get("reportPipeline") or {}
|
|
1334
|
-
cmds = pipeline.get("commands") or []
|
|
1335
|
-
parts.append("<h3>Commands</h3><ul>")
|
|
1336
|
-
for c in cmds:
|
|
1337
|
-
parts.append(
|
|
1338
|
-
f"<li><code>{esc(c.get('command'))}</code> "
|
|
1339
|
-
f"exitCode={esc(c.get('exit_code'))}</li>"
|
|
1340
|
-
)
|
|
1341
|
-
parts.append("</ul>")
|
|
1342
|
-
|
|
1343
|
-
ver = summary.get("verification") or {}
|
|
1344
|
-
unit = ver.get("unitTests") or {}
|
|
1345
|
-
api = ver.get("apiTests") or {}
|
|
1346
|
-
parts.append("<h3>Verification</h3>")
|
|
1347
|
-
parts.append(
|
|
1348
|
-
"<p>unitTests: run={run} failures={failures} errors={errors} "
|
|
1349
|
-
"skipped={skipped} passRate={passRate} status={status}</p>".format(
|
|
1350
|
-
run=esc(unit.get("run")),
|
|
1351
|
-
failures=esc(unit.get("failures")),
|
|
1352
|
-
errors=esc(unit.get("errors")),
|
|
1353
|
-
skipped=esc(unit.get("skipped")),
|
|
1354
|
-
passRate=esc(unit.get("passRate")),
|
|
1355
|
-
status=esc(unit.get("status")),
|
|
1356
|
-
)
|
|
1357
|
-
)
|
|
1358
|
-
parts.append(
|
|
1359
|
-
"<p>apiTests: status={status} total={total} passed={passed} "
|
|
1360
|
-
"failed={failed} blocked={blocked}</p>".format(
|
|
1361
|
-
status=esc(api.get("status")),
|
|
1362
|
-
total=esc(api.get("total")),
|
|
1363
|
-
passed=esc(api.get("passed")),
|
|
1364
|
-
failed=esc(api.get("failed")),
|
|
1365
|
-
blocked=esc(api.get("blocked")),
|
|
1366
|
-
)
|
|
1367
|
-
)
|
|
1368
|
-
parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
|
|
1369
|
-
|
|
1370
|
-
parts.append("<h3>Changed Files</h3><ul>")
|
|
1371
|
-
for f in summary.get("changedFiles") or []:
|
|
1372
|
-
parts.append(
|
|
1373
|
-
f"<li>{esc(f.get('path'))} +{esc(f.get('insertions'))} "
|
|
1374
|
-
f"-{esc(f.get('deletions'))}</li>"
|
|
1375
|
-
)
|
|
1376
|
-
parts.append("</ul>")
|
|
1377
|
-
|
|
1378
|
-
am = summary.get("archiveManifest") or {}
|
|
1379
|
-
parts.append("<h3>Archive Manifest</h3>")
|
|
1380
|
-
parts.append(
|
|
1381
|
-
"<p>movedFiles={moved} generatedFiles={gen} totalArchiveFiles={total} "
|
|
1382
|
-
"checksumStatus={cs}</p>".format(
|
|
1383
|
-
moved=esc(am.get("movedFiles")),
|
|
1384
|
-
gen=esc(am.get("generatedFiles")),
|
|
1385
|
-
total=esc(am.get("totalArchiveFiles")),
|
|
1386
|
-
cs=esc(am.get("checksumStatus")),
|
|
1387
|
-
)
|
|
1388
|
-
)
|
|
1389
|
-
|
|
1390
|
-
def _list_section(title: str, items: Any) -> None:
|
|
1391
|
-
parts.append(f"<h3>{esc(title)}</h3><ul>")
|
|
1392
|
-
for it in items or []:
|
|
1393
|
-
parts.append(f"<li>{esc(it)}</li>")
|
|
1394
|
-
parts.append("</ul>")
|
|
1395
|
-
|
|
1396
|
-
_list_section("Known Risks", summary.get("knownRisks"))
|
|
1397
|
-
_list_section("Manual Actions", summary.get("manualActions"))
|
|
1398
|
-
_list_section("Maintenance Notes", summary.get("maintenanceNotes"))
|
|
1399
|
-
|
|
1400
|
-
parts.append("</body></html>")
|
|
1401
|
-
return "\n".join(parts) + "\n"
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
def render_final_summary(
|
|
1405
|
-
change_dir: Path,
|
|
1406
|
-
summary_path: Path,
|
|
1407
|
-
) -> dict[str, Any]:
|
|
1408
|
-
"""Render final-summary.html via Node; fall back to a Python renderer.
|
|
1409
|
-
|
|
1410
|
-
Returns ``{ok, renderer, fallbackReason, out_path}``:
|
|
1411
|
-
- Node success -> renderer="node".
|
|
1412
|
-
- Node unavailable/timeout/non-zero/no-file -> Python fallback; success ->
|
|
1413
|
-
renderer="python-fallback" (fallbackReason carries the node failure cause).
|
|
1414
|
-
- Both fail or produce no file -> ok=False (caller must restore + exit non-0).
|
|
1415
|
-
"""
|
|
1416
|
-
out_path = change_dir / "reports" / "final" / "final-summary.html"
|
|
1417
|
-
project = find_project_root(change_dir)
|
|
1418
|
-
node = resolve_node_path(project)
|
|
1419
|
-
fallback_reason = ""
|
|
1420
|
-
|
|
1421
|
-
if node and RENDER_SCRIPT.is_file():
|
|
1422
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1423
|
-
try:
|
|
1424
|
-
proc = subprocess.run(
|
|
1425
|
-
[
|
|
1426
|
-
node,
|
|
1427
|
-
str(RENDER_SCRIPT),
|
|
1428
|
-
"--summary",
|
|
1429
|
-
str(summary_path),
|
|
1430
|
-
"--out",
|
|
1431
|
-
str(out_path),
|
|
1432
|
-
],
|
|
1433
|
-
capture_output=True,
|
|
1434
|
-
text=True,
|
|
1435
|
-
encoding="utf-8",
|
|
1436
|
-
errors="replace",
|
|
1437
|
-
timeout=60,
|
|
1438
|
-
check=False,
|
|
1439
|
-
)
|
|
1440
|
-
if proc.returncode == 0 and out_path.is_file():
|
|
1441
|
-
return {
|
|
1442
|
-
"ok": True,
|
|
1443
|
-
"renderer": "node",
|
|
1444
|
-
"fallbackReason": "",
|
|
1445
|
-
"out_path": str(out_path),
|
|
1446
|
-
}
|
|
1447
|
-
fallback_reason = (
|
|
1448
|
-
f"node render exit {proc.returncode}: "
|
|
1449
|
-
f"{(proc.stderr or proc.stdout or '')[:200]}"
|
|
1450
|
-
)
|
|
1451
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
1452
|
-
fallback_reason = f"node render failed: {exc}"
|
|
1453
|
-
else:
|
|
1454
|
-
fallback_reason = (
|
|
1455
|
-
"node unavailable" if not node else f"renderer missing: {RENDER_SCRIPT}"
|
|
1456
|
-
)
|
|
1457
|
-
|
|
1458
|
-
# Python fallback
|
|
1459
|
-
try:
|
|
1460
|
-
summary = read_json(summary_path)
|
|
1461
|
-
html = render_fallback_html(summary)
|
|
1462
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1463
|
-
out_path.write_text(html, encoding="utf-8", newline="\n")
|
|
1464
|
-
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
1465
|
-
return {
|
|
1466
|
-
"ok": False,
|
|
1467
|
-
"renderer": "none",
|
|
1468
|
-
"fallbackReason": f"{fallback_reason}; fallback failed: {exc}",
|
|
1469
|
-
"out_path": str(out_path),
|
|
1470
|
-
}
|
|
1471
|
-
if out_path.is_file():
|
|
1472
|
-
return {
|
|
1473
|
-
"ok": True,
|
|
1474
|
-
"renderer": "python-fallback",
|
|
1475
|
-
"fallbackReason": fallback_reason,
|
|
1476
|
-
"out_path": str(out_path),
|
|
1477
|
-
}
|
|
1478
|
-
return {
|
|
1479
|
-
"ok": False,
|
|
1480
|
-
"renderer": "none",
|
|
1481
|
-
"fallbackReason": f"{fallback_reason}; fallback produced no file",
|
|
1482
|
-
"out_path": str(out_path),
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
# ---------------------------------------------------------------------------
|
|
1487
|
-
# validate
|
|
1488
|
-
# ---------------------------------------------------------------------------
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
def validate_summary(
|
|
1492
|
-
summary: dict[str, Any],
|
|
1493
|
-
html_path: Path | None,
|
|
1494
|
-
*,
|
|
1495
|
-
render_skipped: bool = False,
|
|
1496
|
-
) -> dict[str, Any]:
|
|
1497
|
-
"""Validate final-summary covers summary-data key facts (in-process)."""
|
|
1498
|
-
issues: list[dict[str, str]] = []
|
|
1499
|
-
|
|
1500
|
-
change_id = str(summary.get("changeName") or "")
|
|
1501
|
-
html = ""
|
|
1502
|
-
if html_path and html_path.is_file():
|
|
1503
|
-
try:
|
|
1504
|
-
html = html_path.read_text(encoding="utf-8-sig")
|
|
1505
|
-
except OSError as exc:
|
|
1506
|
-
issues.append(
|
|
1507
|
-
{
|
|
1508
|
-
"code": "missing-final-report",
|
|
1509
|
-
"severity": "error",
|
|
1510
|
-
"message": f"cannot read final-summary: {exc}",
|
|
1511
|
-
}
|
|
1512
|
-
)
|
|
1513
|
-
else:
|
|
1514
|
-
# Task 2 (§4.1 rule 5): 不再存在"没有 HTML 但只 warning"的分支。
|
|
1515
|
-
# 缺 final-summary 恒为 error;finalize 会 restore + exit 非 0。
|
|
1516
|
-
issues.append(
|
|
1517
|
-
{
|
|
1518
|
-
"code": "missing-final-report",
|
|
1519
|
-
"severity": "error",
|
|
1520
|
-
"message": "reports/final/final-summary.html not found",
|
|
1521
|
-
}
|
|
1522
|
-
)
|
|
1523
|
-
|
|
1524
|
-
def has_text(needle: str) -> bool:
|
|
1525
|
-
if not needle or needle == NOT_AVAILABLE:
|
|
1526
|
-
return True
|
|
1527
|
-
return needle in html
|
|
1528
|
-
|
|
1529
|
-
# change id
|
|
1530
|
-
if html and change_id and not has_text(change_id):
|
|
1531
|
-
issues.append(
|
|
1532
|
-
{
|
|
1533
|
-
"code": "missing-change-id",
|
|
1534
|
-
"severity": "error",
|
|
1535
|
-
"message": f"final-summary missing change id '{change_id}'",
|
|
1536
|
-
}
|
|
1537
|
-
)
|
|
1538
|
-
|
|
1539
|
-
# key commands — stock render-summary.mjs does not embed reportPipeline.commands.
|
|
1540
|
-
# Require commands to be present in summary-data; HTML absence is warning only.
|
|
1541
|
-
commands = (summary.get("reportPipeline") or {}).get("commands") or []
|
|
1542
|
-
if html:
|
|
1543
|
-
for cmd in commands[:8]:
|
|
1544
|
-
c = str(cmd.get("command") or "").strip()
|
|
1545
|
-
if not c:
|
|
1546
|
-
continue
|
|
1547
|
-
fragment = c if len(c) <= 60 else c[:60]
|
|
1548
|
-
token = c.split()[-1] if c.split() else c
|
|
1549
|
-
in_html = (
|
|
1550
|
-
fragment in html
|
|
1551
|
-
or _html_escape(fragment) in html
|
|
1552
|
-
or (len(token) >= 4 and (token in html or _html_escape(token) in html))
|
|
1553
|
-
)
|
|
1554
|
-
if not in_html:
|
|
1555
|
-
issues.append(
|
|
1556
|
-
{
|
|
1557
|
-
"code": "missing-command",
|
|
1558
|
-
"severity": "warning",
|
|
1559
|
-
"message": (
|
|
1560
|
-
f"final-summary HTML omits command (renderer may not "
|
|
1561
|
-
f"embed commands): {c}"
|
|
1562
|
-
),
|
|
1563
|
-
}
|
|
1564
|
-
)
|
|
1565
|
-
|
|
1566
|
-
# verification
|
|
1567
|
-
ver = summary.get("verification") or {}
|
|
1568
|
-
unit = ver.get("unitTests") or {}
|
|
1569
|
-
api = ver.get("apiTests") or {}
|
|
1570
|
-
pass_rate = unit.get("passRate")
|
|
1571
|
-
if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
|
|
1572
|
-
issues.append(
|
|
1573
|
-
{
|
|
1574
|
-
"code": "missing-verification",
|
|
1575
|
-
"severity": "warning",
|
|
1576
|
-
"message": f"unitTests.passRate '{pass_rate}' not in final-summary",
|
|
1577
|
-
}
|
|
1578
|
-
)
|
|
1579
|
-
api_status = str(api.get("status") or "")
|
|
1580
|
-
if api_status and api_status not in {"", NOT_AVAILABLE} and api_status not in html:
|
|
1581
|
-
# Renderer shows api status; soft warning
|
|
1582
|
-
if api_status in {"USER_SKIPPED", "BLOCKED_BY_DBA", "NOT_RUN", "PARTIAL"}:
|
|
1583
|
-
issues.append(
|
|
1584
|
-
{
|
|
1585
|
-
"code": "missing-verification",
|
|
1586
|
-
"severity": "warning",
|
|
1587
|
-
"message": f"apiTests.status '{api_status}' not visible in final-summary",
|
|
1588
|
-
}
|
|
1589
|
-
)
|
|
1590
|
-
|
|
1591
|
-
# artifacts / summary-data path hints
|
|
1592
|
-
am = summary.get("archiveManifest") or {}
|
|
1593
|
-
total = am.get("totalArchiveFiles")
|
|
1594
|
-
if total is not None and str(total) not in html:
|
|
1595
|
-
issues.append(
|
|
1596
|
-
{
|
|
1597
|
-
"code": "missing-artifact",
|
|
1598
|
-
"severity": "warning",
|
|
1599
|
-
"message": "archiveManifest.totalArchiveFiles not reflected in final-summary",
|
|
1600
|
-
}
|
|
1601
|
-
)
|
|
1602
|
-
|
|
1603
|
-
# risks / manual actions — empty arrays are OK (placeholder)
|
|
1604
|
-
for risk in summary.get("knownRisks") or []:
|
|
1605
|
-
text = str(risk)
|
|
1606
|
-
if text and text not in html and _html_escape(text) not in html:
|
|
1607
|
-
issues.append(
|
|
1608
|
-
{
|
|
1609
|
-
"code": "missing-risk",
|
|
1610
|
-
"severity": "warning",
|
|
1611
|
-
"message": f"knownRisk not in final-summary: {text[:80]}",
|
|
1612
|
-
}
|
|
1613
|
-
)
|
|
1614
|
-
|
|
1615
|
-
# status contradiction
|
|
1616
|
-
final_status = str(summary.get("finalStatus") or "")
|
|
1617
|
-
has_skip = api_status == "USER_SKIPPED" or str(
|
|
1618
|
-
ver.get("dbCompatibility") or ""
|
|
1619
|
-
) == "BLOCKED_BY_DBA"
|
|
1620
|
-
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
1621
|
-
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
1622
|
-
stage = summary.get("stageStatus") or {}
|
|
1623
|
-
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
1624
|
-
|
|
1625
|
-
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
1626
|
-
issues.append(
|
|
1627
|
-
{
|
|
1628
|
-
"code": "status-contradiction",
|
|
1629
|
-
"severity": "error",
|
|
1630
|
-
"message": (
|
|
1631
|
-
"finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
|
|
1632
|
-
"failed verification present"
|
|
1633
|
-
),
|
|
1634
|
-
}
|
|
1635
|
-
)
|
|
1636
|
-
if has_skip and html:
|
|
1637
|
-
if re.search(r">\s*OK\s*<", html) and "CONDITIONAL" not in html.upper():
|
|
1638
|
-
if final_status != "CONDITIONAL_OK":
|
|
1639
|
-
issues.append(
|
|
1640
|
-
{
|
|
1641
|
-
"code": "status-contradiction",
|
|
1642
|
-
"severity": "error",
|
|
1643
|
-
"message": "final-summary shows pure OK despite USER_SKIPPED/BLOCKED",
|
|
1644
|
-
}
|
|
1645
|
-
)
|
|
1646
|
-
else:
|
|
1647
|
-
# No HTML (and not render_skipped handled above): still check data-level status rules
|
|
1648
|
-
ver = summary.get("verification") or {}
|
|
1649
|
-
unit = ver.get("unitTests") or {}
|
|
1650
|
-
api = ver.get("apiTests") or {}
|
|
1651
|
-
api_status = str(api.get("status") or "")
|
|
1652
|
-
final_status = str(summary.get("finalStatus") or "")
|
|
1653
|
-
has_skip = api_status == "USER_SKIPPED" or str(
|
|
1654
|
-
ver.get("dbCompatibility") or ""
|
|
1655
|
-
) == "BLOCKED_BY_DBA"
|
|
1656
|
-
has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
|
|
1657
|
-
has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
|
|
1658
|
-
stage = summary.get("stageStatus") or {}
|
|
1659
|
-
has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
|
|
1660
|
-
if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
|
|
1661
|
-
issues.append(
|
|
1662
|
-
{
|
|
1663
|
-
"code": "status-contradiction",
|
|
1664
|
-
"severity": "error",
|
|
1665
|
-
"message": (
|
|
1666
|
-
"finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
|
|
1667
|
-
"failed verification present"
|
|
1668
|
-
),
|
|
1669
|
-
}
|
|
1670
|
-
)
|
|
1671
|
-
|
|
1672
|
-
errors = [i for i in issues if i.get("severity") == "error"]
|
|
1673
|
-
warnings = [i for i in issues if i.get("severity") != "error"]
|
|
1674
|
-
return {
|
|
1675
|
-
"ok": len(errors) == 0,
|
|
1676
|
-
"issues": issues,
|
|
1677
|
-
"error_count": len(errors),
|
|
1678
|
-
"warning_count": len(warnings),
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
def _html_escape(text: str) -> str:
|
|
1683
|
-
return (
|
|
1684
|
-
text.replace("&", "&")
|
|
1685
|
-
.replace("<", "<")
|
|
1686
|
-
.replace(">", ">")
|
|
1687
|
-
.replace('"', """)
|
|
1688
|
-
.replace("'", "'")
|
|
1689
|
-
)
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
# ---------------------------------------------------------------------------
|
|
1693
|
-
# knowledge / service post-steps
|
|
1694
|
-
# ---------------------------------------------------------------------------
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
def run_knowledge_poststeps(project_root: Path) -> dict[str, Any]:
|
|
1698
|
-
results: dict[str, Any] = {"ran": False, "steps": [], "warnings": []}
|
|
1699
|
-
if not KNOWLEDGE_SCRIPT.is_file():
|
|
1700
|
-
results["warnings"].append(f"harness_knowledge.py not found: {KNOWLEDGE_SCRIPT}")
|
|
1701
|
-
return results
|
|
1702
|
-
results["ran"] = True
|
|
1703
|
-
for cmd in ("ingest", "dedupe", "auto-supersede", "reverify-stale"):
|
|
1704
|
-
try:
|
|
1705
|
-
proc = subprocess.run(
|
|
1706
|
-
[
|
|
1707
|
-
sys.executable,
|
|
1708
|
-
str(KNOWLEDGE_SCRIPT),
|
|
1709
|
-
cmd,
|
|
1710
|
-
"--project",
|
|
1711
|
-
str(project_root),
|
|
1712
|
-
"--json",
|
|
1713
|
-
],
|
|
1714
|
-
capture_output=True,
|
|
1715
|
-
text=True,
|
|
1716
|
-
encoding="utf-8",
|
|
1717
|
-
errors="replace",
|
|
1718
|
-
timeout=300,
|
|
1719
|
-
check=False,
|
|
1720
|
-
)
|
|
1721
|
-
step = {
|
|
1722
|
-
"command": cmd,
|
|
1723
|
-
"exit_code": proc.returncode,
|
|
1724
|
-
"ok": proc.returncode == 0,
|
|
1725
|
-
}
|
|
1726
|
-
if proc.returncode != 0:
|
|
1727
|
-
step["stderr"] = (proc.stderr or proc.stdout or "")[:500]
|
|
1728
|
-
results["warnings"].append(f"knowledge {cmd} exit {proc.returncode}")
|
|
1729
|
-
results["steps"].append(step)
|
|
1730
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
1731
|
-
results["steps"].append({"command": cmd, "ok": False, "error": str(exc)})
|
|
1732
|
-
results["warnings"].append(f"knowledge {cmd} failed: {exc}")
|
|
1733
|
-
return results
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
def enqueue_maintenance_outbox(project_root: Path, archive_dir: Path) -> dict[str, Any]:
|
|
1737
|
-
"""§8.2: write a pending maintenance-outbox item instead of synchronously
|
|
1738
|
-
running the four knowledge subprocesses. Never rolls back the archive."""
|
|
1739
|
-
pending_dir = (
|
|
1740
|
-
project_root / ".harness" / "knowledge" / "maintenance-outbox" / "pending"
|
|
1741
|
-
)
|
|
1742
|
-
pending_dir.mkdir(parents=True, exist_ok=True)
|
|
1743
|
-
archive_id = archive_dir.name
|
|
1744
|
-
manifest = archive_dir / "evidence" / "archive-manifest-after.json"
|
|
1745
|
-
manifest_hash = "sha256:" + sha256_file(manifest) if manifest.is_file() else ""
|
|
1746
|
-
try:
|
|
1747
|
-
rel = archive_dir.resolve().relative_to(project_root.resolve()).as_posix()
|
|
1748
|
-
except ValueError:
|
|
1749
|
-
rel = str(archive_dir)
|
|
1750
|
-
item = {
|
|
1751
|
-
"schemaVersion": 1,
|
|
1752
|
-
"archiveId": archive_id,
|
|
1753
|
-
"archivePath": rel,
|
|
1754
|
-
"archiveManifestHash": manifest_hash,
|
|
1755
|
-
"status": "pending",
|
|
1756
|
-
"attempts": 0,
|
|
1757
|
-
"createdAt": now_iso(),
|
|
1758
|
-
"lastError": None,
|
|
1759
|
-
}
|
|
1760
|
-
item_path = pending_dir / f"{archive_id}.json"
|
|
1761
|
-
write_json(item_path, item)
|
|
1762
|
-
return {
|
|
1763
|
-
"queued": True,
|
|
1764
|
-
"outboxPath": str(item_path),
|
|
1765
|
-
"archiveId": archive_id,
|
|
1766
|
-
"status": "pending",
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
def run_service_stop(change_dir: Path) -> dict[str, Any]:
|
|
1771
|
-
if not SERVICE_SCRIPT.is_file():
|
|
1772
|
-
return {
|
|
1773
|
-
"ran": False,
|
|
1774
|
-
"skipped": True,
|
|
1775
|
-
"warning": f"harness_service.py not found; stop skipped",
|
|
1776
|
-
}
|
|
1777
|
-
try:
|
|
1778
|
-
proc = subprocess.run(
|
|
1779
|
-
[
|
|
1780
|
-
sys.executable,
|
|
1781
|
-
str(SERVICE_SCRIPT),
|
|
1782
|
-
"stop",
|
|
1783
|
-
"--change-dir",
|
|
1784
|
-
str(change_dir),
|
|
1785
|
-
"--if-started-by-ai",
|
|
1786
|
-
"--json",
|
|
1787
|
-
],
|
|
1788
|
-
capture_output=True,
|
|
1789
|
-
text=True,
|
|
1790
|
-
encoding="utf-8",
|
|
1791
|
-
errors="replace",
|
|
1792
|
-
timeout=60,
|
|
1793
|
-
check=False,
|
|
1794
|
-
)
|
|
1795
|
-
return {
|
|
1796
|
-
"ran": True,
|
|
1797
|
-
"skipped": False,
|
|
1798
|
-
"exit_code": proc.returncode,
|
|
1799
|
-
"ok": proc.returncode == 0,
|
|
1800
|
-
"stdout": (proc.stdout or "")[:500],
|
|
1801
|
-
}
|
|
1802
|
-
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
1803
|
-
return {"ran": True, "skipped": False, "ok": False, "warning": str(exc)}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
# ---------------------------------------------------------------------------
|
|
1807
|
-
# finalize
|
|
1808
|
-
# ---------------------------------------------------------------------------
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
def cmd_finalize(
|
|
1812
|
-
change_dir: Path,
|
|
1813
|
-
archive_root: Path,
|
|
1814
|
-
*,
|
|
1815
|
-
skip_ingest: bool = False,
|
|
1816
|
-
) -> tuple[int, dict[str, Any]]:
|
|
1817
|
-
"""Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
|
|
1818
|
-
warnings: list[str] = []
|
|
1819
|
-
original_change_dir = change_dir.resolve()
|
|
1820
|
-
change_name = original_change_dir.name
|
|
1821
|
-
archive_root = archive_root.resolve()
|
|
1822
|
-
archive_root.mkdir(parents=True, exist_ok=True)
|
|
1823
|
-
archive_dir = archive_root / f"{today_date()}-{change_name}"
|
|
1824
|
-
project_root = find_project_root(original_change_dir)
|
|
1825
|
-
|
|
1826
|
-
payload: dict[str, Any] = {
|
|
1827
|
-
"ok": False,
|
|
1828
|
-
"action": "finalize",
|
|
1829
|
-
"change_dir": str(original_change_dir),
|
|
1830
|
-
"archive_dir": str(archive_dir),
|
|
1831
|
-
"change_name": change_name,
|
|
1832
|
-
"warnings": warnings,
|
|
1833
|
-
"steps": {},
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
if not original_change_dir.is_dir():
|
|
1837
|
-
payload["error"] = f"change dir not found: {original_change_dir}"
|
|
1838
|
-
return 1, payload
|
|
1839
|
-
|
|
1840
|
-
if archive_dir.exists():
|
|
1841
|
-
payload["error"] = f"archive target already exists: {archive_dir}"
|
|
1842
|
-
return 1, payload
|
|
1843
|
-
|
|
1844
|
-
work_dir = original_change_dir
|
|
1845
|
-
|
|
1846
|
-
def _safe_append(**kwargs: Any) -> None:
|
|
1847
|
-
nonlocal work_dir
|
|
1848
|
-
try:
|
|
1849
|
-
append_event(work_dir, **kwargs)
|
|
1850
|
-
except OSError as exc:
|
|
1851
|
-
warnings.append(f"event append failed: {exc}")
|
|
1852
|
-
|
|
1853
|
-
# Step 9 starts here: phase.start
|
|
1854
|
-
_safe_append(phase="archive", type_="phase.start", note="finalize start")
|
|
1855
|
-
|
|
1856
|
-
# --- 1. before-manifest ---
|
|
1857
|
-
before_path = work_dir / "evidence" / "archive-manifest-before.json"
|
|
1858
|
-
try:
|
|
1859
|
-
before_manifest = generate_manifest(work_dir, before_path)
|
|
1860
|
-
payload["steps"]["before_manifest"] = {
|
|
1861
|
-
"ok": True,
|
|
1862
|
-
"path": str(before_path),
|
|
1863
|
-
"fileCount": before_manifest.get("fileCount"),
|
|
1864
|
-
}
|
|
1865
|
-
_safe_append(
|
|
1866
|
-
phase="archive",
|
|
1867
|
-
type_="artifact",
|
|
1868
|
-
path=str(before_path.relative_to(work_dir)).replace("\\", "/"),
|
|
1869
|
-
kind="manifest-before",
|
|
1870
|
-
)
|
|
1871
|
-
_safe_append(
|
|
1872
|
-
phase="archive",
|
|
1873
|
-
type_="command",
|
|
1874
|
-
command="generate_manifest(before)",
|
|
1875
|
-
exit_code=0,
|
|
1876
|
-
note="before-manifest",
|
|
1877
|
-
)
|
|
1878
|
-
except OSError as exc:
|
|
1879
|
-
payload["error"] = f"before-manifest failed: {exc}"
|
|
1880
|
-
_safe_append(
|
|
1881
|
-
phase="archive",
|
|
1882
|
-
type_="issue",
|
|
1883
|
-
code="before-manifest-failed",
|
|
1884
|
-
severity="error",
|
|
1885
|
-
message=str(exc),
|
|
1886
|
-
)
|
|
1887
|
-
return 1, payload
|
|
1888
|
-
|
|
1889
|
-
# --- 2. move ---
|
|
1890
|
-
try:
|
|
1891
|
-
archive_root.mkdir(parents=True, exist_ok=True)
|
|
1892
|
-
shutil.move(str(original_change_dir), str(archive_dir))
|
|
1893
|
-
work_dir = archive_dir
|
|
1894
|
-
payload["steps"]["move"] = {"ok": True, "to": str(archive_dir)}
|
|
1895
|
-
_safe_append(
|
|
1896
|
-
phase="archive",
|
|
1897
|
-
type_="command",
|
|
1898
|
-
command=f"move → {archive_dir.name}",
|
|
1899
|
-
exit_code=0,
|
|
1900
|
-
note="moved change dir to archive",
|
|
1901
|
-
)
|
|
1902
|
-
except OSError as exc:
|
|
1903
|
-
# Move failed: original intact, stop immediately
|
|
1904
|
-
payload["error"] = f"move failed: {exc}"
|
|
1905
|
-
payload["steps"]["move"] = {"ok": False, "error": str(exc)}
|
|
1906
|
-
payload["original_preserved"] = original_change_dir.is_dir()
|
|
1907
|
-
try:
|
|
1908
|
-
append_event(
|
|
1909
|
-
original_change_dir,
|
|
1910
|
-
phase="archive",
|
|
1911
|
-
type_="issue",
|
|
1912
|
-
code="move-failed",
|
|
1913
|
-
severity="error",
|
|
1914
|
-
message=str(exc),
|
|
1915
|
-
)
|
|
1916
|
-
except OSError:
|
|
1917
|
-
pass
|
|
1918
|
-
return 1, payload
|
|
1919
|
-
|
|
1920
|
-
# --- 3. collect ---
|
|
1921
|
-
try:
|
|
1922
|
-
summary = collect_summary_data(
|
|
1923
|
-
work_dir,
|
|
1924
|
-
before_manifest=before_manifest,
|
|
1925
|
-
write=True,
|
|
1926
|
-
for_replay=False,
|
|
1927
|
-
)
|
|
1928
|
-
summary_path = work_dir / "reports" / "final" / "summary-data.json"
|
|
1929
|
-
payload["steps"]["collect"] = {"ok": True, "path": str(summary_path)}
|
|
1930
|
-
_safe_append(
|
|
1931
|
-
phase="archive",
|
|
1932
|
-
type_="artifact",
|
|
1933
|
-
path="reports/final/summary-data.json",
|
|
1934
|
-
kind="summary-data",
|
|
1935
|
-
)
|
|
1936
|
-
except Exception as exc: # noqa: BLE001 — surface collect failures
|
|
1937
|
-
payload["error"] = f"collect failed: {exc}"
|
|
1938
|
-
payload["steps"]["collect"] = {"ok": False, "error": str(exc)}
|
|
1939
|
-
_restore_on_failure(archive_dir, original_change_dir, payload, warnings)
|
|
1940
|
-
return 1, payload
|
|
1941
|
-
|
|
1942
|
-
# --- 4. render (Node, else Python fallback) ---
|
|
1943
|
-
render_result = render_final_summary(work_dir, summary_path)
|
|
1944
|
-
payload["steps"]["render"] = render_result
|
|
1945
|
-
if not render_result.get("ok"):
|
|
1946
|
-
# Node + fallback both failed: restore + exit non-0 (§4.1 rule 4)。
|
|
1947
|
-
# 永不关闭一个没有 final-summary 的归档。
|
|
1948
|
-
msg = str(render_result.get("fallbackReason") or "render failed")
|
|
1949
|
-
payload["error"] = f"final-summary render failed: {msg}"
|
|
1950
|
-
_safe_append(
|
|
1951
|
-
phase="archive",
|
|
1952
|
-
type_="issue",
|
|
1953
|
-
code="render-failed",
|
|
1954
|
-
severity="error",
|
|
1955
|
-
message=msg,
|
|
1956
|
-
)
|
|
1957
|
-
_restore_on_failure(archive_dir, original_change_dir, payload, warnings)
|
|
1958
|
-
_restore_target = original_change_dir if original_change_dir.is_dir() else work_dir
|
|
1959
|
-
try:
|
|
1960
|
-
append_event(
|
|
1961
|
-
_restore_target,
|
|
1962
|
-
phase="archive",
|
|
1963
|
-
type_="phase.end",
|
|
1964
|
-
note="finalize aborted; render failed; original preserved",
|
|
1965
|
-
)
|
|
1966
|
-
except OSError:
|
|
1967
|
-
pass
|
|
1968
|
-
payload["warnings"] = warnings
|
|
1969
|
-
payload["ok"] = False
|
|
1970
|
-
return 1, payload
|
|
1971
|
-
renderer = render_result.get("renderer")
|
|
1972
|
-
if renderer == "python-fallback" and render_result.get("fallbackReason"):
|
|
1973
|
-
warnings.append(
|
|
1974
|
-
f"node render unavailable; used python-fallback: "
|
|
1975
|
-
f"{render_result.get('fallbackReason')}"
|
|
1976
|
-
)
|
|
1977
|
-
_safe_append(
|
|
1978
|
-
phase="archive",
|
|
1979
|
-
type_="artifact",
|
|
1980
|
-
path="reports/final/final-summary.html",
|
|
1981
|
-
kind="final-report",
|
|
1982
|
-
)
|
|
1983
|
-
_safe_append(
|
|
1984
|
-
phase="archive",
|
|
1985
|
-
type_="command",
|
|
1986
|
-
command=f"render-final-summary ({renderer})",
|
|
1987
|
-
exit_code=0,
|
|
1988
|
-
note=f"final-summary rendered by {renderer}",
|
|
1989
|
-
)
|
|
1990
|
-
|
|
1991
|
-
html_path = work_dir / "reports" / "final" / "final-summary.html"
|
|
1992
|
-
|
|
1993
|
-
# --- 5. validate (same process, no re-collect) ---
|
|
1994
|
-
# Refresh summary from disk (collect already wrote it); do not re-collect.
|
|
1995
|
-
try:
|
|
1996
|
-
summary = read_json(summary_path)
|
|
1997
|
-
except (OSError, json.JSONDecodeError):
|
|
1998
|
-
pass
|
|
1999
|
-
validate_result = validate_summary(
|
|
2000
|
-
summary,
|
|
2001
|
-
html_path if html_path.is_file() else None,
|
|
2002
|
-
)
|
|
2003
|
-
payload["steps"]["validate"] = validate_result
|
|
2004
|
-
summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get(
|
|
2005
|
-
"issues"
|
|
2006
|
-
) or []
|
|
2007
|
-
try:
|
|
2008
|
-
write_json(summary_path, summary)
|
|
2009
|
-
except OSError as exc:
|
|
2010
|
-
warnings.append(f"could not write validationIssues: {exc}")
|
|
2011
|
-
|
|
2012
|
-
for issue in validate_result.get("issues") or []:
|
|
2013
|
-
_safe_append(
|
|
2014
|
-
phase="archive",
|
|
2015
|
-
type_="issue",
|
|
2016
|
-
code=str(issue.get("code") or "validate"),
|
|
2017
|
-
severity=str(issue.get("severity") or "warning"),
|
|
2018
|
-
message=str(issue.get("message") or ""),
|
|
2019
|
-
)
|
|
2020
|
-
|
|
2021
|
-
# --- 6. after-manifest + compare ---
|
|
2022
|
-
after_path = work_dir / "evidence" / "archive-manifest-after.json"
|
|
2023
|
-
try:
|
|
2024
|
-
after_manifest = generate_manifest(work_dir, after_path)
|
|
2025
|
-
# Re-read before from archive (moved with the tree)
|
|
2026
|
-
before_in_archive = work_dir / "evidence" / "archive-manifest-before.json"
|
|
2027
|
-
if before_in_archive.is_file():
|
|
2028
|
-
before_manifest = read_json(before_in_archive)
|
|
2029
|
-
compare_result = compare_manifests(before_manifest, after_manifest)
|
|
2030
|
-
payload["steps"]["after_manifest"] = {
|
|
2031
|
-
"ok": True,
|
|
2032
|
-
"path": str(after_path),
|
|
2033
|
-
"compare": compare_result,
|
|
2034
|
-
}
|
|
2035
|
-
_safe_append(
|
|
2036
|
-
phase="archive",
|
|
2037
|
-
type_="artifact",
|
|
2038
|
-
path="evidence/archive-manifest-after.json",
|
|
2039
|
-
kind="manifest-after",
|
|
2040
|
-
)
|
|
2041
|
-
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
2042
|
-
payload["error"] = f"after-manifest failed: {exc}"
|
|
2043
|
-
payload["steps"]["after_manifest"] = {"ok": False, "error": str(exc)}
|
|
2044
|
-
_restore_on_failure(archive_dir, original_change_dir, payload, warnings)
|
|
2045
|
-
return 1, payload
|
|
2046
|
-
|
|
2047
|
-
# Update archiveManifest in summary-data with compare stats
|
|
2048
|
-
summary["archiveManifest"] = {
|
|
2049
|
-
"movedFiles": compare_result.get("movedFiles", 0),
|
|
2050
|
-
"generatedFiles": compare_result.get("generatedFiles", 0),
|
|
2051
|
-
"totalArchiveFiles": compare_result.get("totalArchiveFiles", 0),
|
|
2052
|
-
"checksumStatus": compare_result.get("checksumStatus", "FAIL"),
|
|
2053
|
-
}
|
|
2054
|
-
try:
|
|
2055
|
-
write_json(summary_path, summary)
|
|
2056
|
-
except OSError as exc:
|
|
2057
|
-
warnings.append(f"could not update archiveManifest: {exc}")
|
|
2058
|
-
|
|
2059
|
-
# The first render is needed so the after-manifest includes a final report,
|
|
2060
|
-
# but it predates the final manifest statistics. Render and validate once
|
|
2061
|
-
# more from the now-final summary so HTML never contradicts summary-data.
|
|
2062
|
-
render_result = render_final_summary(work_dir, summary_path)
|
|
2063
|
-
if not render_result.get("ok"):
|
|
2064
|
-
payload["error"] = f"final summary re-render failed: {render_result.get('error')}"
|
|
2065
|
-
_restore_on_failure(archive_dir, original_change_dir, payload, warnings)
|
|
2066
|
-
return 1, payload
|
|
2067
|
-
validate_result = validate_summary(summary, html_path if html_path.is_file() else None)
|
|
2068
|
-
payload["steps"]["validate"] = validate_result
|
|
2069
|
-
summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get("issues") or []
|
|
2070
|
-
write_json(summary_path, summary)
|
|
2071
|
-
|
|
2072
|
-
# --- 7. delete original only if validate+manifest OK ---
|
|
2073
|
-
# After move, "original" is gone; on failure we restore. On success, ensure
|
|
2074
|
-
# the changes path does not linger.
|
|
2075
|
-
validate_ok = bool(validate_result.get("ok"))
|
|
2076
|
-
manifest_ok = bool(compare_result.get("ok"))
|
|
2077
|
-
can_close = validate_ok and manifest_ok
|
|
2078
|
-
|
|
2079
|
-
if not can_close:
|
|
2080
|
-
issues_out = list(validate_result.get("issues") or [])
|
|
2081
|
-
if not manifest_ok:
|
|
2082
|
-
issues_out.append(
|
|
2083
|
-
{
|
|
2084
|
-
"code": "manifest-mismatch",
|
|
2085
|
-
"severity": "error",
|
|
2086
|
-
"message": (
|
|
2087
|
-
f"missing={compare_result.get('missing')} "
|
|
2088
|
-
f"mismatched={compare_result.get('mismatched')}"
|
|
2089
|
-
),
|
|
2090
|
-
}
|
|
2091
|
-
)
|
|
2092
|
-
payload["issues"] = issues_out
|
|
2093
|
-
payload["error"] = "validate or manifest check failed; original change dir restored"
|
|
2094
|
-
payload["steps"]["delete_original"] = {"ok": False, "deleted": False}
|
|
2095
|
-
_restore_on_failure(archive_dir, original_change_dir, payload, warnings)
|
|
2096
|
-
_safe_append_restored = original_change_dir if original_change_dir.is_dir() else None
|
|
2097
|
-
if _safe_append_restored:
|
|
2098
|
-
try:
|
|
2099
|
-
append_event(
|
|
2100
|
-
_safe_append_restored,
|
|
2101
|
-
phase="archive",
|
|
2102
|
-
type_="phase.end",
|
|
2103
|
-
note="finalize aborted; original preserved",
|
|
2104
|
-
)
|
|
2105
|
-
except OSError:
|
|
2106
|
-
pass
|
|
2107
|
-
payload["warnings"] = warnings
|
|
2108
|
-
payload["ok"] = False
|
|
2109
|
-
return 1, payload
|
|
2110
|
-
|
|
2111
|
-
# Success path: original already relocated; confirm absence
|
|
2112
|
-
if original_change_dir.exists():
|
|
2113
|
-
try:
|
|
2114
|
-
shutil.rmtree(original_change_dir)
|
|
2115
|
-
payload["steps"]["delete_original"] = {"ok": True, "deleted": True}
|
|
2116
|
-
except OSError as exc:
|
|
2117
|
-
warnings.append(f"could not remove leftover change dir: {exc}")
|
|
2118
|
-
payload["steps"]["delete_original"] = {"ok": False, "error": str(exc)}
|
|
2119
|
-
else:
|
|
2120
|
-
payload["steps"]["delete_original"] = {
|
|
2121
|
-
"ok": True,
|
|
2122
|
-
"deleted": True,
|
|
2123
|
-
"note": "removed by move",
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
_safe_append(
|
|
2127
|
-
phase="archive",
|
|
2128
|
-
type_="verification",
|
|
2129
|
-
name="archive-closure",
|
|
2130
|
-
status="ok",
|
|
2131
|
-
note="manifest+validate passed; original removed",
|
|
2132
|
-
)
|
|
2133
|
-
|
|
2134
|
-
# --- 8. maintenance outbox + service (§8.2: close no longer runs the four
|
|
2135
|
-
# knowledge subprocesses; it enqueues a pending outbox item and returns) ---
|
|
2136
|
-
if skip_ingest:
|
|
2137
|
-
payload["steps"]["knowledge"] = {"skipped": True, "reason": "--skip-ingest"}
|
|
2138
|
-
payload["knowledgeMaintenance"] = "SKIPPED"
|
|
2139
|
-
else:
|
|
2140
|
-
try:
|
|
2141
|
-
enqueue = enqueue_maintenance_outbox(project_root, work_dir)
|
|
2142
|
-
payload["steps"]["knowledge"] = enqueue
|
|
2143
|
-
payload["knowledgeMaintenance"] = "QUEUED"
|
|
2144
|
-
except OSError as exc:
|
|
2145
|
-
warnings.append(f"maintenance outbox enqueue failed: {exc}")
|
|
2146
|
-
payload["steps"]["knowledge"] = {"queued": False, "error": str(exc)}
|
|
2147
|
-
payload["knowledgeMaintenance"] = "NOT_QUEUED"
|
|
2148
|
-
|
|
2149
|
-
service_result = run_service_stop(work_dir)
|
|
2150
|
-
payload["steps"]["service_stop"] = service_result
|
|
2151
|
-
if service_result.get("warning"):
|
|
2152
|
-
warnings.append(str(service_result["warning"]))
|
|
2153
|
-
|
|
2154
|
-
_safe_append(phase="archive", type_="phase.end", note="finalize complete")
|
|
2155
|
-
|
|
2156
|
-
payload["ok"] = True
|
|
2157
|
-
payload["warnings"] = warnings
|
|
2158
|
-
payload["summary_data"] = str(summary_path)
|
|
2159
|
-
payload["final_summary"] = str(html_path) if html_path.is_file() else None
|
|
2160
|
-
return 0, payload
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
def _restore_on_failure(
|
|
2164
|
-
archive_dir: Path,
|
|
2165
|
-
original_change_dir: Path,
|
|
2166
|
-
payload: dict[str, Any],
|
|
2167
|
-
warnings: list[str],
|
|
2168
|
-
) -> None:
|
|
2169
|
-
"""Move archive back to changes path so validate errors never lose the original."""
|
|
2170
|
-
if original_change_dir.exists():
|
|
2171
|
-
# Already present (e.g. move never happened)
|
|
2172
|
-
payload["original_preserved"] = True
|
|
2173
|
-
# Clean partial archive if present and distinct
|
|
2174
|
-
if archive_dir.exists() and archive_dir.resolve() != original_change_dir.resolve():
|
|
2175
|
-
warnings.append(
|
|
2176
|
-
f"partial archive left at {archive_dir} (original also present)"
|
|
2177
|
-
)
|
|
2178
|
-
return
|
|
2179
|
-
if not archive_dir.exists():
|
|
2180
|
-
payload["original_preserved"] = False
|
|
2181
|
-
warnings.append("restore failed: neither archive nor original exists")
|
|
2182
|
-
return
|
|
2183
|
-
try:
|
|
2184
|
-
original_change_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
2185
|
-
shutil.move(str(archive_dir), str(original_change_dir))
|
|
2186
|
-
payload["original_preserved"] = True
|
|
2187
|
-
payload["restored_to"] = str(original_change_dir)
|
|
2188
|
-
except OSError as exc:
|
|
2189
|
-
payload["original_preserved"] = False
|
|
2190
|
-
warnings.append(f"restore move failed: {exc}; data at {archive_dir}")
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
# ---------------------------------------------------------------------------
|
|
2194
|
-
# replay
|
|
2195
|
-
# ---------------------------------------------------------------------------
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
def cmd_replay(
|
|
2199
|
-
archive_dir: Path,
|
|
2200
|
-
*,
|
|
2201
|
-
out_path: Path | None = None,
|
|
2202
|
-
) -> tuple[int, dict[str, Any]]:
|
|
2203
|
-
"""Read-only collect + validate. Never mutates archive contents."""
|
|
2204
|
-
archive_dir = archive_dir.resolve()
|
|
2205
|
-
if not archive_dir.is_dir():
|
|
2206
|
-
return 1, {"ok": False, "error": f"archive dir not found: {archive_dir}"}
|
|
2207
|
-
|
|
2208
|
-
# Collect without writing into the archive
|
|
2209
|
-
summary = collect_summary_data(archive_dir, write=False, for_replay=True)
|
|
2210
|
-
|
|
2211
|
-
html_path = archive_dir / "reports" / "final" / "final-summary.html"
|
|
2212
|
-
render_skipped = not html_path.is_file()
|
|
2213
|
-
validate_result = validate_summary(
|
|
2214
|
-
summary,
|
|
2215
|
-
html_path if html_path.is_file() else None,
|
|
2216
|
-
render_skipped=render_skipped,
|
|
2217
|
-
)
|
|
2218
|
-
summary.setdefault("reportPipeline", {})["validationIssues"] = (
|
|
2219
|
-
validate_result.get("issues") or []
|
|
2220
|
-
)
|
|
2221
|
-
|
|
2222
|
-
if out_path is not None:
|
|
2223
|
-
# Allowed to write outside the archive
|
|
2224
|
-
out_resolved = out_path.resolve()
|
|
2225
|
-
try:
|
|
2226
|
-
out_resolved.relative_to(archive_dir.resolve())
|
|
2227
|
-
inside = True
|
|
2228
|
-
except ValueError:
|
|
2229
|
-
inside = False
|
|
2230
|
-
if inside:
|
|
2231
|
-
return 1, {
|
|
2232
|
-
"ok": False,
|
|
2233
|
-
"error": "replay refuses to write inside archive dir (read-only)",
|
|
2234
|
-
}
|
|
2235
|
-
write_json(out_resolved, summary)
|
|
2236
|
-
|
|
2237
|
-
payload = {
|
|
2238
|
-
"ok": validate_result.get("ok", False),
|
|
2239
|
-
"action": "replay",
|
|
2240
|
-
"archive_dir": str(archive_dir),
|
|
2241
|
-
"change_name": summary.get("changeName"),
|
|
2242
|
-
"summary_data": summary,
|
|
2243
|
-
"validate": validate_result,
|
|
2244
|
-
"sources": (summary.get("reportPipeline") or {}).get("sources") or [],
|
|
2245
|
-
}
|
|
2246
|
-
# Replay itself is successful as an operation even if validate finds issues;
|
|
2247
|
-
# exit non-zero only on hard failures. Soft: ok mirrors validate.
|
|
2248
|
-
return (0 if payload["ok"] else 1), payload
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
# ---------------------------------------------------------------------------
|
|
2252
|
-
# CLI
|
|
2253
|
-
# ---------------------------------------------------------------------------
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
def cmd_status_cli(args: argparse.Namespace) -> int:
|
|
2257
|
-
change_dir = resolve_path(args.change_dir)
|
|
2258
|
-
result = check_status(change_dir)
|
|
2259
|
-
emit_json(result)
|
|
2260
|
-
# Checks completed → exit 0; archivable flag conveys the gate result.
|
|
2261
|
-
return 0 if result.get("ok") else 1
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
def cmd_finalize_cli(args: argparse.Namespace) -> int:
|
|
2265
|
-
change_dir = resolve_path(args.change_dir)
|
|
2266
|
-
archive_root = resolve_path(args.archive_root)
|
|
2267
|
-
code, payload = cmd_finalize(
|
|
2268
|
-
change_dir,
|
|
2269
|
-
archive_root,
|
|
2270
|
-
skip_ingest=bool(args.skip_ingest),
|
|
2271
|
-
)
|
|
2272
|
-
emit_json(payload)
|
|
2273
|
-
return code
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
def cmd_replay_cli(args: argparse.Namespace) -> int:
|
|
2277
|
-
archive_dir = resolve_path(args.archive_dir)
|
|
2278
|
-
out_path = resolve_path(args.out) if getattr(args, "out", None) else None
|
|
2279
|
-
code, payload = cmd_replay(archive_dir, out_path=out_path)
|
|
2280
|
-
emit_json(payload)
|
|
2281
|
-
return code
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
2285
|
-
parser = argparse.ArgumentParser(
|
|
2286
|
-
prog="harness_archive.py",
|
|
2287
|
-
description="Archive finalize / status / replay (D3)",
|
|
2288
|
-
)
|
|
2289
|
-
sub = parser.add_subparsers(dest="command_name", required=True)
|
|
2290
|
-
|
|
2291
|
-
p_status = sub.add_parser("status", help="pre-archive gate checks (read-only)")
|
|
2292
|
-
p_status.add_argument("--change-dir", required=True)
|
|
2293
|
-
p_status.add_argument("--json", action="store_true", default=True)
|
|
2294
|
-
p_status.set_defaults(func=cmd_status_cli)
|
|
2295
|
-
|
|
2296
|
-
p_fin = sub.add_parser("finalize", help="single-process archive finalize")
|
|
2297
|
-
p_fin.add_argument("--change-dir", required=True)
|
|
2298
|
-
p_fin.add_argument("--archive-root", required=True)
|
|
2299
|
-
p_fin.add_argument("--skip-ingest", action="store_true")
|
|
2300
|
-
p_fin.add_argument("--json", action="store_true", default=True)
|
|
2301
|
-
p_fin.set_defaults(func=cmd_finalize_cli)
|
|
2302
|
-
|
|
2303
|
-
p_rep = sub.add_parser("replay", help="read-only re-collect + validate")
|
|
2304
|
-
p_rep.add_argument("--archive-dir", required=True)
|
|
2305
|
-
p_rep.add_argument("--out", default=None, help="write summary-data JSON outside archive")
|
|
2306
|
-
p_rep.add_argument("--json", action="store_true", default=True)
|
|
2307
|
-
p_rep.set_defaults(func=cmd_replay_cli)
|
|
2308
|
-
|
|
2309
|
-
return parser
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
def main(argv: list[str] | None = None) -> int:
|
|
2313
|
-
parser = build_parser()
|
|
2314
|
-
args = parser.parse_args(argv)
|
|
2315
|
-
return int(args.func(args))
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
if __name__ == "__main__":
|
|
2319
|
-
raise SystemExit(main())
|