@tyroneross/build-loop 0.30.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/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Local SQLite semantic-facts index.
|
|
5
|
+
|
|
6
|
+
The canonical memory store remains file-backed. This module provides the
|
|
7
|
+
local, rebuildable semantic index used when Postgres is absent or deliberately
|
|
8
|
+
disabled. It is stdlib-only so fresh package installs can bootstrap memory
|
|
9
|
+
without database credentials or optional Python packages.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sqlite3
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
HERE = Path(__file__).resolve().parent
|
|
22
|
+
SCRIPTS_DIR = HERE.parent
|
|
23
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from _paths import memory_indexes_dir # type: ignore # noqa: E402
|
|
28
|
+
except Exception: # noqa: BLE001
|
|
29
|
+
memory_indexes_dir = None # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
DB_FILENAME = "semantic_facts.sqlite"
|
|
32
|
+
SCHEMA_VERSION = "1.0.0"
|
|
33
|
+
GLOBAL_PROJECT_KEY = "__GLOBAL__"
|
|
34
|
+
TOKEN_RE = re.compile(r"[A-Za-z0-9_.:/-]+")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def now_iso() -> str:
|
|
38
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_db_path() -> Path:
|
|
42
|
+
if memory_indexes_dir is None:
|
|
43
|
+
return Path.home() / "dev" / "git-folder" / "build-loop-memory" / "indexes" / DB_FILENAME
|
|
44
|
+
return memory_indexes_dir() / DB_FILENAME
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _db_path(db_path: str | Path | None = None) -> Path:
|
|
48
|
+
return Path(db_path).expanduser().resolve() if db_path else default_db_path().expanduser().resolve()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _project_key(project: str | None) -> str:
|
|
52
|
+
return project if project else GLOBAL_PROJECT_KEY
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _json(value: Any) -> str:
|
|
56
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_iso(value: Any) -> float | None:
|
|
60
|
+
if not value:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(value, (int, float)):
|
|
63
|
+
return float(value) / 1000 if value > 10_000_000_000 else float(value)
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
try:
|
|
66
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
|
|
67
|
+
except ValueError:
|
|
68
|
+
return None
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def connect(db_path: str | Path | None = None) -> sqlite3.Connection:
|
|
73
|
+
path = _db_path(db_path)
|
|
74
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
conn = sqlite3.connect(str(path))
|
|
76
|
+
conn.row_factory = sqlite3.Row
|
|
77
|
+
init(conn)
|
|
78
|
+
return conn
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def init(conn_or_path: sqlite3.Connection | str | Path | None = None) -> Path | None:
|
|
82
|
+
owns_conn = not isinstance(conn_or_path, sqlite3.Connection)
|
|
83
|
+
if owns_conn:
|
|
84
|
+
path = _db_path(conn_or_path)
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
conn = sqlite3.connect(str(path))
|
|
87
|
+
else:
|
|
88
|
+
conn = conn_or_path
|
|
89
|
+
if conn is None:
|
|
90
|
+
raise RuntimeError("sqlite connection unavailable")
|
|
91
|
+
try:
|
|
92
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
93
|
+
conn.execute(
|
|
94
|
+
"""
|
|
95
|
+
CREATE TABLE IF NOT EXISTS semantic_facts (
|
|
96
|
+
subject TEXT NOT NULL,
|
|
97
|
+
project_key TEXT NOT NULL,
|
|
98
|
+
predicate TEXT NOT NULL DEFAULT '',
|
|
99
|
+
object TEXT NOT NULL DEFAULT '',
|
|
100
|
+
confidence REAL,
|
|
101
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
102
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
103
|
+
project TEXT,
|
|
104
|
+
tool TEXT,
|
|
105
|
+
task_category TEXT,
|
|
106
|
+
files_touched_json TEXT NOT NULL DEFAULT '[]',
|
|
107
|
+
confidence_source TEXT,
|
|
108
|
+
domain TEXT,
|
|
109
|
+
source_prefix TEXT,
|
|
110
|
+
embedding_json TEXT,
|
|
111
|
+
last_synced TEXT NOT NULL,
|
|
112
|
+
schema_version TEXT NOT NULL DEFAULT '1.0.0',
|
|
113
|
+
PRIMARY KEY(subject, project_key)
|
|
114
|
+
);
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
conn.execute(
|
|
118
|
+
"CREATE INDEX IF NOT EXISTS idx_semantic_project ON semantic_facts(project_key, status, last_synced DESC);"
|
|
119
|
+
)
|
|
120
|
+
conn.execute(
|
|
121
|
+
"CREATE INDEX IF NOT EXISTS idx_semantic_domain ON semantic_facts(domain, status, last_synced DESC);"
|
|
122
|
+
)
|
|
123
|
+
conn.commit()
|
|
124
|
+
finally:
|
|
125
|
+
if owns_conn:
|
|
126
|
+
conn.close()
|
|
127
|
+
return None if isinstance(conn_or_path, sqlite3.Connection) else _db_path(conn_or_path)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _embed_text_for_fact(
|
|
131
|
+
subject: str, predicate: str, object_text: str
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Canonical text fed to the write-path embedder.
|
|
134
|
+
|
|
135
|
+
Mirrors the haystack used by the keyword scorer (``subject + predicate +
|
|
136
|
+
object``) so a query that lexically matches the row is in the same
|
|
137
|
+
semantic neighborhood as the stored vector. The keyword scorer also
|
|
138
|
+
includes ``project / domain / tool`` but those are categorical labels —
|
|
139
|
+
embedding them inflates norm without adding semantic signal, so we
|
|
140
|
+
keep the embed text tight.
|
|
141
|
+
"""
|
|
142
|
+
parts = [subject or "", predicate or "", object_text or ""]
|
|
143
|
+
return " ".join(p for p in parts if p).strip()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _safe_embed_for_write(
|
|
147
|
+
text: str,
|
|
148
|
+
embed_fn: Any,
|
|
149
|
+
) -> list[float] | None:
|
|
150
|
+
"""Best-effort embed for the write path. Returns None on any failure.
|
|
151
|
+
|
|
152
|
+
Contract: writes MUST NEVER fail because the embed backend is down.
|
|
153
|
+
A NULL ``embedding_json`` row stays keyword-discoverable; the backfill
|
|
154
|
+
capability can populate it later when a backend is available.
|
|
155
|
+
|
|
156
|
+
``embed_fn=None`` lazily resolves ``embed_backend.embed`` so missing
|
|
157
|
+
optional MLX/Ollama deps don't fail at module import time.
|
|
158
|
+
"""
|
|
159
|
+
if not text:
|
|
160
|
+
return None
|
|
161
|
+
if embed_fn is None:
|
|
162
|
+
try:
|
|
163
|
+
from embed_backend import embed as _embed # type: ignore # noqa: PLC0415
|
|
164
|
+
except Exception: # noqa: BLE001
|
|
165
|
+
return None
|
|
166
|
+
embed_fn = _embed
|
|
167
|
+
try:
|
|
168
|
+
vec = embed_fn(text)
|
|
169
|
+
except Exception: # noqa: BLE001
|
|
170
|
+
return None
|
|
171
|
+
if not isinstance(vec, list) or not vec:
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
return [float(x) for x in vec]
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def upsert_fact(
|
|
180
|
+
*,
|
|
181
|
+
subject: str,
|
|
182
|
+
predicate: str,
|
|
183
|
+
object_text: str,
|
|
184
|
+
project: str | None,
|
|
185
|
+
confidence: float | None = None,
|
|
186
|
+
status: str = "active",
|
|
187
|
+
metadata: dict[str, Any] | None = None,
|
|
188
|
+
tool: str | None = None,
|
|
189
|
+
task_category: str | None = None,
|
|
190
|
+
files_touched: list[str] | None = None,
|
|
191
|
+
confidence_source: str | None = None,
|
|
192
|
+
domain: str | None = None,
|
|
193
|
+
source_prefix: str | None = None,
|
|
194
|
+
embedding: list[float] | None = None,
|
|
195
|
+
embed_fn: Any = None,
|
|
196
|
+
auto_embed: bool = True,
|
|
197
|
+
db_path: str | Path | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Insert/update a semantic fact.
|
|
200
|
+
|
|
201
|
+
Auto-embed contract (NEW — fixes P1 dormant-production bug):
|
|
202
|
+
- When ``embedding`` is explicitly supplied, it is stored as-is
|
|
203
|
+
(callers can pre-compute or opt into a specific vector).
|
|
204
|
+
- When ``embedding`` is None AND ``auto_embed=True`` (default):
|
|
205
|
+
synthesize the embed text from ``subject + predicate + object``
|
|
206
|
+
and call ``embed_fn`` (or ``embed_backend.embed`` when None).
|
|
207
|
+
Failure (backend down, import error, raise) → store NULL. Writes
|
|
208
|
+
NEVER fail because of an embed-backend outage.
|
|
209
|
+
- ``auto_embed=False``: byte-equivalent to the pre-P1 behavior —
|
|
210
|
+
used by the backfill script (which controls the embedder itself)
|
|
211
|
+
and by tests that want to assert NULL-embedding state.
|
|
212
|
+
|
|
213
|
+
Why opt-in-via-default rather than required: the failure mode this
|
|
214
|
+
fixes is "production rows never get embeddings" — opting in by
|
|
215
|
+
default is the only thing that makes hybrid actually fire against
|
|
216
|
+
live data. Making it optional preserves the legacy contract for
|
|
217
|
+
callers that intentionally don't want write-time embed cost.
|
|
218
|
+
"""
|
|
219
|
+
if not subject.strip():
|
|
220
|
+
raise ValueError("subject is required")
|
|
221
|
+
metadata = dict(metadata or {})
|
|
222
|
+
metadata.setdefault("last_synced", now_iso())
|
|
223
|
+
metadata.setdefault("schema_version", SCHEMA_VERSION)
|
|
224
|
+
files = [str(item) for item in (files_touched or [])]
|
|
225
|
+
|
|
226
|
+
# Auto-embed on write when caller didn't supply a vector. Best-effort;
|
|
227
|
+
# backend down → store NULL, fall back to keyword recall, never raise.
|
|
228
|
+
if embedding is None and auto_embed:
|
|
229
|
+
text = _embed_text_for_fact(subject, predicate, object_text)
|
|
230
|
+
embedding = _safe_embed_for_write(text, embed_fn)
|
|
231
|
+
|
|
232
|
+
conn = connect(db_path)
|
|
233
|
+
try:
|
|
234
|
+
conn.execute(
|
|
235
|
+
"""
|
|
236
|
+
INSERT INTO semantic_facts (
|
|
237
|
+
subject, project_key, predicate, object, confidence, status,
|
|
238
|
+
metadata_json, project, tool, task_category, files_touched_json,
|
|
239
|
+
confidence_source, domain, source_prefix, embedding_json,
|
|
240
|
+
last_synced, schema_version
|
|
241
|
+
)
|
|
242
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
243
|
+
ON CONFLICT(subject, project_key) DO UPDATE SET
|
|
244
|
+
predicate=excluded.predicate,
|
|
245
|
+
object=excluded.object,
|
|
246
|
+
confidence=excluded.confidence,
|
|
247
|
+
status=excluded.status,
|
|
248
|
+
metadata_json=excluded.metadata_json,
|
|
249
|
+
project=excluded.project,
|
|
250
|
+
tool=excluded.tool,
|
|
251
|
+
task_category=excluded.task_category,
|
|
252
|
+
files_touched_json=excluded.files_touched_json,
|
|
253
|
+
confidence_source=excluded.confidence_source,
|
|
254
|
+
domain=excluded.domain,
|
|
255
|
+
source_prefix=excluded.source_prefix,
|
|
256
|
+
embedding_json=excluded.embedding_json,
|
|
257
|
+
last_synced=excluded.last_synced,
|
|
258
|
+
schema_version=excluded.schema_version;
|
|
259
|
+
""",
|
|
260
|
+
(
|
|
261
|
+
subject,
|
|
262
|
+
_project_key(project),
|
|
263
|
+
predicate or "",
|
|
264
|
+
object_text or "",
|
|
265
|
+
confidence,
|
|
266
|
+
status or "active",
|
|
267
|
+
_json(metadata),
|
|
268
|
+
project,
|
|
269
|
+
tool,
|
|
270
|
+
task_category,
|
|
271
|
+
_json(files),
|
|
272
|
+
confidence_source,
|
|
273
|
+
domain,
|
|
274
|
+
source_prefix,
|
|
275
|
+
_json(embedding) if embedding else None,
|
|
276
|
+
metadata["last_synced"],
|
|
277
|
+
SCHEMA_VERSION,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
conn.commit()
|
|
281
|
+
finally:
|
|
282
|
+
conn.close()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def upsert_lesson(
|
|
286
|
+
*,
|
|
287
|
+
lesson: dict[str, Any],
|
|
288
|
+
project: str | None,
|
|
289
|
+
subject_prefix: str = "lesson:nav:",
|
|
290
|
+
confidence: float | None = None,
|
|
291
|
+
confidence_source: str | None = None,
|
|
292
|
+
embedding: list[float] | None = None,
|
|
293
|
+
embed_fn: Any = None,
|
|
294
|
+
auto_embed: bool = True,
|
|
295
|
+
db_path: str | Path | None = None,
|
|
296
|
+
tool: str = "navgator",
|
|
297
|
+
domain: str = "architecture",
|
|
298
|
+
) -> None:
|
|
299
|
+
lesson_id = str(lesson.get("id", "") or "").strip()
|
|
300
|
+
if not lesson_id:
|
|
301
|
+
raise ValueError("lesson id is required")
|
|
302
|
+
context = lesson.get("context") or {}
|
|
303
|
+
files_touched: list[str] = []
|
|
304
|
+
if isinstance(context, dict) and isinstance(context.get("files_affected"), list):
|
|
305
|
+
files_touched = [str(item) for item in context["files_affected"]]
|
|
306
|
+
promoted = bool(lesson.get("promoted", False))
|
|
307
|
+
metadata = {
|
|
308
|
+
"lesson_id": lesson_id,
|
|
309
|
+
"promoted": promoted,
|
|
310
|
+
"navgator_lesson": lesson,
|
|
311
|
+
}
|
|
312
|
+
upsert_fact(
|
|
313
|
+
subject=f"{subject_prefix}{lesson_id}",
|
|
314
|
+
predicate=str(lesson.get("category", "") or "uncategorized"),
|
|
315
|
+
object_text=str(lesson.get("pattern", "") or ""),
|
|
316
|
+
project=project,
|
|
317
|
+
confidence=confidence,
|
|
318
|
+
metadata=metadata,
|
|
319
|
+
tool=tool,
|
|
320
|
+
task_category="research",
|
|
321
|
+
files_touched=files_touched,
|
|
322
|
+
confidence_source=confidence_source,
|
|
323
|
+
domain=domain,
|
|
324
|
+
source_prefix=subject_prefix,
|
|
325
|
+
embedding=embedding,
|
|
326
|
+
embed_fn=embed_fn,
|
|
327
|
+
auto_embed=auto_embed,
|
|
328
|
+
db_path=db_path,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _tokens(query: str) -> list[str]:
|
|
333
|
+
return [m.group(0).lower() for m in TOKEN_RE.finditer(query or "")]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _score(row: sqlite3.Row, tokens: list[str]) -> int:
|
|
337
|
+
if not tokens:
|
|
338
|
+
return 1
|
|
339
|
+
haystack = " ".join(
|
|
340
|
+
str(row[key] or "") for key in ("subject", "predicate", "object", "project", "domain", "tool")
|
|
341
|
+
).lower()
|
|
342
|
+
return sum(1 for token in tokens if token in haystack)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
VALID_RECALL_MODES = ("hybrid", "keyword")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def query_facts(
|
|
349
|
+
*,
|
|
350
|
+
query: str = "",
|
|
351
|
+
limit: int = 10,
|
|
352
|
+
project: str | None = None,
|
|
353
|
+
db_path: str | Path | None = None,
|
|
354
|
+
mode: str = "hybrid",
|
|
355
|
+
embed_fn: Any = None,
|
|
356
|
+
) -> list[dict[str, Any]]:
|
|
357
|
+
"""Recall facts. Default mode is **hybrid** (keyword candidates → embedding rerank).
|
|
358
|
+
|
|
359
|
+
Pipeline:
|
|
360
|
+
1. Keyword scorer (token match count, overfetched to ``limit * 20``).
|
|
361
|
+
2. If ``mode='hybrid'`` AND an embed backend is available AND any
|
|
362
|
+
candidate has a persisted ``embedding_json`` — rerank by adding
|
|
363
|
+
cosine(query, candidate) to the keyword score.
|
|
364
|
+
3. Otherwise return the pure-keyword ranking.
|
|
365
|
+
|
|
366
|
+
Graceful fallback (never raises):
|
|
367
|
+
- ``mode='keyword'``: legacy behavior, byte-equivalent ranking.
|
|
368
|
+
- Embed backend down or no candidate has an embedding: degrades to
|
|
369
|
+
keyword ranking, returns successfully.
|
|
370
|
+
|
|
371
|
+
``embed_fn`` is injectable for tests/callers that want a deterministic
|
|
372
|
+
embedder. When None, ``embed_backend.embed`` is used lazily.
|
|
373
|
+
"""
|
|
374
|
+
if mode not in VALID_RECALL_MODES:
|
|
375
|
+
raise ValueError(f"unknown mode {mode!r}; valid: {VALID_RECALL_MODES}")
|
|
376
|
+
path = _db_path(db_path)
|
|
377
|
+
if not path.exists():
|
|
378
|
+
return []
|
|
379
|
+
conn = connect(path)
|
|
380
|
+
try:
|
|
381
|
+
where = ["status = 'active'"]
|
|
382
|
+
params: list[Any] = []
|
|
383
|
+
if project:
|
|
384
|
+
where.append("project = ?")
|
|
385
|
+
params.append(project)
|
|
386
|
+
rows = conn.execute(
|
|
387
|
+
f"""
|
|
388
|
+
SELECT rowid, subject, predicate, object, project, confidence,
|
|
389
|
+
last_synced, metadata_json, tool, domain, confidence_source,
|
|
390
|
+
embedding_json
|
|
391
|
+
FROM semantic_facts
|
|
392
|
+
WHERE {' AND '.join(where)}
|
|
393
|
+
ORDER BY last_synced DESC, rowid DESC
|
|
394
|
+
LIMIT ?
|
|
395
|
+
""",
|
|
396
|
+
[*params, max(limit * 20, limit)],
|
|
397
|
+
).fetchall()
|
|
398
|
+
finally:
|
|
399
|
+
conn.close()
|
|
400
|
+
tokens = _tokens(query)
|
|
401
|
+
candidates: list[tuple[float, sqlite3.Row]] = []
|
|
402
|
+
for row in rows:
|
|
403
|
+
score = _score(row, tokens)
|
|
404
|
+
if tokens and score <= 0:
|
|
405
|
+
# Hybrid mode admits embedding-only matches: include the row
|
|
406
|
+
# with a zero keyword score so a strong cosine match can still
|
|
407
|
+
# surface synonyms / paraphrases. Keyword mode preserves the
|
|
408
|
+
# legacy "must have at least one token hit" behavior.
|
|
409
|
+
if mode != "hybrid":
|
|
410
|
+
continue
|
|
411
|
+
if not _row_has_embedding(row):
|
|
412
|
+
continue
|
|
413
|
+
candidates.append((float(score), row))
|
|
414
|
+
|
|
415
|
+
use_hybrid = mode == "hybrid" and bool(query and query.strip())
|
|
416
|
+
if use_hybrid:
|
|
417
|
+
from .hybrid import ( # noqa: PLC0415
|
|
418
|
+
_safe_embed_query,
|
|
419
|
+
has_any_embedding,
|
|
420
|
+
rerank_candidates,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if has_any_embedding(candidates):
|
|
424
|
+
query_emb = _safe_embed_query(query, embed_fn)
|
|
425
|
+
if query_emb is not None:
|
|
426
|
+
ranked = rerank_candidates(candidates, query_emb)
|
|
427
|
+
else:
|
|
428
|
+
ranked = _keyword_sort(candidates)
|
|
429
|
+
else:
|
|
430
|
+
ranked = _keyword_sort(candidates)
|
|
431
|
+
else:
|
|
432
|
+
ranked = _keyword_sort(candidates)
|
|
433
|
+
|
|
434
|
+
out: list[dict[str, Any]] = []
|
|
435
|
+
for _, row in ranked[:limit]:
|
|
436
|
+
out.append({
|
|
437
|
+
"_kind": "semantic",
|
|
438
|
+
"_recency_ts": _parse_iso(row["last_synced"]),
|
|
439
|
+
"id": f"sqlite:{row['rowid']}",
|
|
440
|
+
"subject": row["subject"],
|
|
441
|
+
"predicate": row["predicate"],
|
|
442
|
+
"object": row["object"],
|
|
443
|
+
"project": row["project"],
|
|
444
|
+
"confidence": row["confidence"],
|
|
445
|
+
"last_accessed": row["last_synced"],
|
|
446
|
+
"backend": "sqlite",
|
|
447
|
+
"tool": row["tool"],
|
|
448
|
+
"domain": row["domain"],
|
|
449
|
+
"confidence_source": row["confidence_source"],
|
|
450
|
+
})
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _row_has_embedding(row: sqlite3.Row) -> bool:
|
|
455
|
+
try:
|
|
456
|
+
raw = row["embedding_json"]
|
|
457
|
+
except (IndexError, KeyError):
|
|
458
|
+
return False
|
|
459
|
+
return bool(raw)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _keyword_sort(
|
|
463
|
+
candidates: list[tuple[float, sqlite3.Row]],
|
|
464
|
+
) -> list[tuple[float, sqlite3.Row]]:
|
|
465
|
+
"""Legacy keyword ranking, byte-equivalent to pre-hybrid behavior."""
|
|
466
|
+
return sorted(
|
|
467
|
+
candidates,
|
|
468
|
+
key=lambda item: (item[0], _parse_iso(item[1]["last_synced"]) or 0),
|
|
469
|
+
reverse=True,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def stats(db_path: str | Path | None = None) -> dict[str, Any]:
|
|
474
|
+
path = _db_path(db_path)
|
|
475
|
+
if not path.exists():
|
|
476
|
+
return {"db_path": str(path), "exists": False, "rows": 0}
|
|
477
|
+
conn = connect(path)
|
|
478
|
+
try:
|
|
479
|
+
rows = conn.execute("SELECT COUNT(*) FROM semantic_facts").fetchone()[0]
|
|
480
|
+
finally:
|
|
481
|
+
conn.close()
|
|
482
|
+
return {"db_path": str(path), "exists": True, "rows": int(rows)}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Quick latency bench for the SQLite hybrid recall path.
|
|
5
|
+
|
|
6
|
+
Not a unit test — just a measurement harness so the P1 run report can
|
|
7
|
+
record real numbers (cold vs warm; keyword vs hybrid). Run directly:
|
|
8
|
+
|
|
9
|
+
PYTHONPATH=scripts python3 scripts/semantic_index/_bench_hybrid.py
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
HERE = Path(__file__).resolve().parent
|
|
19
|
+
SCRIPTS = HERE.parent
|
|
20
|
+
if str(SCRIPTS) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
22
|
+
|
|
23
|
+
from semantic_index import query_facts, upsert_fact # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_VEC_DIM = 1024
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _det_vec(seed: int) -> list[float]:
|
|
30
|
+
"""Deterministic 1024-dim vector seeded from an int."""
|
|
31
|
+
rng_state = seed
|
|
32
|
+
out: list[float] = []
|
|
33
|
+
for _ in range(_VEC_DIM):
|
|
34
|
+
rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
35
|
+
out.append((rng_state % 1000) / 1000.0 - 0.5)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _embed_det(text: str) -> list[float]:
|
|
40
|
+
return _det_vec(abs(hash(text)) & 0xFFFFFF)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _seed(db: Path, n: int) -> None:
|
|
44
|
+
"""Seed with explicit embeddings (legacy path) — used for read benches."""
|
|
45
|
+
for i in range(n):
|
|
46
|
+
upsert_fact(
|
|
47
|
+
subject=f"fact:{i}",
|
|
48
|
+
predicate="describes",
|
|
49
|
+
object_text=f"row {i} adapter boundary lesson token{i % 50}",
|
|
50
|
+
project="bench",
|
|
51
|
+
embedding=_det_vec(i),
|
|
52
|
+
db_path=db,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bench_write_path(db: Path, n: int) -> dict[str, float]:
|
|
57
|
+
"""Measure auto-embed-on-write latency delta vs explicit-embedding write.
|
|
58
|
+
|
|
59
|
+
Both paths persist the same vector to the same column; the only
|
|
60
|
+
difference is whether ``upsert_fact`` calls ``embed_fn`` internally
|
|
61
|
+
(auto path) or the caller pre-computed it (explicit path). The delta
|
|
62
|
+
is the write-time cost of the f1 dormancy fix.
|
|
63
|
+
|
|
64
|
+
Uses a stub embed_fn so the bench reflects the in-process overhead
|
|
65
|
+
of the new seam, not the (much larger) MLX/Ollama cold-load.
|
|
66
|
+
"""
|
|
67
|
+
from semantic_index import _embed_text_for_fact # noqa: PLC0415
|
|
68
|
+
|
|
69
|
+
# Path A: explicit embedding (legacy).
|
|
70
|
+
def write_explicit():
|
|
71
|
+
for i in range(n):
|
|
72
|
+
upsert_fact(
|
|
73
|
+
subject=f"explicit:{i}",
|
|
74
|
+
predicate="describes",
|
|
75
|
+
object_text=f"row {i} content",
|
|
76
|
+
project="bench-write",
|
|
77
|
+
embedding=_det_vec(i),
|
|
78
|
+
db_path=db,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Path B: auto-embed via injected embed_fn (production path post-f1).
|
|
82
|
+
def write_auto():
|
|
83
|
+
for i in range(n):
|
|
84
|
+
upsert_fact(
|
|
85
|
+
subject=f"auto:{i}",
|
|
86
|
+
predicate="describes",
|
|
87
|
+
object_text=f"row {i} content",
|
|
88
|
+
project="bench-write",
|
|
89
|
+
db_path=db,
|
|
90
|
+
embed_fn=_embed_det,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
t0 = time.perf_counter()
|
|
94
|
+
write_explicit()
|
|
95
|
+
explicit_ms = (time.perf_counter() - t0) * 1000
|
|
96
|
+
t1 = time.perf_counter()
|
|
97
|
+
write_auto()
|
|
98
|
+
auto_ms = (time.perf_counter() - t1) * 1000
|
|
99
|
+
return {
|
|
100
|
+
"explicit_total_ms": round(explicit_ms, 2),
|
|
101
|
+
"auto_total_ms": round(auto_ms, 2),
|
|
102
|
+
"explicit_per_row_ms": round(explicit_ms / n, 3),
|
|
103
|
+
"auto_per_row_ms": round(auto_ms / n, 3),
|
|
104
|
+
"delta_per_row_ms": round((auto_ms - explicit_ms) / n, 3),
|
|
105
|
+
"rows": n,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _bench(fn, runs: int) -> dict[str, float]:
|
|
110
|
+
"""Return ms timings (min/median/mean across runs)."""
|
|
111
|
+
samples: list[float] = []
|
|
112
|
+
for _ in range(runs):
|
|
113
|
+
t0 = time.perf_counter()
|
|
114
|
+
fn()
|
|
115
|
+
samples.append((time.perf_counter() - t0) * 1000)
|
|
116
|
+
samples.sort()
|
|
117
|
+
return {
|
|
118
|
+
"min_ms": round(samples[0], 2),
|
|
119
|
+
"median_ms": round(samples[len(samples) // 2], 2),
|
|
120
|
+
"mean_ms": round(sum(samples) / len(samples), 2),
|
|
121
|
+
"max_ms": round(samples[-1], 2),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> int:
|
|
126
|
+
rows_n = 200
|
|
127
|
+
runs_n = 20
|
|
128
|
+
with tempfile.TemporaryDirectory() as td:
|
|
129
|
+
db = Path(td) / "bench.sqlite"
|
|
130
|
+
t0 = time.perf_counter()
|
|
131
|
+
_seed(db, rows_n)
|
|
132
|
+
seed_ms = (time.perf_counter() - t0) * 1000
|
|
133
|
+
print(f"[seed] {rows_n} rows in {seed_ms:.1f}ms")
|
|
134
|
+
|
|
135
|
+
def run_keyword():
|
|
136
|
+
return query_facts(
|
|
137
|
+
query="adapter token17",
|
|
138
|
+
project="bench",
|
|
139
|
+
db_path=db,
|
|
140
|
+
mode="keyword",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def run_hybrid():
|
|
144
|
+
return query_facts(
|
|
145
|
+
query="adapter token17",
|
|
146
|
+
project="bench",
|
|
147
|
+
db_path=db,
|
|
148
|
+
mode="hybrid",
|
|
149
|
+
embed_fn=_embed_det,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# First call (cold), then warm samples.
|
|
153
|
+
cold_kw = _bench(run_keyword, runs=1)
|
|
154
|
+
warm_kw = _bench(run_keyword, runs=runs_n)
|
|
155
|
+
cold_hy = _bench(run_hybrid, runs=1)
|
|
156
|
+
warm_hy = _bench(run_hybrid, runs=runs_n)
|
|
157
|
+
|
|
158
|
+
print(f"[keyword] cold={cold_kw['min_ms']}ms warm={warm_kw}")
|
|
159
|
+
print(f"[hybrid] cold={cold_hy['min_ms']}ms warm={warm_hy}")
|
|
160
|
+
|
|
161
|
+
# Write-path bench on a separate DB so seeded rows don't pollute
|
|
162
|
+
# the read bench above.
|
|
163
|
+
write_db = Path(td) / "bench-write.sqlite"
|
|
164
|
+
wp = _bench_write_path(write_db, n=100)
|
|
165
|
+
print(
|
|
166
|
+
f"[write] explicit={wp['explicit_per_row_ms']}ms/row "
|
|
167
|
+
f"auto={wp['auto_per_row_ms']}ms/row "
|
|
168
|
+
f"delta=+{wp['delta_per_row_ms']}ms/row "
|
|
169
|
+
f"(n={wp['rows']})"
|
|
170
|
+
)
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
sys.exit(main())
|