@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,541 @@
|
|
|
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
|
+
"""Tests for the hybrid keyword+embedding rerank path in semantic_index.
|
|
5
|
+
|
|
6
|
+
Covers the three P1 acceptance criteria:
|
|
7
|
+
1. Synonym/paraphrase: a query that keyword ranking PUTS WRONG-RANK
|
|
8
|
+
(or misses) gets the right top hit via dense rerank.
|
|
9
|
+
2. Graceful fallback: with no embed backend (force-fail), recall
|
|
10
|
+
returns keyword results and never raises.
|
|
11
|
+
3. Migration idempotency: re-running ``init()`` against an existing
|
|
12
|
+
DB is a no-op; pre-existing rows survive.
|
|
13
|
+
|
|
14
|
+
The tests inject a deterministic ``embed_fn`` so we don't depend on MLX
|
|
15
|
+
or Ollama being installed. The synonym test uses a tiny 4-dim vector
|
|
16
|
+
space where curated coordinates make ``alpha → beta`` cosine-close while
|
|
17
|
+
keyword overlap is zero.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sqlite3
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
HERE = Path(__file__).resolve().parent
|
|
28
|
+
SCRIPTS = HERE.parent
|
|
29
|
+
if str(SCRIPTS) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
31
|
+
|
|
32
|
+
from semantic_index import ( # noqa: E402
|
|
33
|
+
init,
|
|
34
|
+
query_facts,
|
|
35
|
+
upsert_fact,
|
|
36
|
+
)
|
|
37
|
+
from semantic_index.hybrid import ( # noqa: E402
|
|
38
|
+
_cosine,
|
|
39
|
+
_parse_embedding,
|
|
40
|
+
_safe_embed_query,
|
|
41
|
+
has_any_embedding,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------- helpers
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_db(tmp_path: Path) -> Path:
|
|
49
|
+
return tmp_path / "hybrid.sqlite"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Tiny 4-dim semantic space. Two clusters:
|
|
53
|
+
# - "vehicle" cluster: synonyms map to roughly the same direction.
|
|
54
|
+
# - "weather" cluster: orthogonal.
|
|
55
|
+
_VEC = {
|
|
56
|
+
# vehicle cluster
|
|
57
|
+
"auto": [1.0, 0.0, 0.0, 0.0],
|
|
58
|
+
"car": [0.95, 0.31, 0.0, 0.0],
|
|
59
|
+
"automobile": [0.92, 0.39, 0.0, 0.0],
|
|
60
|
+
"vehicle": [0.93, 0.37, 0.0, 0.0],
|
|
61
|
+
# weather cluster (orthogonal)
|
|
62
|
+
"rain": [0.0, 0.0, 1.0, 0.0],
|
|
63
|
+
"storm": [0.0, 0.0, 0.95, 0.31],
|
|
64
|
+
# generic/noise
|
|
65
|
+
"thing": [0.0, 0.0, 0.0, 1.0],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _embed_fake(text: str) -> list[float]:
|
|
70
|
+
"""Deterministic embedder: returns the curated vector for the first
|
|
71
|
+
known token in the text, else a neutral vector.
|
|
72
|
+
"""
|
|
73
|
+
tokens = text.lower().split()
|
|
74
|
+
for t in tokens:
|
|
75
|
+
t = t.strip(".,!?;:")
|
|
76
|
+
if t in _VEC:
|
|
77
|
+
return list(_VEC[t])
|
|
78
|
+
return [0.0, 0.0, 0.0, 1.0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------- 1. synonym
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_dense_rerank_surfaces_synonym_keyword_misses(tmp_path: Path) -> None:
|
|
85
|
+
"""The headline P1 acceptance: a query that keyword-only RANKS WRONG
|
|
86
|
+
gets the right top hit once dense rerank is on.
|
|
87
|
+
|
|
88
|
+
Setup: 3 facts.
|
|
89
|
+
F1 "auto cluster: a transport device" (semantically the right hit
|
|
90
|
+
for query "automobile")
|
|
91
|
+
F2 "thing about the auto cluster" (keyword decoy: matches the
|
|
92
|
+
query token "the" but not
|
|
93
|
+
semantically aligned)
|
|
94
|
+
F3 "rain weather report" (unrelated)
|
|
95
|
+
|
|
96
|
+
Query: "automobile". Keyword-only puts F2 ahead (decoy token "the" +
|
|
97
|
+
semantic-irrelevant noise) — dense rerank pushes F1 to the top
|
|
98
|
+
because ``automobile ≈ auto`` in cosine space.
|
|
99
|
+
"""
|
|
100
|
+
db = _make_db(tmp_path)
|
|
101
|
+
upsert_fact(
|
|
102
|
+
subject="F1",
|
|
103
|
+
predicate="cluster",
|
|
104
|
+
object_text="auto cluster device",
|
|
105
|
+
project="p",
|
|
106
|
+
embedding=_VEC["auto"],
|
|
107
|
+
db_path=db,
|
|
108
|
+
)
|
|
109
|
+
upsert_fact(
|
|
110
|
+
subject="F2",
|
|
111
|
+
predicate="thing",
|
|
112
|
+
object_text="the thing notes",
|
|
113
|
+
project="p",
|
|
114
|
+
embedding=_VEC["thing"],
|
|
115
|
+
db_path=db,
|
|
116
|
+
)
|
|
117
|
+
upsert_fact(
|
|
118
|
+
subject="F3",
|
|
119
|
+
predicate="weather",
|
|
120
|
+
object_text="rain weather report",
|
|
121
|
+
project="p",
|
|
122
|
+
embedding=_VEC["rain"],
|
|
123
|
+
db_path=db,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
keyword_results = query_facts(
|
|
127
|
+
query="automobile", project="p", db_path=db, mode="keyword",
|
|
128
|
+
)
|
|
129
|
+
hybrid_results = query_facts(
|
|
130
|
+
query="automobile",
|
|
131
|
+
project="p",
|
|
132
|
+
db_path=db,
|
|
133
|
+
mode="hybrid",
|
|
134
|
+
embed_fn=_embed_fake,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Control: keyword-only completely MISSES (the query token "automobile"
|
|
138
|
+
# has zero substring hits across F1/F2/F3).
|
|
139
|
+
assert keyword_results == [], (
|
|
140
|
+
f"keyword-only must miss this synonym query; got {[r['subject'] for r in keyword_results]}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Treatment: hybrid SURFACES F1 (auto ≈ automobile in cosine space).
|
|
144
|
+
assert hybrid_results, "hybrid mode must surface at least one result"
|
|
145
|
+
assert hybrid_results[0]["subject"] == "F1", (
|
|
146
|
+
f"hybrid top result expected F1 (auto cluster device); got "
|
|
147
|
+
f"{[r['subject'] for r in hybrid_results]}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------- 2. fallback
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_fallback_to_keyword_when_embed_backend_unavailable(
|
|
155
|
+
tmp_path: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""When the embed backend raises, recall must NOT crash. It must
|
|
158
|
+
degrade to keyword ranking, return successfully, exit cleanly.
|
|
159
|
+
"""
|
|
160
|
+
db = _make_db(tmp_path)
|
|
161
|
+
upsert_fact(
|
|
162
|
+
subject="alpha",
|
|
163
|
+
predicate="describes",
|
|
164
|
+
object_text="contains the word adapter explicitly",
|
|
165
|
+
project="p",
|
|
166
|
+
embedding=_VEC["auto"],
|
|
167
|
+
db_path=db,
|
|
168
|
+
)
|
|
169
|
+
upsert_fact(
|
|
170
|
+
subject="beta",
|
|
171
|
+
predicate="describes",
|
|
172
|
+
object_text="unrelated cluster",
|
|
173
|
+
project="p",
|
|
174
|
+
embedding=_VEC["rain"],
|
|
175
|
+
db_path=db,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def broken_embed(_text: str) -> list[float]:
|
|
179
|
+
raise RuntimeError("simulated MLX + Ollama outage")
|
|
180
|
+
|
|
181
|
+
out = query_facts(
|
|
182
|
+
query="adapter",
|
|
183
|
+
project="p",
|
|
184
|
+
db_path=db,
|
|
185
|
+
mode="hybrid",
|
|
186
|
+
embed_fn=broken_embed,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Got at least one result back (alpha matches keyword "adapter") and
|
|
190
|
+
# no exception was raised — the headline fallback contract.
|
|
191
|
+
assert out, "fallback must return keyword results, not empty"
|
|
192
|
+
assert out[0]["subject"] == "alpha", out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_fallback_when_no_candidate_has_embedding(tmp_path: Path) -> None:
|
|
196
|
+
"""When zero candidates carry an embedding, hybrid must transparently
|
|
197
|
+
degrade to pure keyword ranking — never call the embedder.
|
|
198
|
+
|
|
199
|
+
NOTE: post-f1, ``upsert_fact`` auto-embeds on the write path by
|
|
200
|
+
default. To assert the no-embeddings-anywhere state we must opt out
|
|
201
|
+
via ``auto_embed=False`` (this is the legacy behavior). The new
|
|
202
|
+
production behavior is covered by
|
|
203
|
+
``test_production_path_synonym_hit_without_explicit_embedding``.
|
|
204
|
+
"""
|
|
205
|
+
db = _make_db(tmp_path)
|
|
206
|
+
upsert_fact(
|
|
207
|
+
subject="only-keyword-fact",
|
|
208
|
+
predicate="p",
|
|
209
|
+
object_text="adapter boundary lesson",
|
|
210
|
+
project="p",
|
|
211
|
+
db_path=db,
|
|
212
|
+
auto_embed=False,
|
|
213
|
+
)
|
|
214
|
+
calls = {"n": 0}
|
|
215
|
+
|
|
216
|
+
def tracker_embed(text: str) -> list[float]:
|
|
217
|
+
calls["n"] += 1
|
|
218
|
+
return _VEC.get("auto", [0.0, 0.0, 0.0, 1.0])
|
|
219
|
+
|
|
220
|
+
out = query_facts(
|
|
221
|
+
query="adapter",
|
|
222
|
+
project="p",
|
|
223
|
+
db_path=db,
|
|
224
|
+
mode="hybrid",
|
|
225
|
+
embed_fn=tracker_embed,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
assert out, "must return keyword candidates even with no embeddings"
|
|
229
|
+
assert out[0]["subject"] == "only-keyword-fact"
|
|
230
|
+
# The embedder was never invoked — no candidate had an embedding.
|
|
231
|
+
assert calls["n"] == 0, (
|
|
232
|
+
f"embed_fn should not be called when no candidates carry vectors; "
|
|
233
|
+
f"calls={calls['n']}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_safe_embed_query_swallows_runtime_errors() -> None:
|
|
238
|
+
"""Unit-level guard: ``_safe_embed_query`` must return None on any
|
|
239
|
+
raise. This is the seam that turns backend outages into fallback."""
|
|
240
|
+
|
|
241
|
+
def boom(_text: str) -> list[float]:
|
|
242
|
+
raise RuntimeError("backend down")
|
|
243
|
+
|
|
244
|
+
assert _safe_embed_query("anything", boom) is None
|
|
245
|
+
assert _safe_embed_query("", _embed_fake) is None
|
|
246
|
+
assert _safe_embed_query("auto", _embed_fake) == _VEC["auto"]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------- 3. migration
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_init_is_idempotent(tmp_path: Path) -> None:
|
|
253
|
+
"""Re-running ``init()`` against an existing DB is a no-op: pre-existing
|
|
254
|
+
rows survive, schema stays intact, no exception raised.
|
|
255
|
+
"""
|
|
256
|
+
db = _make_db(tmp_path)
|
|
257
|
+
# First init creates the table + indexes.
|
|
258
|
+
init(db)
|
|
259
|
+
upsert_fact(
|
|
260
|
+
subject="kept",
|
|
261
|
+
predicate="p",
|
|
262
|
+
object_text="should survive re-init",
|
|
263
|
+
project="p",
|
|
264
|
+
embedding=_VEC["auto"],
|
|
265
|
+
db_path=db,
|
|
266
|
+
)
|
|
267
|
+
# Second init must be a no-op (idempotent) — no schema reset.
|
|
268
|
+
init(db)
|
|
269
|
+
# Third init for good measure (proves N-times idempotency).
|
|
270
|
+
init(db)
|
|
271
|
+
|
|
272
|
+
# Verify the row survived.
|
|
273
|
+
conn = sqlite3.connect(str(db))
|
|
274
|
+
try:
|
|
275
|
+
rows = conn.execute(
|
|
276
|
+
"SELECT subject, embedding_json FROM semantic_facts"
|
|
277
|
+
).fetchall()
|
|
278
|
+
finally:
|
|
279
|
+
conn.close()
|
|
280
|
+
assert len(rows) == 1
|
|
281
|
+
assert rows[0][0] == "kept"
|
|
282
|
+
# embedding_json column exists AND the value persisted.
|
|
283
|
+
parsed = _parse_embedding(rows[0][1])
|
|
284
|
+
assert parsed is not None and parsed == _VEC["auto"]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_init_creates_embedding_column_on_fresh_db(tmp_path: Path) -> None:
|
|
288
|
+
"""Fresh DB must have ``embedding_json`` column from the start —
|
|
289
|
+
the embedding store IS the column, no separate migration step."""
|
|
290
|
+
db = _make_db(tmp_path)
|
|
291
|
+
init(db)
|
|
292
|
+
conn = sqlite3.connect(str(db))
|
|
293
|
+
try:
|
|
294
|
+
cols = {r[1] for r in conn.execute("PRAGMA table_info(semantic_facts)").fetchall()}
|
|
295
|
+
finally:
|
|
296
|
+
conn.close()
|
|
297
|
+
assert "embedding_json" in cols, f"missing embedding_json column; got {cols}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------- math
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_cosine_orthogonal_is_zero() -> None:
|
|
304
|
+
assert _cosine([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) == 0.0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_cosine_identical_is_one() -> None:
|
|
308
|
+
assert _cosine([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) == pytest.approx(1.0)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_cosine_zero_vector_returns_zero() -> None:
|
|
312
|
+
assert _cosine([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]) == 0.0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_parse_embedding_handles_corrupt_json() -> None:
|
|
316
|
+
assert _parse_embedding(None) is None
|
|
317
|
+
assert _parse_embedding("") is None
|
|
318
|
+
assert _parse_embedding("not json") is None
|
|
319
|
+
assert _parse_embedding('{"not": "a list"}') is None
|
|
320
|
+
assert _parse_embedding('[1, "bad", 3]') is None
|
|
321
|
+
assert _parse_embedding("[1.0, 2.0, 3.0]") == [1.0, 2.0, 3.0]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------- f3: has_any_embedding
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_has_any_embedding_skips_column_less_rows_finds_later_hit(tmp_path: Path) -> None:
|
|
328
|
+
"""Regression for the dormancy bug: a candidate row WITHOUT the column
|
|
329
|
+
must not short-circuit the probe. A later row WITH a valid embedding
|
|
330
|
+
must still cause the probe to return True.
|
|
331
|
+
|
|
332
|
+
Before the fix: ``has_any_embedding`` returned False on the first
|
|
333
|
+
column-less row, hybrid silently downgraded to keyword, and dense
|
|
334
|
+
rerank never fired against any heterogeneous candidate set.
|
|
335
|
+
"""
|
|
336
|
+
# Build two minimal sqlite3.Row stand-ins via a real SQLite conn.
|
|
337
|
+
conn = sqlite3.connect(":memory:")
|
|
338
|
+
conn.row_factory = sqlite3.Row
|
|
339
|
+
# Row 1: no embedding_json column at all.
|
|
340
|
+
conn.execute("CREATE TABLE no_emb (subject TEXT)")
|
|
341
|
+
conn.execute("INSERT INTO no_emb (subject) VALUES ('without-emb')")
|
|
342
|
+
row_without = conn.execute("SELECT * FROM no_emb").fetchone()
|
|
343
|
+
# Row 2: has embedding_json column and a valid vector.
|
|
344
|
+
conn.execute("CREATE TABLE with_emb (subject TEXT, embedding_json TEXT)")
|
|
345
|
+
conn.execute(
|
|
346
|
+
"INSERT INTO with_emb (subject, embedding_json) VALUES ('with-emb', ?)",
|
|
347
|
+
("[0.1, 0.2, 0.3]",),
|
|
348
|
+
)
|
|
349
|
+
row_with = conn.execute("SELECT * FROM with_emb").fetchone()
|
|
350
|
+
conn.close()
|
|
351
|
+
|
|
352
|
+
# Bug repro order: column-less row FIRST, embedded row SECOND.
|
|
353
|
+
candidates = [(1.0, row_without), (1.0, row_with)]
|
|
354
|
+
assert has_any_embedding(candidates) is True, (
|
|
355
|
+
"has_any_embedding must continue past column-less rows; "
|
|
356
|
+
"before the fix this returned False on the first row and hybrid "
|
|
357
|
+
"silently downgraded to keyword."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Sanity: all column-less → False.
|
|
361
|
+
assert has_any_embedding([(1.0, row_without)]) is False
|
|
362
|
+
|
|
363
|
+
# Sanity: all-embedded → True.
|
|
364
|
+
assert has_any_embedding([(1.0, row_with)]) is True
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------- f1/f2: production write+read path
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_production_path_synonym_hit_without_explicit_embedding(tmp_path: Path) -> None:
|
|
371
|
+
"""The headline f2 regression. ``upsert_fact`` is called WITHOUT
|
|
372
|
+
``embedding=`` and WITHOUT a curated vector — only an ``embed_fn``
|
|
373
|
+
that the write path uses to populate ``embedding_json`` automatically.
|
|
374
|
+
Then we read via the SAME default path (``query_facts``, no explicit
|
|
375
|
+
``embedding`` arg anywhere), inject the SAME embedder, and verify
|
|
376
|
+
that a synonym query (zero keyword overlap) returns the right hit.
|
|
377
|
+
|
|
378
|
+
This test would have FAILED before f1 because:
|
|
379
|
+
- upsert_fact stored embedding_json=NULL,
|
|
380
|
+
- has_any_embedding returned False (no candidate had a vector),
|
|
381
|
+
- hybrid silently degraded to keyword,
|
|
382
|
+
- keyword has zero overlap on the synonym query → empty result.
|
|
383
|
+
|
|
384
|
+
After f1: ``upsert_fact(auto_embed=True)`` (default) calls the
|
|
385
|
+
injected ``embed_fn`` and persists the vector, so the query hits.
|
|
386
|
+
"""
|
|
387
|
+
db = _make_db(tmp_path)
|
|
388
|
+
# NO explicit embedding= on the write — production code never passes one.
|
|
389
|
+
upsert_fact(
|
|
390
|
+
subject="F1",
|
|
391
|
+
predicate="cluster",
|
|
392
|
+
object_text="auto cluster device",
|
|
393
|
+
project="p",
|
|
394
|
+
db_path=db,
|
|
395
|
+
embed_fn=_embed_fake,
|
|
396
|
+
)
|
|
397
|
+
upsert_fact(
|
|
398
|
+
subject="F2",
|
|
399
|
+
predicate="thing",
|
|
400
|
+
object_text="the thing notes",
|
|
401
|
+
project="p",
|
|
402
|
+
db_path=db,
|
|
403
|
+
embed_fn=_embed_fake,
|
|
404
|
+
)
|
|
405
|
+
upsert_fact(
|
|
406
|
+
subject="F3",
|
|
407
|
+
predicate="weather",
|
|
408
|
+
object_text="rain weather report",
|
|
409
|
+
project="p",
|
|
410
|
+
db_path=db,
|
|
411
|
+
embed_fn=_embed_fake,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Verify embeddings were auto-populated on write.
|
|
415
|
+
conn = sqlite3.connect(str(db))
|
|
416
|
+
null_count = conn.execute(
|
|
417
|
+
"SELECT COUNT(*) FROM semantic_facts WHERE embedding_json IS NULL"
|
|
418
|
+
).fetchone()[0]
|
|
419
|
+
conn.close()
|
|
420
|
+
assert null_count == 0, (
|
|
421
|
+
f"production write path must auto-populate embedding_json; "
|
|
422
|
+
f"got {null_count} NULL rows"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Keyword-only sanity: synonym query has zero substring hits.
|
|
426
|
+
keyword_results = query_facts(
|
|
427
|
+
query="automobile", project="p", db_path=db, mode="keyword",
|
|
428
|
+
)
|
|
429
|
+
assert keyword_results == []
|
|
430
|
+
|
|
431
|
+
# Default hybrid path — same embed_fn on the read side, no explicit
|
|
432
|
+
# embedding anywhere. Without f1's fix, this would also be empty.
|
|
433
|
+
hybrid_results = query_facts(
|
|
434
|
+
query="automobile",
|
|
435
|
+
project="p",
|
|
436
|
+
db_path=db,
|
|
437
|
+
mode="hybrid",
|
|
438
|
+
embed_fn=_embed_fake,
|
|
439
|
+
)
|
|
440
|
+
assert hybrid_results, (
|
|
441
|
+
"production path synonym hit failed — hybrid is dormant. "
|
|
442
|
+
"Auto-embed-on-write + read-path embed_fn must both fire."
|
|
443
|
+
)
|
|
444
|
+
assert hybrid_results[0]["subject"] == "F1", (
|
|
445
|
+
f"expected F1 (auto cluster) as the top synonym hit; got "
|
|
446
|
+
f"{[r['subject'] for r in hybrid_results]}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_upsert_fact_auto_embed_false_keeps_null(tmp_path: Path) -> None:
|
|
451
|
+
"""Opt-out preserves legacy behavior — used by the backfill path
|
|
452
|
+
(which controls the embedder itself) and by tests that want to
|
|
453
|
+
assert the NULL-embedding-then-backfill flow."""
|
|
454
|
+
db = _make_db(tmp_path)
|
|
455
|
+
upsert_fact(
|
|
456
|
+
subject="no-vec",
|
|
457
|
+
predicate="p",
|
|
458
|
+
object_text="text",
|
|
459
|
+
project="p",
|
|
460
|
+
db_path=db,
|
|
461
|
+
embed_fn=_embed_fake, # provided but ignored
|
|
462
|
+
auto_embed=False,
|
|
463
|
+
)
|
|
464
|
+
conn = sqlite3.connect(str(db))
|
|
465
|
+
raw = conn.execute(
|
|
466
|
+
"SELECT embedding_json FROM semantic_facts WHERE subject='no-vec'"
|
|
467
|
+
).fetchone()[0]
|
|
468
|
+
conn.close()
|
|
469
|
+
assert raw is None
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_upsert_fact_write_path_swallows_embed_failure(tmp_path: Path) -> None:
|
|
473
|
+
"""Write contract: backend down on auto-embed MUST NOT raise. Row
|
|
474
|
+
lands with NULL embedding_json; backfill can populate later."""
|
|
475
|
+
db = _make_db(tmp_path)
|
|
476
|
+
|
|
477
|
+
def broken(_t: str) -> list[float]:
|
|
478
|
+
raise RuntimeError("simulated backend down at write time")
|
|
479
|
+
|
|
480
|
+
# Must not raise — this is the production-availability contract.
|
|
481
|
+
upsert_fact(
|
|
482
|
+
subject="written-despite-outage",
|
|
483
|
+
predicate="p",
|
|
484
|
+
object_text="content",
|
|
485
|
+
project="p",
|
|
486
|
+
db_path=db,
|
|
487
|
+
embed_fn=broken,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
conn = sqlite3.connect(str(db))
|
|
491
|
+
raw = conn.execute(
|
|
492
|
+
"SELECT embedding_json FROM semantic_facts WHERE subject='written-despite-outage'"
|
|
493
|
+
).fetchone()[0]
|
|
494
|
+
conn.close()
|
|
495
|
+
assert raw is None # NULL — backfill can pick this up later.
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_read_semantic_passes_embed_fn_through_to_query_facts(tmp_path: Path) -> None:
|
|
499
|
+
"""The f1(b) wiring: ``memory_facade.semantic.read_semantic`` must
|
|
500
|
+
forward an ``embed_fn`` to ``query_facts`` so hybrid rerank actually
|
|
501
|
+
fires on the production read path.
|
|
502
|
+
|
|
503
|
+
Verified by writing rows that pass synonym hits via embedding (zero
|
|
504
|
+
keyword overlap), then calling read_semantic with the same embedder.
|
|
505
|
+
"""
|
|
506
|
+
db = _make_db(tmp_path)
|
|
507
|
+
upsert_fact(
|
|
508
|
+
subject="F1",
|
|
509
|
+
predicate="cluster",
|
|
510
|
+
object_text="auto cluster device",
|
|
511
|
+
project="p",
|
|
512
|
+
db_path=db,
|
|
513
|
+
embed_fn=_embed_fake,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# We must point read_semantic's underlying query_facts at OUR temp DB.
|
|
517
|
+
# The facade resolves the path via semantic_index.default_db_path(); we
|
|
518
|
+
# monkey-patch _db_path for the duration of this call.
|
|
519
|
+
import semantic_index as si # noqa: PLC0415
|
|
520
|
+
from memory_facade import semantic as facade # noqa: PLC0415
|
|
521
|
+
|
|
522
|
+
original_path_fn = si._db_path
|
|
523
|
+
si._db_path = lambda _p=None: db # type: ignore[assignment]
|
|
524
|
+
try:
|
|
525
|
+
out, reasons = facade.read_semantic(
|
|
526
|
+
workdir=tmp_path,
|
|
527
|
+
query="automobile",
|
|
528
|
+
limit=5,
|
|
529
|
+
project="p",
|
|
530
|
+
skip_postgres=True,
|
|
531
|
+
embed_fn=_embed_fake,
|
|
532
|
+
)
|
|
533
|
+
finally:
|
|
534
|
+
si._db_path = original_path_fn
|
|
535
|
+
|
|
536
|
+
assert out, f"read_semantic must surface the synonym via embed_fn; reasons={reasons}"
|
|
537
|
+
assert out[0]["subject"] == "F1"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
pytest.main([__file__, "-v"])
|