@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.
Files changed (389) hide show
  1. package/.agents/plugins/marketplace.json +10 -0
  2. package/.claude-plugin/marketplace.json +38 -0
  3. package/.claude-plugin/plugin.json +30 -0
  4. package/.codex-plugin/plugin.json +41 -0
  5. package/.cursor/rules/build-loop-surface.mdc +22 -0
  6. package/AGENTS.md +498 -0
  7. package/LICENSE +202 -0
  8. package/README.md +403 -0
  9. package/agents/alignment-checker.md +124 -0
  10. package/agents/api-assessor.md +136 -0
  11. package/agents/architecture-scout.md +185 -0
  12. package/agents/assessment-orchestrator.md +163 -0
  13. package/agents/build-orchestrator.md +198 -0
  14. package/agents/database-assessor.md +133 -0
  15. package/agents/design-contract-specialist.md +239 -0
  16. package/agents/fact-checker.md +69 -0
  17. package/agents/fix-critique.md +161 -0
  18. package/agents/frontend-assessor.md +136 -0
  19. package/agents/implementer.md +185 -0
  20. package/agents/independent-auditor.md +143 -0
  21. package/agents/mock-scanner.md +78 -0
  22. package/agents/optimize-runner.md +112 -0
  23. package/agents/overfitting-reviewer.md +96 -0
  24. package/agents/performance-assessor.md +159 -0
  25. package/agents/plan-critic.md +85 -0
  26. package/agents/promotion-reviewer.md +121 -0
  27. package/agents/recurring-pattern-detector.md +248 -0
  28. package/agents/retrospective-synthesizer.md +109 -0
  29. package/agents/root-cause-investigator.md +285 -0
  30. package/agents/scope-auditor.md +121 -0
  31. package/agents/security-reviewer.md +151 -0
  32. package/agents/self-improvement-architect.md +125 -0
  33. package/agents/synthesis-critic.md +99 -0
  34. package/agents/transcript-pattern-miner.md +165 -0
  35. package/agents/ui-validator.md +193 -0
  36. package/codex-skills/build-loop/SKILL.md +20 -0
  37. package/codex-skills/debug-loop/SKILL.md +18 -0
  38. package/codex-skills/handoff/SKILL.md +22 -0
  39. package/codex-skills/knowledge/SKILL.md +21 -0
  40. package/codex-skills/optimize/SKILL.md +18 -0
  41. package/codex-skills/research/SKILL.md +18 -0
  42. package/commands/agent-rally-point.md +301 -0
  43. package/commands/assess.md +41 -0
  44. package/commands/debug.md +20 -0
  45. package/commands/debugger-detail.md +34 -0
  46. package/commands/debugger-scan.md +15 -0
  47. package/commands/debugger-status.md +20 -0
  48. package/commands/debugger.md +32 -0
  49. package/commands/handoff.md +113 -0
  50. package/commands/knowledge-review.md +16 -0
  51. package/commands/optimize.md +31 -0
  52. package/commands/plan-verify.md +39 -0
  53. package/commands/promote-experiment.md +129 -0
  54. package/commands/research.md +22 -0
  55. package/commands/run.md +12 -0
  56. package/commands/self-review.md +51 -0
  57. package/commands/start-prd.md +57 -0
  58. package/commands/test.md +34 -0
  59. package/dist/src/assessment-orchestrator.d.ts +78 -0
  60. package/dist/src/assessment-orchestrator.d.ts.map +1 -0
  61. package/dist/src/assessment-orchestrator.js +304 -0
  62. package/dist/src/assessment-orchestrator.js.map +1 -0
  63. package/dist/src/audit-miner.d.ts +21 -0
  64. package/dist/src/audit-miner.d.ts.map +1 -0
  65. package/dist/src/audit-miner.js +421 -0
  66. package/dist/src/audit-miner.js.map +1 -0
  67. package/dist/src/batch-operations.d.ts +30 -0
  68. package/dist/src/batch-operations.d.ts.map +1 -0
  69. package/dist/src/batch-operations.js +339 -0
  70. package/dist/src/batch-operations.js.map +1 -0
  71. package/dist/src/config.d.ts +83 -0
  72. package/dist/src/config.d.ts.map +1 -0
  73. package/dist/src/config.js +176 -0
  74. package/dist/src/config.js.map +1 -0
  75. package/dist/src/context-engine.d.ts +45 -0
  76. package/dist/src/context-engine.d.ts.map +1 -0
  77. package/dist/src/context-engine.js +135 -0
  78. package/dist/src/context-engine.js.map +1 -0
  79. package/dist/src/debug-wrapper.d.ts +58 -0
  80. package/dist/src/debug-wrapper.d.ts.map +1 -0
  81. package/dist/src/debug-wrapper.js +285 -0
  82. package/dist/src/debug-wrapper.js.map +1 -0
  83. package/dist/src/index.d.ts +31 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +149 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/interactive-verifier.d.ts +26 -0
  88. package/dist/src/interactive-verifier.d.ts.map +1 -0
  89. package/dist/src/interactive-verifier.js +435 -0
  90. package/dist/src/interactive-verifier.js.map +1 -0
  91. package/dist/src/lessons.d.ts +59 -0
  92. package/dist/src/lessons.d.ts.map +1 -0
  93. package/dist/src/lessons.js +172 -0
  94. package/dist/src/lessons.js.map +1 -0
  95. package/dist/src/log-reader.d.ts +39 -0
  96. package/dist/src/log-reader.d.ts.map +1 -0
  97. package/dist/src/log-reader.js +291 -0
  98. package/dist/src/log-reader.js.map +1 -0
  99. package/dist/src/logger.d.ts +59 -0
  100. package/dist/src/logger.d.ts.map +1 -0
  101. package/dist/src/logger.js +328 -0
  102. package/dist/src/logger.js.map +1 -0
  103. package/dist/src/parallel-retrieval.d.ts +52 -0
  104. package/dist/src/parallel-retrieval.d.ts.map +1 -0
  105. package/dist/src/parallel-retrieval.js +309 -0
  106. package/dist/src/parallel-retrieval.js.map +1 -0
  107. package/dist/src/pattern-extractor.d.ts +30 -0
  108. package/dist/src/pattern-extractor.d.ts.map +1 -0
  109. package/dist/src/pattern-extractor.js +386 -0
  110. package/dist/src/pattern-extractor.js.map +1 -0
  111. package/dist/src/result-aggregator.d.ts +44 -0
  112. package/dist/src/result-aggregator.d.ts.map +1 -0
  113. package/dist/src/result-aggregator.js +315 -0
  114. package/dist/src/result-aggregator.js.map +1 -0
  115. package/dist/src/retrieval.d.ts +139 -0
  116. package/dist/src/retrieval.d.ts.map +1 -0
  117. package/dist/src/retrieval.js +724 -0
  118. package/dist/src/retrieval.js.map +1 -0
  119. package/dist/src/setup/auto-setup.d.ts +15 -0
  120. package/dist/src/setup/auto-setup.d.ts.map +1 -0
  121. package/dist/src/setup/auto-setup.js +157 -0
  122. package/dist/src/setup/auto-setup.js.map +1 -0
  123. package/dist/src/setup/configure-hooks.d.ts +8 -0
  124. package/dist/src/setup/configure-hooks.d.ts.map +1 -0
  125. package/dist/src/setup/configure-hooks.js +16 -0
  126. package/dist/src/setup/configure-hooks.js.map +1 -0
  127. package/dist/src/setup/create-slash-commands.d.ts +8 -0
  128. package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
  129. package/dist/src/setup/create-slash-commands.js +101 -0
  130. package/dist/src/setup/create-slash-commands.js.map +1 -0
  131. package/dist/src/setup/index.d.ts +8 -0
  132. package/dist/src/setup/index.d.ts.map +1 -0
  133. package/dist/src/setup/index.js +17 -0
  134. package/dist/src/setup/index.js.map +1 -0
  135. package/dist/src/setup/inject-claude-md.d.ts +10 -0
  136. package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
  137. package/dist/src/setup/inject-claude-md.js +150 -0
  138. package/dist/src/setup/inject-claude-md.js.map +1 -0
  139. package/dist/src/setup/uninstall.d.ts +22 -0
  140. package/dist/src/setup/uninstall.d.ts.map +1 -0
  141. package/dist/src/setup/uninstall.js +235 -0
  142. package/dist/src/setup/uninstall.js.map +1 -0
  143. package/dist/src/storage.d.ts +192 -0
  144. package/dist/src/storage.d.ts.map +1 -0
  145. package/dist/src/storage.js +1037 -0
  146. package/dist/src/storage.js.map +1 -0
  147. package/dist/src/string-similarity.d.ts +8 -0
  148. package/dist/src/string-similarity.d.ts.map +1 -0
  149. package/dist/src/string-similarity.js +57 -0
  150. package/dist/src/string-similarity.js.map +1 -0
  151. package/dist/src/traces/adapters/browser.d.ts +69 -0
  152. package/dist/src/traces/adapters/browser.d.ts.map +1 -0
  153. package/dist/src/traces/adapters/browser.js +280 -0
  154. package/dist/src/traces/adapters/browser.js.map +1 -0
  155. package/dist/src/traces/adapters/index.d.ts +23 -0
  156. package/dist/src/traces/adapters/index.d.ts.map +1 -0
  157. package/dist/src/traces/adapters/index.js +67 -0
  158. package/dist/src/traces/adapters/index.js.map +1 -0
  159. package/dist/src/traces/adapters/langchain.d.ts +42 -0
  160. package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
  161. package/dist/src/traces/adapters/langchain.js +151 -0
  162. package/dist/src/traces/adapters/langchain.js.map +1 -0
  163. package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
  164. package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
  165. package/dist/src/traces/adapters/opentelemetry.js +164 -0
  166. package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
  167. package/dist/src/traces/adapters/sentry.d.ts +58 -0
  168. package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
  169. package/dist/src/traces/adapters/sentry.js +212 -0
  170. package/dist/src/traces/adapters/sentry.js.map +1 -0
  171. package/dist/src/traces/index.d.ts +10 -0
  172. package/dist/src/traces/index.d.ts.map +1 -0
  173. package/dist/src/traces/index.js +45 -0
  174. package/dist/src/traces/index.js.map +1 -0
  175. package/dist/src/traces/storage.d.ts +78 -0
  176. package/dist/src/traces/storage.d.ts.map +1 -0
  177. package/dist/src/traces/storage.js +399 -0
  178. package/dist/src/traces/storage.js.map +1 -0
  179. package/dist/src/traces/summarizer.d.ts +83 -0
  180. package/dist/src/traces/summarizer.d.ts.map +1 -0
  181. package/dist/src/traces/summarizer.js +247 -0
  182. package/dist/src/traces/summarizer.js.map +1 -0
  183. package/dist/src/traces/types.d.ts +365 -0
  184. package/dist/src/traces/types.d.ts.map +1 -0
  185. package/dist/src/traces/types.js +14 -0
  186. package/dist/src/traces/types.js.map +1 -0
  187. package/dist/src/types.d.ts +495 -0
  188. package/dist/src/types.d.ts.map +1 -0
  189. package/dist/src/types.js +27 -0
  190. package/dist/src/types.js.map +1 -0
  191. package/docs/agent-surface-policy.md +45 -0
  192. package/docs/memory-setup.md +226 -0
  193. package/hooks/_arch_scan_bg.py +88 -0
  194. package/hooks/_plugin_drift_check_bg.py +85 -0
  195. package/hooks/_session_start_lib.sh +42 -0
  196. package/hooks/git/pre-push +141 -0
  197. package/hooks/hooks.json +126 -0
  198. package/hooks/post-commit +30 -0
  199. package/hooks/pre-commit +29 -0
  200. package/hooks/pre-edit-apppulse.sh +6 -0
  201. package/hooks/pre-edit-architecture.sh +106 -0
  202. package/hooks/pre-edit-rally-point.sh +92 -0
  203. package/hooks/session-start-apppulse.sh +6 -0
  204. package/hooks/session-start-architecture.sh +10 -0
  205. package/hooks/session-start-git-hooks.sh +12 -0
  206. package/hooks/session-start-plugin-drift.sh +27 -0
  207. package/hooks/session-start-rally-point.sh +44 -0
  208. package/hooks/session-start-retrieval.sh +24 -0
  209. package/hooks/session-start-worktree-gc.sh +105 -0
  210. package/hooks/test_apppulse_hooks.sh +6 -0
  211. package/hooks/test_rally_point_hooks.sh +388 -0
  212. package/hooks/test_sec008_hooks.sh +66 -0
  213. package/package.json +111 -0
  214. package/scripts/README.md +301 -0
  215. package/scripts/_db_url.py +61 -0
  216. package/scripts/_paths.py +445 -0
  217. package/scripts/blm.py +162 -0
  218. package/scripts/blm_api.py +263 -0
  219. package/scripts/check_cache_sync.py +490 -0
  220. package/scripts/embed_backend.py +475 -0
  221. package/scripts/install_memory.py +556 -0
  222. package/scripts/lessons_index/__init__.py +77 -0
  223. package/scripts/lessons_index/__main__.py +135 -0
  224. package/scripts/lessons_index/ingest.py +288 -0
  225. package/scripts/lessons_index/query.py +285 -0
  226. package/scripts/lessons_index/schema.py +114 -0
  227. package/scripts/memory_context/__init__.py +610 -0
  228. package/scripts/memory_graph/__init__.py +393 -0
  229. package/scripts/project_resolver.py +172 -0
  230. package/scripts/prune_codex_plugin_cache.py +23 -0
  231. package/scripts/prune_plugin_cache.py +359 -0
  232. package/scripts/semantic_index/__init__.py +482 -0
  233. package/scripts/semantic_index/_bench_hybrid.py +175 -0
  234. package/scripts/semantic_index/backfill.py +263 -0
  235. package/scripts/semantic_index/hybrid.py +174 -0
  236. package/scripts/semantic_index/test_backfill.py +292 -0
  237. package/scripts/semantic_index/test_hybrid.py +541 -0
  238. package/scripts/sync_navgator_lessons.py +666 -0
  239. package/scripts/sync_plugin_cache.py +388 -0
  240. package/skills/agent-rally-point/SKILL.md +165 -0
  241. package/skills/agent-rally-watcher/SKILL.md +83 -0
  242. package/skills/api-registry-bridge/SKILL.md +59 -0
  243. package/skills/architecture/dead/SKILL.md +56 -0
  244. package/skills/architecture/impact/SKILL.md +88 -0
  245. package/skills/architecture/review/SKILL.md +227 -0
  246. package/skills/architecture/rules/SKILL.md +72 -0
  247. package/skills/architecture/scan/SKILL.md +79 -0
  248. package/skills/architecture/trace/SKILL.md +65 -0
  249. package/skills/attribution-standard/SKILL.md +122 -0
  250. package/skills/authentication/SKILL.md +81 -0
  251. package/skills/authentication/references/better-auth-magic-link.md +135 -0
  252. package/skills/authentication/references/better-auth-setup.md +224 -0
  253. package/skills/authentication/references/google-calendar-sync.md +278 -0
  254. package/skills/authentication/references/google-cloud-console.md +159 -0
  255. package/skills/authentication/references/google-geocoding-directions.md +126 -0
  256. package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
  257. package/skills/authentication/references/google-maps.md +162 -0
  258. package/skills/authentication/references/google-oauth-setup.md +343 -0
  259. package/skills/authentication/references/google-places.md +173 -0
  260. package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
  261. package/skills/authentication/references/resend-email.md +227 -0
  262. package/skills/authentication/references/resend-otp-magic-link.md +153 -0
  263. package/skills/authentication/references/supabase-auth.md +172 -0
  264. package/skills/auto-decision-capture/SKILL.md +419 -0
  265. package/skills/build-loop/SKILL.md +516 -0
  266. package/skills/build-loop/detect-plugins.mjs +82 -0
  267. package/skills/build-loop/eval-guide.md +65 -0
  268. package/skills/build-loop/fallbacks.md +549 -0
  269. package/skills/build-loop/phases/fact-check.md +42 -0
  270. package/skills/build-loop/phases/ui-validation.md +267 -0
  271. package/skills/build-loop/references/apple-native-planning.md +439 -0
  272. package/skills/build-loop/references/capability-routing.md +181 -0
  273. package/skills/build-loop/references/codex-subagents.md +98 -0
  274. package/skills/build-loop/references/coordination.md +161 -0
  275. package/skills/build-loop/references/correction-aware-capture.md +177 -0
  276. package/skills/build-loop/references/independent-auditor.md +72 -0
  277. package/skills/build-loop/references/intent-capability-pack.md +170 -0
  278. package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
  279. package/skills/build-loop/references/leadership.md +72 -0
  280. package/skills/build-loop/references/memory.md +284 -0
  281. package/skills/build-loop/references/modular-systems-pack.md +96 -0
  282. package/skills/build-loop/references/output-style.md +129 -0
  283. package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
  284. package/skills/build-loop/references/phase-1-assess.md +218 -0
  285. package/skills/build-loop/references/phase-2-plan.md +77 -0
  286. package/skills/build-loop/references/phase-3-execute.md +47 -0
  287. package/skills/build-loop/references/phase-4-review.md +310 -0
  288. package/skills/build-loop/references/phase-5-iterate.md +71 -0
  289. package/skills/build-loop/references/phase-6-learn.md +56 -0
  290. package/skills/build-loop/references/recent-design-structures.md +274 -0
  291. package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
  292. package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
  293. package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
  294. package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
  295. package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
  296. package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
  297. package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
  298. package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
  299. package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
  300. package/skills/build-loop/references/self-recursive-dev.md +77 -0
  301. package/skills/build-loop/references/self-review.md +232 -0
  302. package/skills/build-loop/references/ui-io-contract.md +116 -0
  303. package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
  304. package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
  305. package/skills/build-loop/templates/backlog-item.md +35 -0
  306. package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
  307. package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
  308. package/skills/build-loop/templates/ux-fix-plan.md +40 -0
  309. package/skills/building-with-deepagents/SKILL.md +195 -0
  310. package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
  311. package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
  312. package/skills/capabilities/SKILL.md +91 -0
  313. package/skills/debug-loop/SKILL.md +278 -0
  314. package/skills/debug-loop/references/convergence-rules.md +145 -0
  315. package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
  316. package/skills/debugging/assess/SKILL.md +111 -0
  317. package/skills/debugging/debug-loop/SKILL.md +231 -0
  318. package/skills/debugging/memory/SKILL.md +147 -0
  319. package/skills/debugging/store/SKILL.md +153 -0
  320. package/skills/debugging-memory/SKILL.md +440 -0
  321. package/skills/debugging-memory/examples/incident-example.json +67 -0
  322. package/skills/debugging-memory/references/incident-documentation.md +280 -0
  323. package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
  324. package/skills/debugging-memory/references/pattern-extraction.md +163 -0
  325. package/skills/debugging-memory/references/subagent-integration.md +312 -0
  326. package/skills/defenseclaw-bridge/SKILL.md +156 -0
  327. package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
  328. package/skills/defenseclaw-bridge/references/output-format.md +108 -0
  329. package/skills/handoff/SKILL.md +94 -0
  330. package/skills/ibr-bridge/SKILL.md +68 -0
  331. package/skills/knowledge/SKILL.md +127 -0
  332. package/skills/knowledge/references/recall-integration.md +83 -0
  333. package/skills/knowledge/templates/madr-minimal.md +56 -0
  334. package/skills/knowledge-review/SKILL.md +112 -0
  335. package/skills/logging-tracer/SKILL.md +202 -0
  336. package/skills/logging-tracer/references/log-analysis.md +128 -0
  337. package/skills/logging-tracer/references/stack-templates.md +274 -0
  338. package/skills/logging-tracer-bridge/SKILL.md +70 -0
  339. package/skills/mcp-builder/SKILL.md +278 -0
  340. package/skills/mcp-builder/references/mcp-security.md +106 -0
  341. package/skills/model-tiering/SKILL.md +216 -0
  342. package/skills/native-ax-driver/SKILL.md +220 -0
  343. package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
  344. package/skills/native-ax-driver/scripts/native_driver.py +495 -0
  345. package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
  346. package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
  347. package/skills/optimize/SKILL.md +202 -0
  348. package/skills/optimize/profiles.md +61 -0
  349. package/skills/plan-verify/SKILL.md +115 -0
  350. package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
  351. package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
  352. package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
  353. package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
  354. package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
  355. package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
  356. package/skills/plugin-builder/SKILL.md +329 -0
  357. package/skills/plugin-builder/references/authoritative-sources.md +104 -0
  358. package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
  359. package/skills/plugin-builder/references/components-guide.md +221 -0
  360. package/skills/plugin-builder/references/distribution.md +317 -0
  361. package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
  362. package/skills/plugin-builder/references/hooks-reference.md +210 -0
  363. package/skills/plugin-builder/references/manifest-schema.md +111 -0
  364. package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
  365. package/skills/plugin-tests/SKILL.md +94 -0
  366. package/skills/prd-bridge/SKILL.md +135 -0
  367. package/skills/research/SKILL.md +80 -0
  368. package/skills/security-methodology/SKILL.md +105 -0
  369. package/skills/security-methodology/references/cross-source-matrix.md +77 -0
  370. package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
  371. package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
  372. package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
  373. package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
  374. package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
  375. package/skills/self-improve/SKILL.md +298 -0
  376. package/skills/spec-writing/SKILL.md +548 -0
  377. package/skills/spec-writing/scripts/check_checklist.py +478 -0
  378. package/skills/sync-skills/SKILL.md +132 -0
  379. package/skills/telemetry/SKILL.md +66 -0
  380. package/skills/ui-design/SKILL.md +75 -0
  381. package/skills/ui-design/references/design-patterns-multi.md +165 -0
  382. package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
  383. package/skills/ui-design/references/evidence-capture-policy.md +55 -0
  384. package/skills/ui-design/references/ui-guidance-sources.md +330 -0
  385. package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
  386. package/skills/ui-design/references/universal-design-principles.md +419 -0
  387. package/templates/memory/MEMORY.md.template +55 -0
  388. package/templates/memory/constitution.md.template +64 -0
  389. package/templates/memory/manifest.json +47 -0
