@hunter-harness/workflow-harness 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/reference.md +2 -8
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +30 -6
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/pitfalls.md +16 -0
- package/harness/bundles/general/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +31 -14
- package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +179 -4
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +269 -3
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +72 -382
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +742 -0
- package/harness/bundles/general/claude-code/scripts/harness_service.py +99 -2
- package/harness/bundles/general/claude-code/scripts/harness_state.py +179 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +2 -8
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +30 -6
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +16 -0
- package/harness/bundles/general/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +31 -14
- package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +179 -4
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +269 -3
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +72 -382
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +742 -0
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +99 -2
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +179 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/reference.md +2 -8
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +5 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-submit/checklist.md +30 -6
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-test/pitfalls.md +16 -0
- package/harness/bundles/general/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +31 -14
- package/harness/bundles/general/codex/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/general/codex/scripts/harness_deploy.py +179 -4
- package/harness/bundles/general/codex/scripts/harness_ledger.py +269 -3
- package/harness/bundles/general/codex/scripts/harness_preflight.py +72 -382
- package/harness/bundles/general/codex/scripts/harness_profile.py +742 -0
- package/harness/bundles/general/codex/scripts/harness_service.py +99 -2
- package/harness/bundles/general/codex/scripts/harness_state.py +179 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/reference.md +2 -8
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/checklist.md +30 -6
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-test/pitfalls.md +16 -0
- package/harness/bundles/general/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +31 -14
- package/harness/bundles/general/cursor/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +179 -4
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +269 -3
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +72 -382
- package/harness/bundles/general/cursor/scripts/harness_profile.py +742 -0
- package/harness/bundles/general/cursor/scripts/harness_service.py +99 -2
- package/harness/bundles/general/cursor/scripts/harness_state.py +179 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/reference.md +2 -8
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-run/checklist.md +3 -1
- package/harness/bundles/java/claude-code/harness-run/reference.md +6 -0
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +30 -6
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/claude-code/harness-test/checklist.md +3 -1
- package/harness/bundles/java/claude-code/harness-test/pitfalls.md +16 -0
- package/harness/bundles/java/claude-code/harness-test/reference.md +2 -0
- package/harness/bundles/java/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +31 -14
- package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +179 -4
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +269 -3
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +72 -382
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +742 -0
- package/harness/bundles/java/claude-code/scripts/harness_service.py +99 -2
- package/harness/bundles/java/claude-code/scripts/harness_state.py +179 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +2 -8
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-run/checklist.md +3 -1
- package/harness/bundles/java/codebuddy/harness-run/reference.md +6 -0
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +30 -6
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/codebuddy/harness-test/checklist.md +3 -1
- package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +16 -0
- package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -0
- package/harness/bundles/java/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +31 -14
- package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +179 -4
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +269 -3
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +72 -382
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +742 -0
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +99 -2
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +179 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/reference.md +2 -8
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +5 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-run/checklist.md +3 -1
- package/harness/bundles/java/codex/harness-run/reference.md +6 -0
- package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-submit/checklist.md +30 -6
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/codex/harness-test/checklist.md +3 -1
- package/harness/bundles/java/codex/harness-test/pitfalls.md +16 -0
- package/harness/bundles/java/codex/harness-test/reference.md +2 -0
- package/harness/bundles/java/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +31 -14
- package/harness/bundles/java/codex/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/java/codex/scripts/harness_deploy.py +179 -4
- package/harness/bundles/java/codex/scripts/harness_ledger.py +269 -3
- package/harness/bundles/java/codex/scripts/harness_preflight.py +72 -382
- package/harness/bundles/java/codex/scripts/harness_profile.py +742 -0
- package/harness/bundles/java/codex/scripts/harness_service.py +99 -2
- package/harness/bundles/java/codex/scripts/harness_state.py +179 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/reference.md +2 -8
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-run/checklist.md +3 -1
- package/harness/bundles/java/cursor/harness-run/reference.md +6 -0
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-submit/checklist.md +30 -6
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/cursor/harness-test/checklist.md +3 -1
- package/harness/bundles/java/cursor/harness-test/pitfalls.md +16 -0
- package/harness/bundles/java/cursor/harness-test/reference.md +2 -0
- package/harness/bundles/java/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +1 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +55 -2
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +1 -8
- package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +13 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +31 -14
- package/harness/bundles/java/cursor/scripts/harness_check_gate.py +111 -0
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +179 -4
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +269 -3
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +72 -382
- package/harness/bundles/java/cursor/scripts/harness_profile.py +742 -0
- package/harness/bundles/java/cursor/scripts/harness_service.py +99 -2
- package/harness/bundles/java/cursor/scripts/harness_state.py +179 -0
- package/harness/manifests/general/claude-code.json +44 -28
- package/harness/manifests/general/codebuddy.json +44 -28
- package/harness/manifests/general/codex.json +44 -28
- package/harness/manifests/general/cursor.json +44 -28
- package/harness/manifests/java/claude-code.json +48 -32
- package/harness/manifests/java/codebuddy.json +48 -32
- package/harness/manifests/java/codex.json +48 -32
- package/harness/manifests/java/cursor.json +48 -32
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py
CHANGED
|
@@ -97,6 +97,29 @@ def write_text(path: Path, text: str) -> None:
|
|
|
97
97
|
path.write_text(text, encoding="utf-8")
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _serialize_json_bytes(data: Any) -> bytes:
|
|
101
|
+
return (json.dumps(data, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def write_json_if_changed(path: Path, data: Any) -> bool:
|
|
105
|
+
"""Write only when byte content would differ; returns True iff a write occurred."""
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
payload = _serialize_json_bytes(data)
|
|
108
|
+
if path.exists() and path.read_bytes() == payload:
|
|
109
|
+
return False
|
|
110
|
+
path.write_bytes(payload)
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def write_text_if_changed(path: Path, text: str) -> bool:
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
payload = text.encode("utf-8")
|
|
117
|
+
if path.exists() and path.read_bytes() == payload:
|
|
118
|
+
return False
|
|
119
|
+
path.write_bytes(payload)
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
100
123
|
def json_clone(value: Any) -> Any:
|
|
101
124
|
return json.loads(json.dumps(value, ensure_ascii=False))
|
|
102
125
|
|
|
@@ -621,21 +644,7 @@ def looks_like_contract_path(path: str) -> bool:
|
|
|
621
644
|
return any(part in lower for part in ["openapi", "contract", "schema", "protocol", "api"])
|
|
622
645
|
|
|
623
646
|
|
|
624
|
-
def
|
|
625
|
-
for sub in ["entries/candidate", "entries/stale", "entries/conflicted", "entries/superseded"]:
|
|
626
|
-
path = knowledge / sub
|
|
627
|
-
if path.exists():
|
|
628
|
-
if sub in {"entries/candidate", "entries/stale"}:
|
|
629
|
-
for entry_path in sorted(path.glob("*.json")):
|
|
630
|
-
try:
|
|
631
|
-
entry = read_json(entry_path)
|
|
632
|
-
except (OSError, json.JSONDecodeError):
|
|
633
|
-
entry_path.unlink(missing_ok=True)
|
|
634
|
-
continue
|
|
635
|
-
if not isinstance(entry, dict) or not entry.get("lifecycle", {}).get("demotedAt"):
|
|
636
|
-
entry_path.unlink(missing_ok=True)
|
|
637
|
-
else:
|
|
638
|
-
shutil.rmtree(path)
|
|
647
|
+
def ensure_knowledge_dirs(knowledge: Path) -> None:
|
|
639
648
|
for sub in [
|
|
640
649
|
"entries/candidate",
|
|
641
650
|
"entries/active",
|
|
@@ -650,6 +659,50 @@ def reset_generated_knowledge(knowledge: Path) -> None:
|
|
|
650
659
|
(knowledge / sub).mkdir(parents=True, exist_ok=True)
|
|
651
660
|
|
|
652
661
|
|
|
662
|
+
def prune_generated_entries(knowledge: Path, current_entries: list[dict[str, Any]]) -> int:
|
|
663
|
+
"""Remove generated entry files that disappeared or moved to another status dir.
|
|
664
|
+
|
|
665
|
+
Replaces the old reset-and-rewrite sweep. Preserved entries (active, and
|
|
666
|
+
candidate/stale carrying a manual ``demotedAt``) are never removed, so the
|
|
667
|
+
write-if-changed persist step can detect unchanged entries by content.
|
|
668
|
+
Returns the number of files removed.
|
|
669
|
+
"""
|
|
670
|
+
current_by_id: dict[str, dict[str, Any]] = {}
|
|
671
|
+
for entry in current_entries:
|
|
672
|
+
if isinstance(entry, dict) and entry.get("id"):
|
|
673
|
+
current_by_id[str(entry["id"])] = entry
|
|
674
|
+
preserved_ids = {entry["id"] for entry in load_preserved_entries(knowledge)}
|
|
675
|
+
removed = 0
|
|
676
|
+
for status in ["candidate", "stale", "superseded", "conflicted"]:
|
|
677
|
+
status_dir = knowledge / "entries" / status
|
|
678
|
+
if not status_dir.exists():
|
|
679
|
+
continue
|
|
680
|
+
for path in sorted(status_dir.glob("*.json")):
|
|
681
|
+
try:
|
|
682
|
+
entry = read_json(path)
|
|
683
|
+
except (OSError, json.JSONDecodeError):
|
|
684
|
+
path.unlink(missing_ok=True)
|
|
685
|
+
removed += 1
|
|
686
|
+
continue
|
|
687
|
+
eid = entry.get("id") if isinstance(entry, dict) else None
|
|
688
|
+
if eid is None:
|
|
689
|
+
path.unlink(missing_ok=True)
|
|
690
|
+
removed += 1
|
|
691
|
+
continue
|
|
692
|
+
if eid in preserved_ids:
|
|
693
|
+
continue
|
|
694
|
+
replacement = current_by_id.get(str(eid))
|
|
695
|
+
if replacement is None:
|
|
696
|
+
path.unlink(missing_ok=True)
|
|
697
|
+
removed += 1
|
|
698
|
+
elif str(replacement.get("status")) != status:
|
|
699
|
+
# entry moved to another status dir; the new file was already
|
|
700
|
+
# written by the persist step, so remove the stale old file.
|
|
701
|
+
path.unlink(missing_ok=True)
|
|
702
|
+
removed += 1
|
|
703
|
+
return removed
|
|
704
|
+
|
|
705
|
+
|
|
653
706
|
def load_entries_from_dir(path: Path) -> list[dict[str, Any]]:
|
|
654
707
|
entries: list[dict[str, Any]] = []
|
|
655
708
|
if not path.exists():
|
|
@@ -676,7 +729,6 @@ def load_cached_archive_entries(
|
|
|
676
729
|
*,
|
|
677
730
|
summary_path: str,
|
|
678
731
|
summary_hash: str,
|
|
679
|
-
head_commit: str | None,
|
|
680
732
|
) -> list[dict[str, Any]] | None:
|
|
681
733
|
if not cache_path.exists():
|
|
682
734
|
return None
|
|
@@ -692,8 +744,9 @@ def load_cached_archive_entries(
|
|
|
692
744
|
return None
|
|
693
745
|
if payload.get("summarySha256") != summary_hash:
|
|
694
746
|
return None
|
|
695
|
-
|
|
696
|
-
|
|
747
|
+
# headCommit is intentionally ignored: archive extraction depends only on the
|
|
748
|
+
# summary-data.json content, so an unrelated HEAD change must not invalidate
|
|
749
|
+
# the extraction cache (design §3.5, cluster 6 — true incremental).
|
|
697
750
|
entries = payload.get("entries")
|
|
698
751
|
if not isinstance(entries, list):
|
|
699
752
|
return None
|
|
@@ -707,7 +760,6 @@ def write_cached_archive_entries(
|
|
|
707
760
|
*,
|
|
708
761
|
summary_path: str,
|
|
709
762
|
summary_hash: str,
|
|
710
|
-
head_commit: str | None,
|
|
711
763
|
entries: list[dict[str, Any]],
|
|
712
764
|
) -> None:
|
|
713
765
|
write_json(
|
|
@@ -717,7 +769,6 @@ def write_cached_archive_entries(
|
|
|
717
769
|
"generatedAt": now_iso(),
|
|
718
770
|
"summaryData": summary_path,
|
|
719
771
|
"summarySha256": summary_hash,
|
|
720
|
-
"headCommit": head_commit,
|
|
721
772
|
"entries": entries,
|
|
722
773
|
},
|
|
723
774
|
)
|
|
@@ -863,7 +914,25 @@ def calculate_confidence(entry: dict[str, Any], config: dict[str, Any]) -> dict[
|
|
|
863
914
|
|
|
864
915
|
def apply_confidence_scores(entries: list[dict[str, Any]], config: dict[str, Any]) -> None:
|
|
865
916
|
for entry in entries:
|
|
866
|
-
|
|
917
|
+
fresh = calculate_confidence(entry, config)
|
|
918
|
+
existing = entry.get("confidence") if isinstance(entry.get("confidence"), dict) else None
|
|
919
|
+
if existing is not None and (
|
|
920
|
+
existing.get("score") == fresh["score"]
|
|
921
|
+
and existing.get("level") == fresh["level"]
|
|
922
|
+
and existing.get("signals") == fresh["signals"]
|
|
923
|
+
):
|
|
924
|
+
# The inputs that determine confidence are unchanged; keep the previous
|
|
925
|
+
# lastCalculatedAt so the entry content (and mtime) does not churn on
|
|
926
|
+
# every ingest. This is what makes write-if-changed effective for
|
|
927
|
+
# unchanged entries.
|
|
928
|
+
entry["confidence"] = {
|
|
929
|
+
"score": fresh["score"],
|
|
930
|
+
"level": fresh["level"],
|
|
931
|
+
"signals": fresh["signals"],
|
|
932
|
+
"lastCalculatedAt": existing.get("lastCalculatedAt"),
|
|
933
|
+
}
|
|
934
|
+
else:
|
|
935
|
+
entry["confidence"] = fresh
|
|
867
936
|
|
|
868
937
|
|
|
869
938
|
def should_auto_promote(entry: dict[str, Any], policy: dict[str, Any]) -> bool:
|
|
@@ -926,14 +995,47 @@ def apply_auto_promote_policy(entries: list[dict[str, Any]], config: dict[str, A
|
|
|
926
995
|
return actions
|
|
927
996
|
|
|
928
997
|
|
|
929
|
-
def
|
|
998
|
+
def _preserve_confidence_timestamp(target: Path, entry: dict[str, Any]) -> None:
|
|
999
|
+
"""Keep the on-disk ``lastCalculatedAt`` when confidence is otherwise unchanged.
|
|
1000
|
+
|
|
1001
|
+
Extraction cache stores raw entries whose ``confidence`` is still the legacy
|
|
1002
|
+
string, so the first ``apply_confidence_scores`` of every build would mint a
|
|
1003
|
+
fresh ``lastCalculatedAt`` and churn unchanged entries. By re-reading the
|
|
1004
|
+
on-disk entry just before writing, an unchanged entry keeps its previous
|
|
1005
|
+
timestamp and ``write_json_if_changed`` becomes a true no-op (cluster 6).
|
|
1006
|
+
"""
|
|
1007
|
+
if not target.exists():
|
|
1008
|
+
return
|
|
1009
|
+
try:
|
|
1010
|
+
disk = read_json(target)
|
|
1011
|
+
except (OSError, json.JSONDecodeError):
|
|
1012
|
+
return
|
|
1013
|
+
if not isinstance(disk, dict) or disk.get("id") != entry.get("id"):
|
|
1014
|
+
return
|
|
1015
|
+
disk_conf = disk.get("confidence") if isinstance(disk.get("confidence"), dict) else None
|
|
1016
|
+
new_conf = entry.get("confidence") if isinstance(entry.get("confidence"), dict) else None
|
|
1017
|
+
if not (disk_conf and new_conf):
|
|
1018
|
+
return
|
|
1019
|
+
if (
|
|
1020
|
+
disk_conf.get("score") == new_conf.get("score")
|
|
1021
|
+
and disk_conf.get("level") == new_conf.get("level")
|
|
1022
|
+
and disk_conf.get("signals") == new_conf.get("signals")
|
|
1023
|
+
):
|
|
1024
|
+
entry["confidence"]["lastCalculatedAt"] = disk_conf.get("lastCalculatedAt")
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def persist_entry_updates(knowledge: Path, entries: list[dict[str, Any]]) -> int:
|
|
1028
|
+
written = 0
|
|
930
1029
|
for entry in entries:
|
|
931
1030
|
status = str(entry.get("status") or "")
|
|
932
1031
|
if status not in {"candidate", "active", "stale", "superseded", "conflicted"}:
|
|
933
1032
|
continue
|
|
934
1033
|
path = knowledge / "entries" / status / entry_filename(entry)
|
|
935
1034
|
if path.exists():
|
|
936
|
-
|
|
1035
|
+
_preserve_confidence_timestamp(path, entry)
|
|
1036
|
+
if write_json_if_changed(path, entry):
|
|
1037
|
+
written += 1
|
|
1038
|
+
return written
|
|
937
1039
|
|
|
938
1040
|
|
|
939
1041
|
def normalized_entry_text(entry: dict[str, Any]) -> str:
|
|
@@ -1614,21 +1716,196 @@ def apply_ttl_stale(entries: list[dict[str, Any]], config: dict[str, Any]) -> No
|
|
|
1614
1716
|
lifecycle["lastCheckedAt"] = now_iso()
|
|
1615
1717
|
|
|
1616
1718
|
|
|
1617
|
-
|
|
1719
|
+
SQLITE_SCHEMA_VERSION = 1
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def compute_inputs_hash(
|
|
1723
|
+
records: list[dict[str, Any]], config: dict[str, Any], knowledge: Path
|
|
1724
|
+
) -> str:
|
|
1725
|
+
"""Stable hash of the inputs that determine knowledge index content.
|
|
1726
|
+
|
|
1727
|
+
Excludes HEAD on purpose: a business-code commit that does not touch any
|
|
1728
|
+
archive must not invalidate the index (design §3.5, cluster 6 — true
|
|
1729
|
+
incremental). Inputs are archive checksums + knowledge config + schema
|
|
1730
|
+
versions + preserved entries (active / manually demoted), so promote/demote
|
|
1731
|
+
correctly invalidate the no-op fast path.
|
|
1732
|
+
"""
|
|
1733
|
+
archive_fingerprint = json.dumps(
|
|
1734
|
+
sorted(
|
|
1735
|
+
(
|
|
1736
|
+
{"path": r.get("summaryData"), "sha256": r.get("summarySha256")}
|
|
1737
|
+
for r in records
|
|
1738
|
+
),
|
|
1739
|
+
key=lambda item: item["path"] or "",
|
|
1740
|
+
),
|
|
1741
|
+
ensure_ascii=False,
|
|
1742
|
+
sort_keys=True,
|
|
1743
|
+
)
|
|
1744
|
+
config_fingerprint = json.dumps(config, ensure_ascii=False, sort_keys=True)
|
|
1745
|
+
schema_fingerprint = json.dumps(
|
|
1746
|
+
{
|
|
1747
|
+
"entrySchemaVersion": 1,
|
|
1748
|
+
"indexSchemaVersion": 1,
|
|
1749
|
+
"sqliteSchemaVersion": SQLITE_SCHEMA_VERSION,
|
|
1750
|
+
},
|
|
1751
|
+
sort_keys=True,
|
|
1752
|
+
)
|
|
1753
|
+
preserved = load_preserved_entries(knowledge)
|
|
1754
|
+
preserved_fingerprint = json.dumps(
|
|
1755
|
+
sorted(
|
|
1756
|
+
(
|
|
1757
|
+
{
|
|
1758
|
+
"id": e.get("id"),
|
|
1759
|
+
"status": e.get("status"),
|
|
1760
|
+
"lifecycle": e.get("lifecycle", {}),
|
|
1761
|
+
}
|
|
1762
|
+
for e in preserved
|
|
1763
|
+
),
|
|
1764
|
+
key=lambda item: item["id"] or "",
|
|
1765
|
+
),
|
|
1766
|
+
ensure_ascii=False,
|
|
1767
|
+
sort_keys=True,
|
|
1768
|
+
)
|
|
1769
|
+
digest = hashlib.sha256()
|
|
1770
|
+
digest.update(archive_fingerprint.encode("utf-8"))
|
|
1771
|
+
digest.update(b"|config|")
|
|
1772
|
+
digest.update(config_fingerprint.encode("utf-8"))
|
|
1773
|
+
digest.update(b"|schema|")
|
|
1774
|
+
digest.update(schema_fingerprint.encode("utf-8"))
|
|
1775
|
+
digest.update(b"|preserved|")
|
|
1776
|
+
digest.update(preserved_fingerprint.encode("utf-8"))
|
|
1777
|
+
return digest.hexdigest()
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
class KnowledgeSnapshot:
|
|
1781
|
+
"""Shared per-invocation snapshot (design §3.5 / cluster 6 要点2).
|
|
1782
|
+
|
|
1783
|
+
Loads config + archive records + inputs_hash once so auto/maintain/sync/query
|
|
1784
|
+
can pass it through instead of independently recomputing inputs_hash, reloading
|
|
1785
|
+
config, or rescanning archives. HEAD is intentionally not part of the snapshot
|
|
1786
|
+
-- an unrelated business-code commit must not invalidate the index.
|
|
1787
|
+
"""
|
|
1788
|
+
|
|
1789
|
+
__slots__ = (
|
|
1790
|
+
"project",
|
|
1791
|
+
"knowledge",
|
|
1792
|
+
"pname",
|
|
1793
|
+
"config",
|
|
1794
|
+
"summary_paths",
|
|
1795
|
+
"archive_records",
|
|
1796
|
+
"inputs_hash",
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
def __init__(
|
|
1800
|
+
self,
|
|
1801
|
+
project: Path,
|
|
1802
|
+
knowledge: Path,
|
|
1803
|
+
pname: str,
|
|
1804
|
+
config: dict[str, Any],
|
|
1805
|
+
summary_paths: list[Path],
|
|
1806
|
+
archive_records: list[dict[str, Any]],
|
|
1807
|
+
inputs_hash: str,
|
|
1808
|
+
) -> None:
|
|
1809
|
+
self.project = project
|
|
1810
|
+
self.knowledge = knowledge
|
|
1811
|
+
self.pname = pname
|
|
1812
|
+
self.config = config
|
|
1813
|
+
self.summary_paths = summary_paths
|
|
1814
|
+
self.archive_records = archive_records
|
|
1815
|
+
self.inputs_hash = inputs_hash
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
def build_snapshot(project: Path) -> KnowledgeSnapshot:
|
|
1819
|
+
"""Load config + archive records + inputs_hash exactly once for one invocation."""
|
|
1618
1820
|
project = project.resolve()
|
|
1619
|
-
|
|
1620
|
-
archive_root = harness / "archive"
|
|
1621
|
-
knowledge = harness / "knowledge"
|
|
1821
|
+
knowledge = project / ".harness" / "knowledge"
|
|
1622
1822
|
pname = project_id(project)
|
|
1623
1823
|
config = load_config(knowledge)
|
|
1624
|
-
|
|
1625
|
-
|
|
1824
|
+
summary_paths = sorted(
|
|
1825
|
+
(project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
|
|
1826
|
+
)
|
|
1827
|
+
archive_records = archive_summary_records(project, summary_paths)
|
|
1828
|
+
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
1829
|
+
return KnowledgeSnapshot(
|
|
1830
|
+
project, knowledge, pname, config, summary_paths, archive_records, inputs_hash
|
|
1831
|
+
)
|
|
1626
1832
|
|
|
1627
|
-
|
|
1833
|
+
|
|
1834
|
+
def build_index(
|
|
1835
|
+
project: Path,
|
|
1836
|
+
incremental: bool = True,
|
|
1837
|
+
*,
|
|
1838
|
+
snapshot: KnowledgeSnapshot | None = None,
|
|
1839
|
+
) -> dict[str, Any]:
|
|
1840
|
+
project = project.resolve()
|
|
1841
|
+
if snapshot is not None:
|
|
1842
|
+
# Reuse the single-invocation snapshot: no recomputation of
|
|
1843
|
+
# config / archive records / inputs_hash (design §3.5, cluster 6 要点2).
|
|
1844
|
+
knowledge = snapshot.knowledge
|
|
1845
|
+
pname = snapshot.pname
|
|
1846
|
+
config = snapshot.config
|
|
1847
|
+
summary_paths = snapshot.summary_paths
|
|
1848
|
+
archive_records = snapshot.archive_records
|
|
1849
|
+
inputs_hash = snapshot.inputs_hash
|
|
1850
|
+
else:
|
|
1851
|
+
knowledge = project / ".harness" / "knowledge"
|
|
1852
|
+
pname = project_id(project)
|
|
1853
|
+
config = load_config(knowledge)
|
|
1854
|
+
summary_paths = sorted(
|
|
1855
|
+
(project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
|
|
1856
|
+
)
|
|
1857
|
+
archive_records = archive_summary_records(project, summary_paths)
|
|
1858
|
+
inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
|
|
1859
|
+
ensure_knowledge_dirs(knowledge)
|
|
1860
|
+
|
|
1861
|
+
# No-op fast path: inputs (archive checksums + config + schema) are unchanged.
|
|
1862
|
+
# Write nothing — entries, sqlite, index and views all stay byte-identical,
|
|
1863
|
+
# so a repeated ingest is a true no-op (design §3.5, cluster 6, UT-025).
|
|
1864
|
+
# Also require index.sqlite to exist so a query never sees a stale index.json
|
|
1865
|
+
# pointing at a missing sqlite (API-009 single ensure-current).
|
|
1866
|
+
if incremental:
|
|
1867
|
+
old_index: dict[str, Any] | None = None
|
|
1868
|
+
index_path = knowledge / "index.json"
|
|
1869
|
+
if index_path.exists():
|
|
1870
|
+
try:
|
|
1871
|
+
old_index = read_json(index_path)
|
|
1872
|
+
except (OSError, json.JSONDecodeError):
|
|
1873
|
+
old_index = None
|
|
1874
|
+
if (
|
|
1875
|
+
isinstance(old_index, dict)
|
|
1876
|
+
and old_index.get("inputsHash") == inputs_hash
|
|
1877
|
+
and (knowledge / "index.sqlite").exists()
|
|
1878
|
+
):
|
|
1879
|
+
stale_mode = dict(old_index.get("ingestMode", {}))
|
|
1880
|
+
stale_mode.update(
|
|
1881
|
+
{
|
|
1882
|
+
"mode": "no-op",
|
|
1883
|
+
"incremental": incremental,
|
|
1884
|
+
"inputsHash": inputs_hash,
|
|
1885
|
+
"entriesWritten": 0,
|
|
1886
|
+
"entriesPruned": 0,
|
|
1887
|
+
"archivesExtracted": 0,
|
|
1888
|
+
"archivesReused": len(summary_paths),
|
|
1889
|
+
"cacheWrites": 0,
|
|
1890
|
+
"sqliteRebuild": 0,
|
|
1891
|
+
"sqliteUpsert": 0,
|
|
1892
|
+
"sqliteDelete": 0,
|
|
1893
|
+
}
|
|
1894
|
+
)
|
|
1895
|
+
result = dict(old_index)
|
|
1896
|
+
result["ingestMode"] = stale_mode
|
|
1897
|
+
return result
|
|
1898
|
+
|
|
1899
|
+
mode = "cold" if not (knowledge / "index.json").exists() else "warm"
|
|
1900
|
+
current_head = git_head(project) # recorded in manifest only; not an invalidation key
|
|
1628
1901
|
entries: list[dict[str, Any]] = []
|
|
1629
1902
|
failures: list[dict[str, str]] = []
|
|
1630
1903
|
ingest_mode: dict[str, Any] = {
|
|
1631
1904
|
"incremental": incremental,
|
|
1905
|
+
"mode": mode,
|
|
1906
|
+
"inputsHash": inputs_hash,
|
|
1907
|
+
"entriesWritten": 0,
|
|
1908
|
+
"entriesPruned": 0,
|
|
1632
1909
|
"archivesExtracted": 0,
|
|
1633
1910
|
"archivesReused": 0,
|
|
1634
1911
|
"cacheWrites": 0,
|
|
@@ -1638,6 +1915,9 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1638
1915
|
"validationChecked": 0,
|
|
1639
1916
|
"validationFailed": 0,
|
|
1640
1917
|
"validationAutoDemoted": 0,
|
|
1918
|
+
"sqliteRebuild": 0,
|
|
1919
|
+
"sqliteUpsert": 0,
|
|
1920
|
+
"sqliteDelete": 0,
|
|
1641
1921
|
}
|
|
1642
1922
|
|
|
1643
1923
|
for summary_path in summary_paths:
|
|
@@ -1651,7 +1931,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1651
1931
|
cache_path,
|
|
1652
1932
|
summary_path=summary_rel,
|
|
1653
1933
|
summary_hash=summary_hash,
|
|
1654
|
-
head_commit=current_head,
|
|
1655
1934
|
)
|
|
1656
1935
|
if archive_entries is None:
|
|
1657
1936
|
archive_entries = extract_entries(project, pname, summary_path)
|
|
@@ -1661,7 +1940,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1661
1940
|
cache_path,
|
|
1662
1941
|
summary_path=summary_rel,
|
|
1663
1942
|
summary_hash=summary_hash,
|
|
1664
|
-
head_commit=current_head,
|
|
1665
1943
|
entries=archive_entries,
|
|
1666
1944
|
)
|
|
1667
1945
|
ingest_mode["cacheWrites"] += 1
|
|
@@ -1716,11 +1994,13 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1716
1994
|
failures.append({"id": entry["id"], "reason": "filename collision",
|
|
1717
1995
|
"path": str(target), "conflictsWith": existing.get("id")})
|
|
1718
1996
|
continue
|
|
1719
|
-
|
|
1997
|
+
_preserve_confidence_timestamp(target, entry)
|
|
1998
|
+
if write_json_if_changed(target, entry):
|
|
1999
|
+
ingest_mode["entriesWritten"] += 1
|
|
1720
2000
|
|
|
1721
2001
|
indexed_entries = combine_generated_with_preserved(knowledge, deduped)
|
|
1722
2002
|
apply_confidence_scores(indexed_entries, config)
|
|
1723
|
-
persist_entry_updates(knowledge, indexed_entries)
|
|
2003
|
+
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
1724
2004
|
auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
|
|
1725
2005
|
ingest_mode["activeAutoDemoted"] = len(auto_demotions)
|
|
1726
2006
|
validation = apply_knowledge_validation(project, knowledge, config)
|
|
@@ -1732,10 +2012,12 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1732
2012
|
if validation["checked"]:
|
|
1733
2013
|
indexed_entries = [entry for _, entry in load_entry_files(knowledge)]
|
|
1734
2014
|
apply_confidence_scores(indexed_entries, config)
|
|
1735
|
-
persist_entry_updates(knowledge, indexed_entries)
|
|
2015
|
+
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
1736
2016
|
ingest_mode["confidenceScored"] = len(indexed_entries)
|
|
1737
|
-
|
|
1738
|
-
|
|
2017
|
+
ingest_mode["entriesPruned"] = prune_generated_entries(knowledge, indexed_entries)
|
|
2018
|
+
|
|
2019
|
+
sqlite_stats = write_sqlite(knowledge / "index.sqlite", indexed_entries)
|
|
2020
|
+
ingest_mode.update(sqlite_stats)
|
|
1739
2021
|
index = make_manifest(
|
|
1740
2022
|
project,
|
|
1741
2023
|
pname,
|
|
@@ -1746,7 +2028,7 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1746
2028
|
duplicates,
|
|
1747
2029
|
ingest_mode,
|
|
1748
2030
|
)
|
|
1749
|
-
|
|
2031
|
+
write_json_if_changed(knowledge / "index.json", index)
|
|
1750
2032
|
write_views(knowledge, index, indexed_entries)
|
|
1751
2033
|
write_ingest_report(knowledge, index, failures, duplicates)
|
|
1752
2034
|
return index
|
|
@@ -2009,6 +2291,7 @@ def make_manifest(
|
|
|
2009
2291
|
"projectId": pname,
|
|
2010
2292
|
"projectRoot": str(project),
|
|
2011
2293
|
"headCommit": git_head(project),
|
|
2294
|
+
"inputsHash": (ingest_mode or {}).get("inputsHash"),
|
|
2012
2295
|
"archives": {
|
|
2013
2296
|
"scanned": len(summary_paths),
|
|
2014
2297
|
"indexed": len(summary_paths) - len(failures),
|
|
@@ -2035,90 +2318,153 @@ def make_manifest(
|
|
|
2035
2318
|
}
|
|
2036
2319
|
|
|
2037
2320
|
|
|
2038
|
-
def write_sqlite(path: Path, entries: list[dict[str, Any]]) ->
|
|
2321
|
+
def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
2322
|
+
"""Persist entries to SQLite using a transactional dirty-set.
|
|
2323
|
+
|
|
2324
|
+
Reads existing rows, then upserts only changed entries and deletes removed
|
|
2325
|
+
ones within a single transaction. When the schema version changes or the
|
|
2326
|
+
table is missing, performs a full rebuild. If nothing changed the file is
|
|
2327
|
+
not touched (cluster 6, design §3.5 — true incremental).
|
|
2328
|
+
"""
|
|
2039
2329
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
2040
|
-
|
|
2041
|
-
|
|
2330
|
+
stats: dict[str, Any] = {
|
|
2331
|
+
"sqliteRebuild": 0,
|
|
2332
|
+
"sqliteUpsert": 0,
|
|
2333
|
+
"sqliteDelete": 0,
|
|
2334
|
+
"sqliteUnchanged": 0,
|
|
2335
|
+
}
|
|
2042
2336
|
con = sqlite3.connect(path)
|
|
2043
2337
|
try:
|
|
2044
2338
|
con.execute("pragma journal_mode=wal")
|
|
2045
|
-
con.execute(
|
|
2046
|
-
""
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
type text not null,
|
|
2051
|
-
status text not null,
|
|
2052
|
-
title text not null,
|
|
2053
|
-
summary text not null,
|
|
2054
|
-
body text not null,
|
|
2055
|
-
source_archive text not null,
|
|
2056
|
-
source_commit text,
|
|
2057
|
-
source_files_json text not null,
|
|
2058
|
-
keywords_json text not null,
|
|
2059
|
-
entry_json text not null
|
|
2060
|
-
)
|
|
2061
|
-
"""
|
|
2339
|
+
table_exists = con.execute(
|
|
2340
|
+
"select name from sqlite_master where type='table' and name='entries'"
|
|
2341
|
+
).fetchone()
|
|
2342
|
+
existing_version = (
|
|
2343
|
+
con.execute("pragma user_version").fetchone()[0] if table_exists else 0
|
|
2062
2344
|
)
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
"""
|
|
2072
|
-
)
|
|
2073
|
-
con.execute("create virtual table entries_fts using fts5(id, title, summary, body, keywords)")
|
|
2074
|
-
con.execute("create index idx_entries_status on entries(status)")
|
|
2075
|
-
con.execute("create index idx_entries_type on entries(type)")
|
|
2076
|
-
con.execute("create index idx_entries_source_archive on entries(source_archive)")
|
|
2077
|
-
con.execute("create index idx_entry_files_source_file on entry_files(source_file)")
|
|
2345
|
+
need_rebuild = (not table_exists) or existing_version != SQLITE_SCHEMA_VERSION
|
|
2346
|
+
|
|
2347
|
+
existing_map: dict[str, str] = {}
|
|
2348
|
+
if not need_rebuild:
|
|
2349
|
+
rows = con.execute("select id, entry_json from entries").fetchall()
|
|
2350
|
+
existing_map = {str(row[0]): str(row[1]) for row in rows}
|
|
2351
|
+
|
|
2352
|
+
new_map: dict[str, str] = {}
|
|
2078
2353
|
for entry in entries:
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2354
|
+
if isinstance(entry, dict) and entry.get("id"):
|
|
2355
|
+
new_map[str(entry["id"])] = json.dumps(entry, ensure_ascii=False)
|
|
2356
|
+
|
|
2357
|
+
to_upsert = [eid for eid in new_map if new_map[eid] != existing_map.get(eid)]
|
|
2358
|
+
to_delete = [eid for eid in existing_map if eid not in new_map]
|
|
2359
|
+
stats["sqliteUnchanged"] = len(new_map) - len(to_upsert)
|
|
2360
|
+
|
|
2361
|
+
if not need_rebuild and not to_upsert and not to_delete:
|
|
2362
|
+
# nothing to do; leave the file (and its mtime) untouched
|
|
2363
|
+
return stats
|
|
2364
|
+
|
|
2365
|
+
if need_rebuild:
|
|
2366
|
+
con.execute("drop table if exists entries")
|
|
2367
|
+
con.execute("drop table if exists entry_files")
|
|
2368
|
+
con.execute("drop table if exists entries_fts")
|
|
2082
2369
|
con.execute(
|
|
2083
2370
|
"""
|
|
2084
|
-
|
|
2085
|
-
id
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
source_files_json,
|
|
2100
|
-
keywords_json,
|
|
2101
|
-
entry_json,
|
|
2102
|
-
),
|
|
2371
|
+
create table entries (
|
|
2372
|
+
id text primary key,
|
|
2373
|
+
project_id text not null,
|
|
2374
|
+
type text not null,
|
|
2375
|
+
status text not null,
|
|
2376
|
+
title text not null,
|
|
2377
|
+
summary text not null,
|
|
2378
|
+
body text not null,
|
|
2379
|
+
source_archive text not null,
|
|
2380
|
+
source_commit text,
|
|
2381
|
+
source_files_json text not null,
|
|
2382
|
+
keywords_json text not null,
|
|
2383
|
+
entry_json text not null
|
|
2384
|
+
)
|
|
2385
|
+
"""
|
|
2103
2386
|
)
|
|
2104
2387
|
con.execute(
|
|
2105
|
-
"
|
|
2106
|
-
(
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2388
|
+
"""
|
|
2389
|
+
create table entry_files (
|
|
2390
|
+
entry_id text not null,
|
|
2391
|
+
source_file text not null,
|
|
2392
|
+
primary key (entry_id, source_file),
|
|
2393
|
+
foreign key (entry_id) references entries(id)
|
|
2394
|
+
)
|
|
2395
|
+
"""
|
|
2113
2396
|
)
|
|
2114
|
-
|
|
2397
|
+
con.execute("create virtual table entries_fts using fts5(id, title, summary, body, keywords)")
|
|
2398
|
+
con.execute("create index idx_entries_status on entries(status)")
|
|
2399
|
+
con.execute("create index idx_entries_type on entries(type)")
|
|
2400
|
+
con.execute("create index idx_entries_source_archive on entries(source_archive)")
|
|
2401
|
+
con.execute("create index idx_entry_files_source_file on entry_files(source_file)")
|
|
2402
|
+
con.execute(f"pragma user_version = {SQLITE_SCHEMA_VERSION}")
|
|
2403
|
+
stats["sqliteRebuild"] = 1
|
|
2404
|
+
# full rebuild => every entry must be (re)inserted
|
|
2405
|
+
to_upsert = list(new_map.keys())
|
|
2406
|
+
to_delete = []
|
|
2407
|
+
|
|
2408
|
+
con.execute("begin")
|
|
2409
|
+
try:
|
|
2410
|
+
for eid in to_delete:
|
|
2411
|
+
con.execute("delete from entries where id=?", (eid,))
|
|
2412
|
+
con.execute("delete from entries_fts where id=?", (eid,))
|
|
2413
|
+
con.execute("delete from entry_files where entry_id=?", (eid,))
|
|
2414
|
+
for entry in entries:
|
|
2415
|
+
eid = str(entry["id"])
|
|
2416
|
+
if eid not in to_upsert:
|
|
2417
|
+
continue
|
|
2418
|
+
source_files_json = json.dumps(entry["scope"]["sourceFiles"], ensure_ascii=False)
|
|
2419
|
+
keywords_json = json.dumps(entry["keywords"], ensure_ascii=False)
|
|
2420
|
+
con.execute(
|
|
2421
|
+
"""
|
|
2422
|
+
insert or replace into entries (
|
|
2423
|
+
id, project_id, type, status, title, summary, body, source_archive,
|
|
2424
|
+
source_commit, source_files_json, keywords_json, entry_json
|
|
2425
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2426
|
+
""",
|
|
2427
|
+
(
|
|
2428
|
+
entry["id"],
|
|
2429
|
+
entry["projectId"],
|
|
2430
|
+
entry["type"],
|
|
2431
|
+
entry["status"],
|
|
2432
|
+
entry["title"],
|
|
2433
|
+
entry["summary"],
|
|
2434
|
+
entry["body"],
|
|
2435
|
+
entry["source"]["archive"],
|
|
2436
|
+
entry["source"]["sourceCommit"],
|
|
2437
|
+
source_files_json,
|
|
2438
|
+
keywords_json,
|
|
2439
|
+
new_map[eid],
|
|
2440
|
+
),
|
|
2441
|
+
)
|
|
2442
|
+
con.execute("delete from entries_fts where id=?", (eid,))
|
|
2115
2443
|
con.execute(
|
|
2116
|
-
"insert
|
|
2117
|
-
(
|
|
2444
|
+
"insert into entries_fts (id, title, summary, body, keywords) values (?, ?, ?, ?, ?)",
|
|
2445
|
+
(
|
|
2446
|
+
entry["id"],
|
|
2447
|
+
entry["title"],
|
|
2448
|
+
entry["summary"],
|
|
2449
|
+
entry["body"],
|
|
2450
|
+
" ".join(entry["keywords"]),
|
|
2451
|
+
),
|
|
2118
2452
|
)
|
|
2119
|
-
|
|
2453
|
+
con.execute("delete from entry_files where entry_id=?", (eid,))
|
|
2454
|
+
for source_file in entry["scope"]["sourceFiles"]:
|
|
2455
|
+
con.execute(
|
|
2456
|
+
"insert or ignore into entry_files (entry_id, source_file) values (?, ?)",
|
|
2457
|
+
(eid, source_file),
|
|
2458
|
+
)
|
|
2459
|
+
stats["sqliteUpsert"] = len(to_upsert)
|
|
2460
|
+
stats["sqliteDelete"] = len(to_delete)
|
|
2461
|
+
con.commit()
|
|
2462
|
+
except Exception:
|
|
2463
|
+
con.rollback()
|
|
2464
|
+
raise
|
|
2120
2465
|
finally:
|
|
2121
2466
|
con.close()
|
|
2467
|
+
return stats
|
|
2122
2468
|
|
|
2123
2469
|
|
|
2124
2470
|
def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str, Any]]) -> None:
|
|
@@ -2141,7 +2487,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2141
2487
|
for entry in entries[:50]:
|
|
2142
2488
|
dashboard.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
|
|
2143
2489
|
dashboard.append(f" - source: `{entry['source']['archive']}`")
|
|
2144
|
-
|
|
2490
|
+
write_text_if_changed(knowledge / "views" / "knowledge-dashboard.md", "\n".join(dashboard) + "\n")
|
|
2145
2491
|
|
|
2146
2492
|
by_file: dict[str, list[dict[str, Any]]] = {}
|
|
2147
2493
|
for entry in entries:
|
|
@@ -2153,7 +2499,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2153
2499
|
for entry in by_file[source_file]:
|
|
2154
2500
|
lines.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
|
|
2155
2501
|
lines.append("")
|
|
2156
|
-
|
|
2502
|
+
write_text_if_changed(knowledge / "views" / "by-file.md", "\n".join(lines))
|
|
2157
2503
|
|
|
2158
2504
|
stale = [entry for entry in entries if entry["status"] == "stale"]
|
|
2159
2505
|
lines = ["# Harness Stale Knowledge", ""]
|
|
@@ -2163,7 +2509,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2163
2509
|
lines.append(f"- **{entry['type']}** {entry['title']}")
|
|
2164
2510
|
for reason in entry["lifecycle"]["staleReasons"]:
|
|
2165
2511
|
lines.append(f" - {reason}")
|
|
2166
|
-
|
|
2512
|
+
write_text_if_changed(knowledge / "views" / "stale-items.md", "\n".join(lines) + "\n")
|
|
2167
2513
|
|
|
2168
2514
|
superseded = [entry for entry in entries if entry["status"] == "superseded"]
|
|
2169
2515
|
lines = ["# Harness Superseded Knowledge", ""]
|
|
@@ -2176,7 +2522,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2176
2522
|
lines.append(f" - supersededBy: `{superseded_by}`")
|
|
2177
2523
|
for reason in entry["lifecycle"].get("staleReasons") or []:
|
|
2178
2524
|
lines.append(f" - {reason}")
|
|
2179
|
-
|
|
2525
|
+
write_text_if_changed(knowledge / "views" / "superseded-items.md", "\n".join(lines) + "\n")
|
|
2180
2526
|
|
|
2181
2527
|
conflicted = [entry for entry in entries if entry["status"] == "conflicted"]
|
|
2182
2528
|
lines = ["# Harness Conflicted Knowledge", ""]
|
|
@@ -2188,7 +2534,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2188
2534
|
lines.append(f" - conflictsWith: `{conflict_id}`")
|
|
2189
2535
|
for reason in entry["lifecycle"].get("staleReasons") or []:
|
|
2190
2536
|
lines.append(f" - {reason}")
|
|
2191
|
-
|
|
2537
|
+
write_text_if_changed(knowledge / "views" / "conflicted-items.md", "\n".join(lines) + "\n")
|
|
2192
2538
|
|
|
2193
2539
|
active_review = active_review_items(entries)
|
|
2194
2540
|
lines = ["# Harness Active Review", ""]
|
|
@@ -2199,7 +2545,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2199
2545
|
lines.append(f" - id: `{entry['id']}`")
|
|
2200
2546
|
for reason in entry.get("reviewReasons") or []:
|
|
2201
2547
|
lines.append(f" - {reason}")
|
|
2202
|
-
|
|
2548
|
+
write_text_if_changed(knowledge / "views" / "active-review.md", "\n".join(lines) + "\n")
|
|
2203
2549
|
|
|
2204
2550
|
base = [
|
|
2205
2551
|
'filters:',
|
|
@@ -2234,7 +2580,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2234
2580
|
' - file.folder',
|
|
2235
2581
|
' - file.mtime',
|
|
2236
2582
|
]
|
|
2237
|
-
|
|
2583
|
+
write_text_if_changed(knowledge / "views" / "knowledge.base", "\n".join(base) + "\n")
|
|
2238
2584
|
|
|
2239
2585
|
|
|
2240
2586
|
def write_ingest_report(
|
|
@@ -2639,11 +2985,12 @@ def query_index(
|
|
|
2639
2985
|
project = project.resolve()
|
|
2640
2986
|
knowledge = project / ".harness" / "knowledge"
|
|
2641
2987
|
sqlite_path = knowledge / "index.sqlite"
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2988
|
+
# API-009: one ensure-current. Build the shared snapshot once (inputs_hash
|
|
2989
|
+
# computed exactly once) and a single build_index call whose no-op fast path
|
|
2990
|
+
# keeps an up-to-date project a true no-op. Replaces the old sync_status +
|
|
2991
|
+
# build_index double orchestration, which computed inputs_hash twice.
|
|
2992
|
+
snapshot = build_snapshot(project)
|
|
2993
|
+
build_index(project, snapshot=snapshot)
|
|
2647
2994
|
entries = search_entries(sqlite_path, query, limit, file_filters, statuses, types)
|
|
2648
2995
|
context_path = write_context_pack(project, knowledge, query, entries)
|
|
2649
2996
|
filters = {
|
|
@@ -2738,9 +3085,14 @@ def sync_status(project: Path, update: bool = False, incremental: bool = True) -
|
|
|
2738
3085
|
if current_by_path[path].get("summarySha256") != indexed_by_path[path].get("summarySha256"):
|
|
2739
3086
|
reasons.append("archive checksum changed: " + str(path))
|
|
2740
3087
|
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3088
|
+
# HEAD is intentionally not an invalidation key (cluster 6, UT-027): a
|
|
3089
|
+
# business-code commit that does not touch any archive must not force a
|
|
3090
|
+
# rebuild. The archive checksum checks above already cover archive
|
|
3091
|
+
# changes; compare the full input fingerprint to catch config/schema
|
|
3092
|
+
# drift that the per-archive checks would miss.
|
|
3093
|
+
current_inputs_hash = compute_inputs_hash(current_records, load_config(knowledge), knowledge)
|
|
3094
|
+
if current_inputs_hash != index.get("inputsHash") and not reasons:
|
|
3095
|
+
reasons.append("knowledge inputs changed (config or schema)")
|
|
2744
3096
|
|
|
2745
3097
|
action = "none"
|
|
2746
3098
|
refreshed: dict[str, Any] | None = None
|