@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,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())