@@ -0,0 +1,135 @@
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
+ """CLI for the lessons_index package.
5
+
6
+ Usage:
7
+ python3 scripts/lessons_index ingest [--project X] [--db PATH]
8
+ python3 scripts/lessons_index query --goal "<text>" [--project X] [--limit N] [--json]
9
+ python3 scripts/lessons_index stats [--db PATH]
10
+
11
+ Default DB: $BUILD_LOOP_MEMORY_STORE_ROOT/indexes/lessons_index.db
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ HERE = Path(__file__).resolve().parent
21
+ _SCRIPTS_DIR = HERE.parent
22
+ for _p in (str(HERE), str(_SCRIPTS_DIR)):
23
+ if _p not in sys.path:
24
+ sys.path.insert(0, _p)
25
+
26
+ import lessons_index as li # noqa: E402 (intra-package via __init__)
27
+
28
+
29
+ def _cmd_ingest(args: argparse.Namespace) -> int:
30
+ project = args.project or None
31
+ db_path = args.db or None
32
+ result = li.ingest(project=project, db_path=db_path)
33
+ if args.json:
34
+ json.dump(result, sys.stdout, indent=2)
35
+ sys.stdout.write("\n")
36
+ else:
37
+ print(
38
+ f"ingest complete: upserted={result['upserted']} "
39
+ f"skipped={result['skipped']} errors={len(result['errors'])} "
40
+ f"scanned={result['total_scanned']}"
41
+ )
42
+ if result["errors"]:
43
+ for e in result["errors"]:
44
+ print(f" ERROR: {e['file']}: {e['error']}", file=sys.stderr)
45
+ return 1 if result["errors"] else 0
46
+
47
+
48
+ def _cmd_query(args: argparse.Namespace) -> int:
49
+ results = li.query(
50
+ args.goal,
51
+ project=args.project or None,
52
+ limit=args.limit,
53
+ db_path=args.db or None,
54
+ )
55
+ if args.json:
56
+ json.dump(results, sys.stdout, indent=2)
57
+ sys.stdout.write("\n")
58
+ else:
59
+ if not results:
60
+ print("No results.")
61
+ return 0
62
+ for i, r in enumerate(results, 1):
63
+ print(f"\n[{i}] {r['name']}")
64
+ if r["description"]:
65
+ print(f" {r['description']}")
66
+ if r["snippet"]:
67
+ print(f" {r['snippet']}")
68
+ print(f" lane={r['lane']} score={r['score']:.4f}")
69
+ print(f" {r['source_path']}")
70
+ return 0
71
+
72
+
73
+ def _cmd_stats(args: argparse.Namespace) -> int:
74
+ s = li.stats(db_path=args.db or None)
75
+ if args.json:
76
+ json.dump(s, sys.stdout, indent=2)
77
+ sys.stdout.write("\n")
78
+ else:
79
+ print(f"total_facts: {s['total_facts']}")
80
+ print(f"schema_version: {s['schema_version']}")
81
+ print(f"last_ingest_ts: {s['last_ingest_ts']}")
82
+ return 0
83
+
84
+
85
+ def _db_arg(parser: argparse.ArgumentParser) -> None:
86
+ """Add --db and --json to a subcommand parser."""
87
+ parser.add_argument(
88
+ "--db", default=None, metavar="PATH",
89
+ help="Override DB path (default: $MEMORY_STORE_ROOT/indexes/lessons_index.db)",
90
+ )
91
+ parser.add_argument("--json", action="store_true", help="Output JSON")
92
+
93
+
94
+ def _build_parser() -> argparse.ArgumentParser:
95
+ p = argparse.ArgumentParser(
96
+ prog="python3 -m lessons_index",
97
+ description="Portable SQLite/FTS5 lessons index for build-loop.",
98
+ )
99
+
100
+ sub = p.add_subparsers(dest="cmd", required=True)
101
+
102
+ # ingest
103
+ ing = sub.add_parser("ingest", help="Walk memory lanes and upsert into DB")
104
+ ing.add_argument("--project", default=None,
105
+ help="Project tag (default: top-level lanes)")
106
+ _db_arg(ing)
107
+
108
+ # query
109
+ qry = sub.add_parser("query", help="Query the index by goal text")
110
+ qry.add_argument("--goal", required=True, help="Free-text retrieval goal")
111
+ qry.add_argument("--project", default=None, help="Scope to project (+ _unscoped)")
112
+ qry.add_argument("--limit", type=int, default=5, help="Max results (default 5)")
113
+ _db_arg(qry)
114
+
115
+ # stats
116
+ sta = sub.add_parser("stats", help="Print index statistics")
117
+ _db_arg(sta)
118
+
119
+ return p
120
+
121
+
122
+ def main(argv=None) -> int:
123
+ parser = _build_parser()
124
+ args = parser.parse_args(argv)
125
+ # Propagate --db and --json to subcommand args (they're on the parent).
126
+ dispatch = {
127
+ "ingest": _cmd_ingest,
128
+ "query": _cmd_query,
129
+ "stats": _cmd_stats,
130
+ }
131
+ return dispatch[args.cmd](args)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,288 @@
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
+ """Ingest markdown memory files into the SQLite lessons index.
5
+
6
+ Walks the memory lanes for a project (or top-level) and upserts facts
7
+ into the SQLite DB, skipping files whose sha256 is unchanged (incremental).
8
+
9
+ Embeddings are computed lazily: if the embed backend is available AND the
10
+ fact's sha256 is new/changed, a vector is stored in `embeddings`. If the
11
+ backend is unavailable (MLX not installed, Ollama down, etc.) ingest
12
+ continues silently in FTS-only mode — graceful degradation is mandatory.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import re
20
+ import struct
21
+ import sys
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ HERE = Path(__file__).resolve().parent
27
+ _SCRIPTS_DIR = HERE.parent
28
+ # Insert scripts/ for _paths, embed_backend deps.
29
+ if str(_SCRIPTS_DIR) not in sys.path:
30
+ sys.path.insert(0, str(_SCRIPTS_DIR))
31
+
32
+ # Import open_db via importlib to avoid collision with write_decision/schema.py
33
+ # which is also named 'schema' and appears on sys.path in the full test suite.
34
+ import importlib.util as _iutil # noqa: E402
35
+
36
+ def _load_schema_module():
37
+ spec = _iutil.spec_from_file_location(
38
+ "lessons_index._schema", HERE / "schema.py"
39
+ )
40
+ mod = _iutil.module_from_spec(spec)
41
+ spec.loader.exec_module(mod)
42
+ return mod
43
+
44
+ _schema_mod = _load_schema_module()
45
+ open_db = _schema_mod.open_db
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Frontmatter parser — reuse the same tiny YAML subset as memory_writer.py.
50
+ # Imported from write_decision/frontmatter if available, otherwise inline.
51
+ # ---------------------------------------------------------------------------
52
+
53
+ def _split_frontmatter(text: str) -> tuple[dict, str]:
54
+ """Return (frontmatter_dict, body). Empty dict if no frontmatter.
55
+
56
+ Identical subset as memory_writer._split_frontmatter: handles key: value,
57
+ key: ["list"], booleans, null, int, float, quoted strings.
58
+ """
59
+ if not text.startswith("---\n"):
60
+ return {}, text
61
+ end = text.find("\n---\n", 4)
62
+ if end == -1:
63
+ return {}, text
64
+ block = text[4:end]
65
+ body = text[end + 5:]
66
+ fm: dict[str, Any] = {}
67
+ for line in block.splitlines():
68
+ line = line.rstrip()
69
+ if not line or line.lstrip().startswith("#"):
70
+ continue
71
+ m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line)
72
+ if not m:
73
+ continue
74
+ fm[m.group(1)] = _coerce(m.group(2).strip())
75
+ return fm, body
76
+
77
+
78
+ def _coerce(val: str) -> Any:
79
+ if val == "" or val == "~" or val.lower() == "null":
80
+ return None
81
+ if val.lower() == "true":
82
+ return True
83
+ if val.lower() == "false":
84
+ return False
85
+ if val.startswith("[") and val.endswith("]"):
86
+ try:
87
+ return json.loads(val)
88
+ except json.JSONDecodeError:
89
+ return val
90
+ if (val.startswith('"') and val.endswith('"')) or (
91
+ val.startswith("'") and val.endswith("'")
92
+ ):
93
+ return val[1:-1]
94
+ try:
95
+ return int(val)
96
+ except ValueError:
97
+ pass
98
+ try:
99
+ return float(val)
100
+ except ValueError:
101
+ pass
102
+ return val
103
+
104
+
105
+ def _sha256(text: str) -> str:
106
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
107
+
108
+
109
+ def _pack_floats(vec: list[float]) -> bytes:
110
+ """Pack a list of float32 into a BLOB."""
111
+ return struct.pack(f"{len(vec)}f", *vec)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Embed backend — lazy import, fail-safe
116
+ # ---------------------------------------------------------------------------
117
+
118
+ def _try_embed(text: str) -> tuple[list[float] | None, str | None]:
119
+ """Return (vector, model_name) or (None, None) if backend unavailable.
120
+
121
+ Checks EMBED_BACKEND_UNAVAILABLE env var first so tests can force the
122
+ no-embedding path without complex mocking.
123
+ """
124
+ if os.environ.get("EMBED_BACKEND_UNAVAILABLE"):
125
+ return None, None
126
+ try:
127
+ import embed_backend as _eb # type: ignore # noqa: PLC0415
128
+ vec = _eb.embed(text)
129
+ model = _eb.active_model()
130
+ return vec, model
131
+ except Exception: # noqa: BLE001
132
+ return None, None
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Lane discovery
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def _lane_dirs(project: str | None) -> list[tuple[str, Path]]:
140
+ """Return [(lane_name, directory)] pairs to walk for the given project.
141
+
142
+ Lane names correspond to the memory lane taxonomy:
143
+ lessons, decisions, product, architecture (per project or top-level).
144
+ """
145
+ from _paths import ( # type: ignore # noqa: PLC0415
146
+ project_decisions_dir,
147
+ project_lessons_dir,
148
+ project_product_dir,
149
+ top_level_lessons_dir,
150
+ )
151
+ if project:
152
+ return [
153
+ ("lessons", project_lessons_dir(project)),
154
+ ("decisions", project_decisions_dir(project)),
155
+ ("product", project_product_dir(project)),
156
+ ]
157
+ else:
158
+ return [
159
+ ("lessons", top_level_lessons_dir()),
160
+ ]
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Core ingest
165
+ # ---------------------------------------------------------------------------
166
+
167
+ def ingest(
168
+ project: str | None = None,
169
+ db_path: str | Path | None = None,
170
+ ) -> dict:
171
+ """Walk memory lanes and upsert markdown → SQLite.
172
+
173
+ Returns a summary dict: {upserted, skipped, errors, total_scanned}.
174
+
175
+ Rules:
176
+ - Skip files whose sha256 is unchanged (incremental).
177
+ - Skip non-.md files and INDEX.md / MEMORY.md / TELEMETRY.jsonl.
178
+ - Embeddings: computed lazily; silently skipped if backend unavailable.
179
+ - Idempotent: safe to call multiple times.
180
+ """
181
+ from _paths import memory_store_root # type: ignore # noqa: PLC0415
182
+
183
+ if db_path is None:
184
+ db_path = memory_store_root() / "indexes" / "lessons_index.db"
185
+ db_path = Path(db_path)
186
+ db_path.parent.mkdir(parents=True, exist_ok=True)
187
+
188
+ conn = open_db(db_path)
189
+ summary = {"upserted": 0, "skipped": 0, "errors": [], "total_scanned": 0}
190
+
191
+ try:
192
+ lanes = _lane_dirs(project)
193
+ for lane_name, lane_dir in lanes:
194
+ if not lane_dir.exists():
195
+ continue
196
+ for md_path in sorted(lane_dir.glob("*.md")):
197
+ if md_path.name in {"INDEX.md", "MEMORY.md", "README.md"}:
198
+ continue
199
+ summary["total_scanned"] += 1
200
+ try:
201
+ _ingest_file(conn, md_path, lane_name,
202
+ project or "_unscoped", summary)
203
+ except Exception as exc: # noqa: BLE001
204
+ summary["errors"].append(
205
+ {"file": str(md_path), "error": str(exc)}
206
+ )
207
+ finally:
208
+ # Update last_ingest_ts.
209
+ now_ts = datetime.now(timezone.utc).isoformat()
210
+ conn.execute(
211
+ "INSERT OR REPLACE INTO meta(key, value) VALUES ('last_ingest_ts', ?)",
212
+ (now_ts,),
213
+ )
214
+ conn.commit()
215
+ conn.close()
216
+
217
+ return summary
218
+
219
+
220
+ def _ingest_file(
221
+ conn,
222
+ path: Path,
223
+ lane: str,
224
+ project: str,
225
+ summary: dict,
226
+ ) -> None:
227
+ """Upsert one markdown file into the DB."""
228
+ text = path.read_text(encoding="utf-8")
229
+ sha = _sha256(text)
230
+ source_path = str(path)
231
+ mtime = path.stat().st_mtime
232
+
233
+ # Check if sha256 is unchanged — skip if so.
234
+ row = conn.execute(
235
+ "SELECT id, sha256 FROM facts WHERE source_path = ?", (source_path,)
236
+ ).fetchone()
237
+
238
+ if row is not None and row["sha256"] == sha:
239
+ summary["skipped"] += 1
240
+ return
241
+
242
+ # Parse frontmatter + body.
243
+ fm, body = _split_frontmatter(text)
244
+ name = str(fm.get("name") or path.stem)
245
+ description = str(fm.get("description") or "")
246
+ fm_json = json.dumps(fm, ensure_ascii=False, default=str)
247
+
248
+ # Upsert into facts.
249
+ if row is None:
250
+ # New row.
251
+ cur = conn.execute(
252
+ """INSERT INTO facts
253
+ (lane, project, name, description, body, frontmatter_json,
254
+ source_path, mtime, sha256)
255
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
256
+ (lane, project, name, description, body.strip(),
257
+ fm_json, source_path, mtime, sha),
258
+ )
259
+ fact_id = cur.lastrowid
260
+ # FTS triggers handle INSERT automatically.
261
+ else:
262
+ fact_id = row["id"]
263
+ conn.execute(
264
+ """UPDATE facts SET
265
+ lane=?, project=?, name=?, description=?, body=?,
266
+ frontmatter_json=?, mtime=?, sha256=?
267
+ WHERE id=?""",
268
+ (lane, project, name, description, body.strip(),
269
+ fm_json, mtime, sha, fact_id),
270
+ )
271
+ # FTS triggers handle UPDATE automatically.
272
+
273
+ conn.commit()
274
+ summary["upserted"] += 1
275
+
276
+ # Embeddings: lazy, skip on any failure.
277
+ vec, model = _try_embed(f"{name}\n{description}\n{body}")
278
+ if vec is not None and model is not None:
279
+ packed = _pack_floats(vec)
280
+ dim = len(vec)
281
+ conn.execute(
282
+ """INSERT INTO embeddings(fact_id, model, dim, vec)
283
+ VALUES (?, ?, ?, ?)
284
+ ON CONFLICT(fact_id) DO UPDATE SET
285
+ model=excluded.model, dim=excluded.dim, vec=excluded.vec""",
286
+ (fact_id, model, dim, packed),
287
+ )
288
+ conn.commit()
@@ -0,0 +1,285 @@
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
+ """Query the SQLite lessons index with progressive disclosure.
5
+
6
+ query(goal_text, project, limit) flow:
7
+ 1. FTS5 BM25 match on goal_text, scoped to project + _unscoped.
8
+ 2. If embeddings table is populated AND embed backend is available,
9
+ compute goal embedding and cosine-rerank the BM25 candidates (hybrid).
10
+ 3. Return top-N results as list of dicts.
11
+
12
+ Graceful degradation: if the embed backend is unavailable or the embeddings
13
+ table is empty, returns pure BM25 results with no error.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ import os
19
+ import struct
20
+ import sys
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ HERE = Path(__file__).resolve().parent
25
+ _SCRIPTS_DIR = HERE.parent
26
+ if str(_SCRIPTS_DIR) not in sys.path:
27
+ sys.path.insert(0, str(_SCRIPTS_DIR))
28
+
29
+ import importlib.util as _iutil # noqa: E402
30
+
31
+ def _load_schema_module():
32
+ spec = _iutil.spec_from_file_location(
33
+ "lessons_index._schema", HERE / "schema.py"
34
+ )
35
+ mod = _iutil.module_from_spec(spec)
36
+ spec.loader.exec_module(mod)
37
+ return mod
38
+
39
+ _schema_mod = _load_schema_module()
40
+ open_db = _schema_mod.open_db
41
+
42
+ _SNIPPET_TOKENS = 64 # chars for snippet extraction
43
+
44
+
45
+ def _snippet(text: str, query_words: list[str], max_chars: int = _SNIPPET_TOKENS * 4) -> str:
46
+ """Return a short excerpt near the first query word hit."""
47
+ text_lower = text.lower()
48
+ best_pos = len(text)
49
+ for word in query_words:
50
+ pos = text_lower.find(word.lower())
51
+ if 0 <= pos < best_pos:
52
+ best_pos = pos
53
+ start = max(0, best_pos - 80)
54
+ end = min(len(text), start + max_chars)
55
+ excerpt = text[start:end].strip()
56
+ if start > 0:
57
+ excerpt = "…" + excerpt
58
+ if end < len(text):
59
+ excerpt = excerpt + "…"
60
+ return excerpt
61
+
62
+
63
+ def _cosine(a: list[float], b: list[float]) -> float:
64
+ dot = sum(x * y for x, y in zip(a, b))
65
+ mag_a = math.sqrt(sum(x * x for x in a))
66
+ mag_b = math.sqrt(sum(x * x for x in b))
67
+ if mag_a == 0 or mag_b == 0:
68
+ return 0.0
69
+ return dot / (mag_a * mag_b)
70
+
71
+
72
+ def _unpack_floats(blob: bytes) -> list[float]:
73
+ n = len(blob) // 4
74
+ return list(struct.unpack(f"{n}f", blob))
75
+
76
+
77
+ def _try_embed(text: str) -> list[float] | None:
78
+ """Return embedding vector or None if backend unavailable."""
79
+ if os.environ.get("EMBED_BACKEND_UNAVAILABLE"):
80
+ return None
81
+ try:
82
+ import embed_backend as _eb # type: ignore # noqa: PLC0415
83
+ vec = _eb.embed(text)
84
+ return list(vec)
85
+ except Exception: # noqa: BLE001
86
+ return None
87
+
88
+
89
+ def query(
90
+ goal_text: str,
91
+ project: str | None = None,
92
+ limit: int = 5,
93
+ db_path: str | Path | None = None,
94
+ ) -> list[dict[str, Any]]:
95
+ """Return top-N lessons matching goal_text.
96
+
97
+ Scoping: includes rows where project matches AND rows where
98
+ project='_unscoped' (top-level lanes). When project is None, returns
99
+ results from all projects.
100
+
101
+ Each result dict: {name, description, snippet, score, source_path, lane}.
102
+ """
103
+ from _paths import memory_store_root # type: ignore # noqa: PLC0415
104
+
105
+ if db_path is None:
106
+ db_path = memory_store_root() / "indexes" / "lessons_index.db"
107
+ db_path = Path(db_path)
108
+
109
+ if not db_path.exists():
110
+ return []
111
+
112
+ conn = open_db(db_path)
113
+ try:
114
+ return _run_query(conn, goal_text, project, limit)
115
+ finally:
116
+ conn.close()
117
+
118
+
119
+ def _run_query(
120
+ conn,
121
+ goal_text: str,
122
+ project: str | None,
123
+ limit: int,
124
+ ) -> list[dict[str, Any]]:
125
+ # Check if FTS table has any content.
126
+ fts_count = conn.execute("SELECT COUNT(*) FROM facts_fts").fetchone()[0]
127
+ if fts_count == 0:
128
+ # FTS table may be behind facts (e.g. if triggers weren't active).
129
+ # Rebuild from facts.
130
+ _rebuild_fts(conn)
131
+ fts_count = conn.execute("SELECT COUNT(*) FROM facts_fts").fetchone()[0]
132
+ if fts_count == 0:
133
+ return []
134
+
135
+ # Sanitize goal_text for FTS5: escape special chars, wrap in quotes for
136
+ # phrase matching fallback if the raw query fails.
137
+ fts_query = _sanitize_fts_query(goal_text)
138
+
139
+ # BM25 candidates: fetch up to limit*4 for reranking headroom.
140
+ candidates = _fts_candidates(conn, fts_query, project, limit * 4)
141
+ if not candidates:
142
+ # FTS found nothing — last resort LIKE on name + description.
143
+ candidates = _like_fallback(conn, goal_text, project, limit)
144
+
145
+ if not candidates:
146
+ return []
147
+
148
+ # Hybrid rerank if embeddings are available.
149
+ goal_vec = _try_embed(goal_text)
150
+ if goal_vec is not None and _embeddings_populated(conn):
151
+ candidates = _cosine_rerank(conn, candidates, goal_vec)
152
+
153
+ # Trim to limit.
154
+ results = candidates[:limit]
155
+
156
+ # Build output dicts.
157
+ query_words = [w for w in goal_text.lower().split() if len(w) > 2]
158
+ out = []
159
+ for row in results:
160
+ out.append({
161
+ "name": row["name"],
162
+ "description": row["description"],
163
+ "snippet": _snippet(row["body"], query_words),
164
+ "score": float(row.get("score", 0.0)),
165
+ "source_path": row["source_path"],
166
+ "lane": row["lane"],
167
+ })
168
+ return out
169
+
170
+
171
+ def _sanitize_fts_query(text: str) -> str:
172
+ """Strip FTS5 syntax chars and return a safe query string."""
173
+ # Remove FTS5 special operators to avoid syntax errors on raw user input.
174
+ sanitized = text.replace('"', " ").replace("'", " ")
175
+ sanitized = sanitized.replace("^", " ").replace("*", " ")
176
+ # Collapse whitespace, take individual tokens.
177
+ tokens = [t for t in sanitized.split() if t]
178
+ if not tokens:
179
+ return '""'
180
+ # Simple OR: each token must match somewhere.
181
+ return " OR ".join(tokens)
182
+
183
+
184
+ def _fts_candidates(conn, fts_query: str, project: str | None, n: int) -> list:
185
+ """Run BM25 FTS5 query, return rows ordered by relevance."""
186
+ try:
187
+ if project is not None:
188
+ sql = """
189
+ SELECT f.id, f.lane, f.project, f.name, f.description,
190
+ f.body, f.source_path,
191
+ bm25(facts_fts) AS score
192
+ FROM facts_fts
193
+ JOIN facts f ON facts_fts.rowid = f.id
194
+ WHERE facts_fts MATCH ?
195
+ AND (f.project = ? OR f.project = '_unscoped')
196
+ ORDER BY score
197
+ LIMIT ?
198
+ """
199
+ rows = conn.execute(sql, (fts_query, project, n)).fetchall()
200
+ else:
201
+ sql = """
202
+ SELECT f.id, f.lane, f.project, f.name, f.description,
203
+ f.body, f.source_path,
204
+ bm25(facts_fts) AS score
205
+ FROM facts_fts
206
+ JOIN facts f ON facts_fts.rowid = f.id
207
+ WHERE facts_fts MATCH ?
208
+ ORDER BY score
209
+ LIMIT ?
210
+ """
211
+ rows = conn.execute(sql, (fts_query, n)).fetchall()
212
+ return [dict(r) for r in rows]
213
+ except Exception: # noqa: BLE001
214
+ return []
215
+
216
+
217
+ def _like_fallback(conn, text: str, project: str | None, n: int) -> list:
218
+ """Simple LIKE fallback when FTS returns nothing."""
219
+ pattern = f"%{text[:50]}%"
220
+ try:
221
+ if project is not None:
222
+ sql = """
223
+ SELECT id, lane, project, name, description, body, source_path,
224
+ 0.0 AS score
225
+ FROM facts
226
+ WHERE (name LIKE ? OR description LIKE ? OR body LIKE ?)
227
+ AND (project = ? OR project = '_unscoped')
228
+ LIMIT ?
229
+ """
230
+ rows = conn.execute(sql, (pattern, pattern, pattern, project, n)).fetchall()
231
+ else:
232
+ sql = """
233
+ SELECT id, lane, project, name, description, body, source_path,
234
+ 0.0 AS score
235
+ FROM facts
236
+ WHERE name LIKE ? OR description LIKE ? OR body LIKE ?
237
+ LIMIT ?
238
+ """
239
+ rows = conn.execute(sql, (pattern, pattern, pattern, n)).fetchall()
240
+ return [dict(r) for r in rows]
241
+ except Exception: # noqa: BLE001
242
+ return []
243
+
244
+
245
+ def _embeddings_populated(conn) -> bool:
246
+ """True if there are any rows in the embeddings table."""
247
+ count = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
248
+ return count > 0
249
+
250
+
251
+ def _cosine_rerank(conn, candidates: list[dict], goal_vec: list[float]) -> list[dict]:
252
+ """Rerank BM25 candidates by cosine similarity of stored embeddings.
253
+
254
+ Returns candidates sorted by cosine similarity (descending). Candidates
255
+ without an embedding retain their BM25 rank at the end.
256
+ """
257
+ ids = [r["id"] for r in candidates]
258
+ placeholders = ",".join("?" * len(ids))
259
+ emb_rows = conn.execute(
260
+ f"SELECT fact_id, vec FROM embeddings WHERE fact_id IN ({placeholders})",
261
+ ids,
262
+ ).fetchall()
263
+ emb_map = {row["fact_id"]: _unpack_floats(row["vec"]) for row in emb_rows}
264
+
265
+ scored = []
266
+ unscored = []
267
+ for r in candidates:
268
+ if r["id"] in emb_map:
269
+ cos = _cosine(goal_vec, emb_map[r["id"]])
270
+ scored.append({**r, "score": cos})
271
+ else:
272
+ unscored.append(r)
273
+
274
+ scored.sort(key=lambda x: x["score"], reverse=True)
275
+ return scored + unscored
276
+
277
+
278
+ def _rebuild_fts(conn) -> None:
279
+ """Populate FTS from the facts table (recovery path)."""
280
+ conn.execute("DELETE FROM facts_fts")
281
+ conn.execute(
282
+ "INSERT INTO facts_fts(rowid, name, description, body) "
283
+ "SELECT id, name, description, body FROM facts"
284
+ )
285
+ conn.commit()