@hunter-harness/workflow-harness 0.2.0 → 0.2.2
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 +35 -21
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +2 -2
- 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 +103 -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 +35 -21
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +2 -2
- 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 +103 -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 +35 -21
- package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +2 -2
- 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 +103 -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 +35 -21
- package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +2 -2
- 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 +103 -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 +35 -21
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +1 -1
- 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 +2 -2
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/claude-code/harness-test/pitfalls.md +16 -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 +103 -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 +35 -21
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +1 -1
- 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 +2 -2
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +16 -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 +103 -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 +35 -21
- package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +1 -1
- 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 +2 -2
- package/harness/bundles/java/codex/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/codex/harness-test/pitfalls.md +16 -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 +103 -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 +35 -21
- package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +53 -7
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
- 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 +1 -1
- 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 +2 -2
- package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -5
- package/harness/bundles/java/cursor/harness-test/pitfalls.md +16 -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 +103 -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 +46 -30
- package/harness/manifests/general/codebuddy.json +46 -30
- package/harness/manifests/general/codex.json +46 -30
- package/harness/manifests/general/cursor.json +46 -30
- package/harness/manifests/java/claude-code.json +47 -31
- package/harness/manifests/java/codebuddy.json +47 -31
- package/harness/manifests/java/codex.json +47 -31
- package/harness/manifests/java/cursor.json +47 -31
- 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
|
@@ -58,6 +58,20 @@ DEFAULT_AUTO_KNOWLEDGE_CONFIG = {
|
|
|
58
58
|
"supersededPenalty": 0.8,
|
|
59
59
|
"conflictPenalty": 0.8,
|
|
60
60
|
},
|
|
61
|
+
"activeLifecycle": {
|
|
62
|
+
"autoDemote": True,
|
|
63
|
+
"targetStatus": "stale",
|
|
64
|
+
},
|
|
65
|
+
"knowledgeValidation": {
|
|
66
|
+
"enabled": True,
|
|
67
|
+
"autoDemoteActive": True,
|
|
68
|
+
"defaultTargetStatus": "stale",
|
|
69
|
+
"allowCommandValidators": False,
|
|
70
|
+
"commandTimeoutSeconds": 60,
|
|
71
|
+
},
|
|
72
|
+
"judge": {
|
|
73
|
+
"maxCandidatesPerRun": 100,
|
|
74
|
+
},
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
|
|
@@ -83,6 +97,29 @@ def write_text(path: Path, text: str) -> None:
|
|
|
83
97
|
path.write_text(text, encoding="utf-8")
|
|
84
98
|
|
|
85
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
|
+
|
|
86
123
|
def json_clone(value: Any) -> Any:
|
|
87
124
|
return json.loads(json.dumps(value, ensure_ascii=False))
|
|
88
125
|
|
|
@@ -106,6 +143,9 @@ def ensure_auto_knowledge_config(knowledge: Path) -> dict[str, Any]:
|
|
|
106
143
|
"path": str(config_path),
|
|
107
144
|
"created": False,
|
|
108
145
|
"autoPromoteEnabled": auto_promote_config(config)["enabled"],
|
|
146
|
+
"autoDemoteEnabled": active_lifecycle_config(config)["autoDemote"],
|
|
147
|
+
"autoDemoteActiveEnabled": knowledge_validation_config(config)["autoDemoteActive"],
|
|
148
|
+
"maxCandidatesPerRun": judge_config(config)["maxCandidatesPerRun"],
|
|
109
149
|
"appliedBy": "existing",
|
|
110
150
|
"candidateAutoPromoted": None,
|
|
111
151
|
}
|
|
@@ -115,6 +155,9 @@ def ensure_auto_knowledge_config(knowledge: Path) -> dict[str, Any]:
|
|
|
115
155
|
"path": str(config_path),
|
|
116
156
|
"created": True,
|
|
117
157
|
"autoPromoteEnabled": True,
|
|
158
|
+
"autoDemoteEnabled": True,
|
|
159
|
+
"autoDemoteActiveEnabled": True,
|
|
160
|
+
"maxCandidatesPerRun": judge_config(config)["maxCandidatesPerRun"],
|
|
118
161
|
"appliedBy": None,
|
|
119
162
|
"candidateAutoPromoted": None,
|
|
120
163
|
}
|
|
@@ -185,6 +228,16 @@ def auto_promote_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
185
228
|
}
|
|
186
229
|
|
|
187
230
|
|
|
231
|
+
def judge_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
232
|
+
raw = config.get("judge")
|
|
233
|
+
if not isinstance(raw, dict):
|
|
234
|
+
raw = {}
|
|
235
|
+
max_candidates = int(raw.get("maxCandidatesPerRun") or 100)
|
|
236
|
+
return {
|
|
237
|
+
"maxCandidatesPerRun": max(1, max_candidates),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
188
241
|
def sha256_file(path: Path) -> str:
|
|
189
242
|
h = hashlib.sha256()
|
|
190
243
|
with path.open("rb") as f:
|
|
@@ -591,21 +644,7 @@ def looks_like_contract_path(path: str) -> bool:
|
|
|
591
644
|
return any(part in lower for part in ["openapi", "contract", "schema", "protocol", "api"])
|
|
592
645
|
|
|
593
646
|
|
|
594
|
-
def
|
|
595
|
-
for sub in ["entries/candidate", "entries/stale", "entries/conflicted", "entries/superseded"]:
|
|
596
|
-
path = knowledge / sub
|
|
597
|
-
if path.exists():
|
|
598
|
-
if sub in {"entries/candidate", "entries/stale"}:
|
|
599
|
-
for entry_path in sorted(path.glob("*.json")):
|
|
600
|
-
try:
|
|
601
|
-
entry = read_json(entry_path)
|
|
602
|
-
except (OSError, json.JSONDecodeError):
|
|
603
|
-
entry_path.unlink(missing_ok=True)
|
|
604
|
-
continue
|
|
605
|
-
if not isinstance(entry, dict) or not entry.get("lifecycle", {}).get("demotedAt"):
|
|
606
|
-
entry_path.unlink(missing_ok=True)
|
|
607
|
-
else:
|
|
608
|
-
shutil.rmtree(path)
|
|
647
|
+
def ensure_knowledge_dirs(knowledge: Path) -> None:
|
|
609
648
|
for sub in [
|
|
610
649
|
"entries/candidate",
|
|
611
650
|
"entries/active",
|
|
@@ -620,6 +659,50 @@ def reset_generated_knowledge(knowledge: Path) -> None:
|
|
|
620
659
|
(knowledge / sub).mkdir(parents=True, exist_ok=True)
|
|
621
660
|
|
|
622
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
|
+
|
|
623
706
|
def load_entries_from_dir(path: Path) -> list[dict[str, Any]]:
|
|
624
707
|
entries: list[dict[str, Any]] = []
|
|
625
708
|
if not path.exists():
|
|
@@ -646,7 +729,6 @@ def load_cached_archive_entries(
|
|
|
646
729
|
*,
|
|
647
730
|
summary_path: str,
|
|
648
731
|
summary_hash: str,
|
|
649
|
-
head_commit: str | None,
|
|
650
732
|
) -> list[dict[str, Any]] | None:
|
|
651
733
|
if not cache_path.exists():
|
|
652
734
|
return None
|
|
@@ -662,8 +744,9 @@ def load_cached_archive_entries(
|
|
|
662
744
|
return None
|
|
663
745
|
if payload.get("summarySha256") != summary_hash:
|
|
664
746
|
return None
|
|
665
|
-
|
|
666
|
-
|
|
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).
|
|
667
750
|
entries = payload.get("entries")
|
|
668
751
|
if not isinstance(entries, list):
|
|
669
752
|
return None
|
|
@@ -677,7 +760,6 @@ def write_cached_archive_entries(
|
|
|
677
760
|
*,
|
|
678
761
|
summary_path: str,
|
|
679
762
|
summary_hash: str,
|
|
680
|
-
head_commit: str | None,
|
|
681
763
|
entries: list[dict[str, Any]],
|
|
682
764
|
) -> None:
|
|
683
765
|
write_json(
|
|
@@ -687,7 +769,6 @@ def write_cached_archive_entries(
|
|
|
687
769
|
"generatedAt": now_iso(),
|
|
688
770
|
"summaryData": summary_path,
|
|
689
771
|
"summarySha256": summary_hash,
|
|
690
|
-
"headCommit": head_commit,
|
|
691
772
|
"entries": entries,
|
|
692
773
|
},
|
|
693
774
|
)
|
|
@@ -833,7 +914,25 @@ def calculate_confidence(entry: dict[str, Any], config: dict[str, Any]) -> dict[
|
|
|
833
914
|
|
|
834
915
|
def apply_confidence_scores(entries: list[dict[str, Any]], config: dict[str, Any]) -> None:
|
|
835
916
|
for entry in entries:
|
|
836
|
-
|
|
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
|
|
837
936
|
|
|
838
937
|
|
|
839
938
|
def should_auto_promote(entry: dict[str, Any], policy: dict[str, Any]) -> bool:
|
|
@@ -896,14 +995,47 @@ def apply_auto_promote_policy(entries: list[dict[str, Any]], config: dict[str, A
|
|
|
896
995
|
return actions
|
|
897
996
|
|
|
898
997
|
|
|
899
|
-
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
|
|
900
1029
|
for entry in entries:
|
|
901
1030
|
status = str(entry.get("status") or "")
|
|
902
1031
|
if status not in {"candidate", "active", "stale", "superseded", "conflicted"}:
|
|
903
1032
|
continue
|
|
904
1033
|
path = knowledge / "entries" / status / entry_filename(entry)
|
|
905
1034
|
if path.exists():
|
|
906
|
-
|
|
1035
|
+
_preserve_confidence_timestamp(path, entry)
|
|
1036
|
+
if write_json_if_changed(path, entry):
|
|
1037
|
+
written += 1
|
|
1038
|
+
return written
|
|
907
1039
|
|
|
908
1040
|
|
|
909
1041
|
def normalized_entry_text(entry: dict[str, Any]) -> str:
|
|
@@ -1584,21 +1716,196 @@ def apply_ttl_stale(entries: list[dict[str, Any]], config: dict[str, Any]) -> No
|
|
|
1584
1716
|
lifecycle["lastCheckedAt"] = now_iso()
|
|
1585
1717
|
|
|
1586
1718
|
|
|
1587
|
-
|
|
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."""
|
|
1588
1820
|
project = project.resolve()
|
|
1589
|
-
|
|
1590
|
-
archive_root = harness / "archive"
|
|
1591
|
-
knowledge = harness / "knowledge"
|
|
1821
|
+
knowledge = project / ".harness" / "knowledge"
|
|
1592
1822
|
pname = project_id(project)
|
|
1593
1823
|
config = load_config(knowledge)
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
+
)
|
|
1596
1832
|
|
|
1597
|
-
|
|
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
|
|
1598
1901
|
entries: list[dict[str, Any]] = []
|
|
1599
1902
|
failures: list[dict[str, str]] = []
|
|
1600
1903
|
ingest_mode: dict[str, Any] = {
|
|
1601
1904
|
"incremental": incremental,
|
|
1905
|
+
"mode": mode,
|
|
1906
|
+
"inputsHash": inputs_hash,
|
|
1907
|
+
"entriesWritten": 0,
|
|
1908
|
+
"entriesPruned": 0,
|
|
1602
1909
|
"archivesExtracted": 0,
|
|
1603
1910
|
"archivesReused": 0,
|
|
1604
1911
|
"cacheWrites": 0,
|
|
@@ -1608,6 +1915,9 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1608
1915
|
"validationChecked": 0,
|
|
1609
1916
|
"validationFailed": 0,
|
|
1610
1917
|
"validationAutoDemoted": 0,
|
|
1918
|
+
"sqliteRebuild": 0,
|
|
1919
|
+
"sqliteUpsert": 0,
|
|
1920
|
+
"sqliteDelete": 0,
|
|
1611
1921
|
}
|
|
1612
1922
|
|
|
1613
1923
|
for summary_path in summary_paths:
|
|
@@ -1621,7 +1931,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1621
1931
|
cache_path,
|
|
1622
1932
|
summary_path=summary_rel,
|
|
1623
1933
|
summary_hash=summary_hash,
|
|
1624
|
-
head_commit=current_head,
|
|
1625
1934
|
)
|
|
1626
1935
|
if archive_entries is None:
|
|
1627
1936
|
archive_entries = extract_entries(project, pname, summary_path)
|
|
@@ -1631,7 +1940,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1631
1940
|
cache_path,
|
|
1632
1941
|
summary_path=summary_rel,
|
|
1633
1942
|
summary_hash=summary_hash,
|
|
1634
|
-
head_commit=current_head,
|
|
1635
1943
|
entries=archive_entries,
|
|
1636
1944
|
)
|
|
1637
1945
|
ingest_mode["cacheWrites"] += 1
|
|
@@ -1686,11 +1994,13 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1686
1994
|
failures.append({"id": entry["id"], "reason": "filename collision",
|
|
1687
1995
|
"path": str(target), "conflictsWith": existing.get("id")})
|
|
1688
1996
|
continue
|
|
1689
|
-
|
|
1997
|
+
_preserve_confidence_timestamp(target, entry)
|
|
1998
|
+
if write_json_if_changed(target, entry):
|
|
1999
|
+
ingest_mode["entriesWritten"] += 1
|
|
1690
2000
|
|
|
1691
2001
|
indexed_entries = combine_generated_with_preserved(knowledge, deduped)
|
|
1692
2002
|
apply_confidence_scores(indexed_entries, config)
|
|
1693
|
-
persist_entry_updates(knowledge, indexed_entries)
|
|
2003
|
+
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
1694
2004
|
auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
|
|
1695
2005
|
ingest_mode["activeAutoDemoted"] = len(auto_demotions)
|
|
1696
2006
|
validation = apply_knowledge_validation(project, knowledge, config)
|
|
@@ -1702,10 +2012,12 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1702
2012
|
if validation["checked"]:
|
|
1703
2013
|
indexed_entries = [entry for _, entry in load_entry_files(knowledge)]
|
|
1704
2014
|
apply_confidence_scores(indexed_entries, config)
|
|
1705
|
-
persist_entry_updates(knowledge, indexed_entries)
|
|
2015
|
+
ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
|
|
1706
2016
|
ingest_mode["confidenceScored"] = len(indexed_entries)
|
|
1707
|
-
|
|
1708
|
-
|
|
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)
|
|
1709
2021
|
index = make_manifest(
|
|
1710
2022
|
project,
|
|
1711
2023
|
pname,
|
|
@@ -1716,7 +2028,7 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
|
|
|
1716
2028
|
duplicates,
|
|
1717
2029
|
ingest_mode,
|
|
1718
2030
|
)
|
|
1719
|
-
|
|
2031
|
+
write_json_if_changed(knowledge / "index.json", index)
|
|
1720
2032
|
write_views(knowledge, index, indexed_entries)
|
|
1721
2033
|
write_ingest_report(knowledge, index, failures, duplicates)
|
|
1722
2034
|
return index
|
|
@@ -1979,6 +2291,7 @@ def make_manifest(
|
|
|
1979
2291
|
"projectId": pname,
|
|
1980
2292
|
"projectRoot": str(project),
|
|
1981
2293
|
"headCommit": git_head(project),
|
|
2294
|
+
"inputsHash": (ingest_mode or {}).get("inputsHash"),
|
|
1982
2295
|
"archives": {
|
|
1983
2296
|
"scanned": len(summary_paths),
|
|
1984
2297
|
"indexed": len(summary_paths) - len(failures),
|
|
@@ -2005,90 +2318,153 @@ def make_manifest(
|
|
|
2005
2318
|
}
|
|
2006
2319
|
|
|
2007
2320
|
|
|
2008
|
-
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
|
+
"""
|
|
2009
2329
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
2010
|
-
|
|
2011
|
-
|
|
2330
|
+
stats: dict[str, Any] = {
|
|
2331
|
+
"sqliteRebuild": 0,
|
|
2332
|
+
"sqliteUpsert": 0,
|
|
2333
|
+
"sqliteDelete": 0,
|
|
2334
|
+
"sqliteUnchanged": 0,
|
|
2335
|
+
}
|
|
2012
2336
|
con = sqlite3.connect(path)
|
|
2013
2337
|
try:
|
|
2014
2338
|
con.execute("pragma journal_mode=wal")
|
|
2015
|
-
con.execute(
|
|
2016
|
-
""
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
type text not null,
|
|
2021
|
-
status text not null,
|
|
2022
|
-
title text not null,
|
|
2023
|
-
summary text not null,
|
|
2024
|
-
body text not null,
|
|
2025
|
-
source_archive text not null,
|
|
2026
|
-
source_commit text,
|
|
2027
|
-
source_files_json text not null,
|
|
2028
|
-
keywords_json text not null,
|
|
2029
|
-
entry_json text not null
|
|
2030
|
-
)
|
|
2031
|
-
"""
|
|
2032
|
-
)
|
|
2033
|
-
con.execute(
|
|
2034
|
-
"""
|
|
2035
|
-
create table entry_files (
|
|
2036
|
-
entry_id text not null,
|
|
2037
|
-
source_file text not null,
|
|
2038
|
-
primary key (entry_id, source_file),
|
|
2039
|
-
foreign key (entry_id) references entries(id)
|
|
2040
|
-
)
|
|
2041
|
-
"""
|
|
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
|
|
2042
2344
|
)
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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] = {}
|
|
2048
2353
|
for entry in entries:
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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")
|
|
2052
2369
|
con.execute(
|
|
2053
2370
|
"""
|
|
2054
|
-
|
|
2055
|
-
id
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
source_files_json,
|
|
2070
|
-
keywords_json,
|
|
2071
|
-
entry_json,
|
|
2072
|
-
),
|
|
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
|
+
"""
|
|
2073
2386
|
)
|
|
2074
2387
|
con.execute(
|
|
2075
|
-
"
|
|
2076
|
-
(
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
+
"""
|
|
2083
2396
|
)
|
|
2084
|
-
|
|
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)
|
|
2085
2420
|
con.execute(
|
|
2086
|
-
"
|
|
2087
|
-
(
|
|
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,))
|
|
2443
|
+
con.execute(
|
|
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
|
+
),
|
|
2088
2452
|
)
|
|
2089
|
-
|
|
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
|
|
2090
2465
|
finally:
|
|
2091
2466
|
con.close()
|
|
2467
|
+
return stats
|
|
2092
2468
|
|
|
2093
2469
|
|
|
2094
2470
|
def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str, Any]]) -> None:
|
|
@@ -2111,7 +2487,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2111
2487
|
for entry in entries[:50]:
|
|
2112
2488
|
dashboard.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
|
|
2113
2489
|
dashboard.append(f" - source: `{entry['source']['archive']}`")
|
|
2114
|
-
|
|
2490
|
+
write_text_if_changed(knowledge / "views" / "knowledge-dashboard.md", "\n".join(dashboard) + "\n")
|
|
2115
2491
|
|
|
2116
2492
|
by_file: dict[str, list[dict[str, Any]]] = {}
|
|
2117
2493
|
for entry in entries:
|
|
@@ -2123,7 +2499,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2123
2499
|
for entry in by_file[source_file]:
|
|
2124
2500
|
lines.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
|
|
2125
2501
|
lines.append("")
|
|
2126
|
-
|
|
2502
|
+
write_text_if_changed(knowledge / "views" / "by-file.md", "\n".join(lines))
|
|
2127
2503
|
|
|
2128
2504
|
stale = [entry for entry in entries if entry["status"] == "stale"]
|
|
2129
2505
|
lines = ["# Harness Stale Knowledge", ""]
|
|
@@ -2133,7 +2509,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2133
2509
|
lines.append(f"- **{entry['type']}** {entry['title']}")
|
|
2134
2510
|
for reason in entry["lifecycle"]["staleReasons"]:
|
|
2135
2511
|
lines.append(f" - {reason}")
|
|
2136
|
-
|
|
2512
|
+
write_text_if_changed(knowledge / "views" / "stale-items.md", "\n".join(lines) + "\n")
|
|
2137
2513
|
|
|
2138
2514
|
superseded = [entry for entry in entries if entry["status"] == "superseded"]
|
|
2139
2515
|
lines = ["# Harness Superseded Knowledge", ""]
|
|
@@ -2146,7 +2522,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2146
2522
|
lines.append(f" - supersededBy: `{superseded_by}`")
|
|
2147
2523
|
for reason in entry["lifecycle"].get("staleReasons") or []:
|
|
2148
2524
|
lines.append(f" - {reason}")
|
|
2149
|
-
|
|
2525
|
+
write_text_if_changed(knowledge / "views" / "superseded-items.md", "\n".join(lines) + "\n")
|
|
2150
2526
|
|
|
2151
2527
|
conflicted = [entry for entry in entries if entry["status"] == "conflicted"]
|
|
2152
2528
|
lines = ["# Harness Conflicted Knowledge", ""]
|
|
@@ -2158,7 +2534,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2158
2534
|
lines.append(f" - conflictsWith: `{conflict_id}`")
|
|
2159
2535
|
for reason in entry["lifecycle"].get("staleReasons") or []:
|
|
2160
2536
|
lines.append(f" - {reason}")
|
|
2161
|
-
|
|
2537
|
+
write_text_if_changed(knowledge / "views" / "conflicted-items.md", "\n".join(lines) + "\n")
|
|
2162
2538
|
|
|
2163
2539
|
active_review = active_review_items(entries)
|
|
2164
2540
|
lines = ["# Harness Active Review", ""]
|
|
@@ -2169,7 +2545,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2169
2545
|
lines.append(f" - id: `{entry['id']}`")
|
|
2170
2546
|
for reason in entry.get("reviewReasons") or []:
|
|
2171
2547
|
lines.append(f" - {reason}")
|
|
2172
|
-
|
|
2548
|
+
write_text_if_changed(knowledge / "views" / "active-review.md", "\n".join(lines) + "\n")
|
|
2173
2549
|
|
|
2174
2550
|
base = [
|
|
2175
2551
|
'filters:',
|
|
@@ -2204,7 +2580,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
|
|
|
2204
2580
|
' - file.folder',
|
|
2205
2581
|
' - file.mtime',
|
|
2206
2582
|
]
|
|
2207
|
-
|
|
2583
|
+
write_text_if_changed(knowledge / "views" / "knowledge.base", "\n".join(base) + "\n")
|
|
2208
2584
|
|
|
2209
2585
|
|
|
2210
2586
|
def write_ingest_report(
|
|
@@ -2513,7 +2889,7 @@ def auto_knowledge(
|
|
|
2513
2889
|
*,
|
|
2514
2890
|
limit: int = 20,
|
|
2515
2891
|
suggest_statuses: list[str] | None = None,
|
|
2516
|
-
apply_suggestions: bool =
|
|
2892
|
+
apply_suggestions: bool = True,
|
|
2517
2893
|
incremental: bool = True,
|
|
2518
2894
|
audit_limit: int = 10,
|
|
2519
2895
|
) -> dict[str, Any]:
|
|
@@ -2539,6 +2915,7 @@ def auto_knowledge(
|
|
|
2539
2915
|
)
|
|
2540
2916
|
verification = verify_knowledge(project)
|
|
2541
2917
|
audit = audit_entries(project, limit=audit_limit)
|
|
2918
|
+
ingest_mode = sync.get("index", {}).get("ingestMode", {}) if isinstance(sync.get("index"), dict) else {}
|
|
2542
2919
|
return {
|
|
2543
2920
|
"project": str(project),
|
|
2544
2921
|
"generatedAt": now_iso(),
|
|
@@ -2578,6 +2955,15 @@ def auto_knowledge(
|
|
|
2578
2955
|
"conflictReview": len(audit["conflictReview"]),
|
|
2579
2956
|
"activeReview": len(audit["activeReview"]),
|
|
2580
2957
|
},
|
|
2958
|
+
"lifecycle": {
|
|
2959
|
+
"validatorsApplied": suggestions["applied"],
|
|
2960
|
+
"candidateAutoPromoted": ingest_mode.get(
|
|
2961
|
+
"candidateAutoPromoted", config_summary.get("candidateAutoPromoted") or 0
|
|
2962
|
+
),
|
|
2963
|
+
"activeAutoDemoted": ingest_mode.get("activeAutoDemoted", 0),
|
|
2964
|
+
"validationAutoDemoted": ingest_mode.get("validationAutoDemoted", verification["autoDemoted"]),
|
|
2965
|
+
"pendingAgentJudge": len(audit["conflictReview"]) + len(audit["candidateReview"]),
|
|
2966
|
+
},
|
|
2581
2967
|
}
|
|
2582
2968
|
|
|
2583
2969
|
|
|
@@ -2599,11 +2985,12 @@ def query_index(
|
|
|
2599
2985
|
project = project.resolve()
|
|
2600
2986
|
knowledge = project / ".harness" / "knowledge"
|
|
2601
2987
|
sqlite_path = knowledge / "index.sqlite"
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
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)
|
|
2607
2994
|
entries = search_entries(sqlite_path, query, limit, file_filters, statuses, types)
|
|
2608
2995
|
context_path = write_context_pack(project, knowledge, query, entries)
|
|
2609
2996
|
filters = {
|
|
@@ -2698,9 +3085,14 @@ def sync_status(project: Path, update: bool = False, incremental: bool = True) -
|
|
|
2698
3085
|
if current_by_path[path].get("summarySha256") != indexed_by_path[path].get("summarySha256"):
|
|
2699
3086
|
reasons.append("archive checksum changed: " + str(path))
|
|
2700
3087
|
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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)")
|
|
2704
3096
|
|
|
2705
3097
|
action = "none"
|
|
2706
3098
|
refreshed: dict[str, Any] | None = None
|
|
@@ -3902,7 +4294,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3902
4294
|
default=[],
|
|
3903
4295
|
help="Only suggest validators for this lifecycle status",
|
|
3904
4296
|
)
|
|
3905
|
-
auto.add_argument(
|
|
4297
|
+
auto.add_argument(
|
|
4298
|
+
"--no-apply-suggestions",
|
|
4299
|
+
action="store_true",
|
|
4300
|
+
help="Do not write validator suggestions into entry JSON files (default applies them)",
|
|
4301
|
+
)
|
|
3906
4302
|
auto.add_argument("--no-incremental", action="store_true", help="Refresh without reusing the archive entry cache")
|
|
3907
4303
|
|
|
3908
4304
|
audit = sub.add_parser("audit", help="Generate review lists for candidate, stale, and superseded entries")
|
|
@@ -3999,7 +4395,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3999
4395
|
Path(args.project),
|
|
4000
4396
|
limit=args.limit,
|
|
4001
4397
|
suggest_statuses=args.suggest_statuses,
|
|
4002
|
-
apply_suggestions=args.
|
|
4398
|
+
apply_suggestions=not args.no_apply_suggestions,
|
|
4003
4399
|
incremental=not args.no_incremental,
|
|
4004
4400
|
audit_limit=args.audit_limit,
|
|
4005
4401
|
)
|