attocode-code-intel 0.1.0__py3-none-any.whl

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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,761 @@
1
+ """Graph query DSL parser and executor for dependency graph queries.
2
+
3
+ Supports a simple Cypher-inspired syntax for traversing the import graph::
4
+
5
+ MATCH "src/api/app.py" -[IMPORTS*1..3]-> target RETURN target
6
+ MATCH file <-[IMPORTED_BY]- caller WHERE caller.language = "python" RETURN caller
7
+ MATCH a -[IMPORTS]-> b -[IMPORTS]-> c RETURN a, c
8
+
9
+ Architecture:
10
+ - ``GraphQueryParser.parse(query) -> GraphQueryAST`` — regex tokenizer + recursive descent
11
+ - ``GraphQueryExecutor.execute(ast, dep_graph, files) -> list[dict]`` — BFS with depth constraints
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import fnmatch
17
+ import re
18
+ from collections import deque
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # AST dataclasses
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @dataclass(slots=True)
28
+ class NodePattern:
29
+ """A node in the MATCH pattern — either a concrete file or a variable."""
30
+
31
+ variable: str # Variable name (e.g. "target", "a") or empty for anonymous
32
+ file_pattern: str | None = None # Quoted literal path or glob (None = any file)
33
+
34
+
35
+ @dataclass(slots=True)
36
+ class EdgePattern:
37
+ """A directed edge between two node patterns."""
38
+
39
+ source: int # Index into GraphQueryAST.nodes
40
+ target: int # Index into GraphQueryAST.nodes
41
+ edge_type: str # "IMPORTS" or "IMPORTED_BY"
42
+ min_depth: int = 1 # Minimum hops
43
+ max_depth: int = 1 # Maximum hops
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class WhereClause:
48
+ """A single WHERE filter condition: variable.field op value."""
49
+
50
+ variable: str
51
+ field_name: str # "language", "line_count", "importance", "path", "fan_in", "fan_out"
52
+ operator: str # "=", "!=", ">", "<", ">=", "<=", "LIKE"
53
+ value: str # String value (numeric comparisons will be cast)
54
+
55
+
56
+ @dataclass(slots=True)
57
+ class ReturnSpec:
58
+ """A variable (and optional field) to return."""
59
+
60
+ variable: str
61
+ field_name: str | None = None # If None, return the file path
62
+
63
+
64
+ @dataclass(slots=True)
65
+ class GraphQueryAST:
66
+ """Parsed representation of a graph DSL query."""
67
+
68
+ nodes: list[NodePattern] = field(default_factory=list)
69
+ edges: list[EdgePattern] = field(default_factory=list)
70
+ where_clauses: list[WhereClause] = field(default_factory=list)
71
+ return_specs: list[ReturnSpec] = field(default_factory=list)
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Tokenizer
76
+ # ---------------------------------------------------------------------------
77
+
78
+ # Token types
79
+ _TOKEN_PATTERNS = [
80
+ ("KEYWORD", r"\b(?:MATCH|WHERE|RETURN|AND|OR|LIKE|COUNT)\b"),
81
+ ("EDGE_RIGHT", r"-\["), # -[
82
+ ("EDGE_RIGHT_CLOSE", r"\]->"), # ]->
83
+ ("EDGE_LEFT", r"<-\["), # <-[
84
+ ("EDGE_LEFT_CLOSE", r"\]-"), # ]-
85
+ ("RANGE", r"\*(\d+)\.\.(\d+)"), # *1..3
86
+ ("DEPTH_SINGLE", r"\*(\d+)"), # *2 (single depth = min..max same)
87
+ ("OP", r"[!><]=?|="), # =, !=, >, <, >=, <=
88
+ ("DOT", r"\."),
89
+ ("COMMA", r","),
90
+ ("QUOTED", r'"([^"]*)"'), # "quoted string"
91
+ ("QUOTED_SINGLE", r"'([^']*)'"), # 'quoted string'
92
+ ("IDENT", r"[A-Za-z_][A-Za-z0-9_]*"),
93
+ ("NUMBER", r"\d+(?:\.\d+)?"),
94
+ ("WS", r"\s+"),
95
+ ]
96
+
97
+ _TOKEN_RE = re.compile("|".join(f"(?P<{name}>{pat})" for name, pat in _TOKEN_PATTERNS))
98
+
99
+
100
+ @dataclass(slots=True)
101
+ class Token:
102
+ """A single token from the query string."""
103
+
104
+ kind: str
105
+ value: str
106
+ pos: int # Character position in original query
107
+
108
+
109
+ def _tokenize(query: str) -> list[Token]:
110
+ """Split a query string into tokens."""
111
+ tokens: list[Token] = []
112
+ for m in _TOKEN_RE.finditer(query):
113
+ kind = m.lastgroup
114
+ if kind == "WS":
115
+ continue
116
+ value = m.group()
117
+ tokens.append(Token(kind=kind, value=value, pos=m.start())) # type: ignore[arg-type]
118
+ return tokens
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Parser
123
+ # ---------------------------------------------------------------------------
124
+
125
+ class GraphQueryParser:
126
+ """Parse a graph DSL query string into a ``GraphQueryAST``."""
127
+
128
+ def parse(self, query: str) -> GraphQueryAST:
129
+ """Parse *query* and return the AST.
130
+
131
+ Raises ``ValueError`` on syntax errors with a descriptive message.
132
+ """
133
+ tokens = _tokenize(query.strip())
134
+ if not tokens:
135
+ raise ValueError("Empty query")
136
+
137
+ self._tokens = tokens
138
+ self._pos = 0
139
+ self._ast = GraphQueryAST()
140
+
141
+ self._parse_match()
142
+ self._parse_optional_where()
143
+ self._parse_return()
144
+
145
+ if self._pos < len(self._tokens):
146
+ tok = self._tokens[self._pos]
147
+ raise ValueError(
148
+ f"Unexpected token '{tok.value}' at position {tok.pos} "
149
+ f"(expected end of query)"
150
+ )
151
+
152
+ return self._ast
153
+
154
+ # -- Helpers --
155
+
156
+ def _peek(self) -> Token | None:
157
+ if self._pos < len(self._tokens):
158
+ return self._tokens[self._pos]
159
+ return None
160
+
161
+ def _advance(self) -> Token:
162
+ if self._pos >= len(self._tokens):
163
+ raise ValueError("Unexpected end of query")
164
+ tok = self._tokens[self._pos]
165
+ self._pos += 1
166
+ return tok
167
+
168
+ def _expect(self, kind: str, value: str | None = None) -> Token:
169
+ tok = self._advance()
170
+ if tok.kind != kind or (value is not None and tok.value.upper() != value.upper()):
171
+ expected = f"'{value}'" if value else kind
172
+ raise ValueError(
173
+ f"Expected {expected} at position {tok.pos}, got '{tok.value}'"
174
+ )
175
+ return tok
176
+
177
+ def _expect_keyword(self, keyword: str) -> Token:
178
+ return self._expect("KEYWORD", keyword)
179
+
180
+ # -- MATCH clause --
181
+
182
+ def _parse_match(self) -> None:
183
+ self._expect_keyword("MATCH")
184
+ # Parse first node
185
+ self._parse_node()
186
+
187
+ # Parse alternating edge-node pairs
188
+ while self._pos < len(self._tokens):
189
+ tok = self._peek()
190
+ if tok is None:
191
+ break
192
+ # Check for edge start: -[ or <-[
193
+ if tok.kind in ("EDGE_RIGHT", "EDGE_LEFT"):
194
+ src_idx = len(self._ast.nodes) - 1
195
+ edge = self._parse_edge()
196
+ self._parse_node()
197
+ tgt_idx = len(self._ast.nodes) - 1
198
+ edge.source = src_idx
199
+ edge.target = tgt_idx
200
+ self._ast.edges.append(edge)
201
+ else:
202
+ break
203
+
204
+ def _parse_node(self) -> None:
205
+ """Parse a node pattern: variable, "quoted path", or both."""
206
+ tok = self._peek()
207
+ if tok is None:
208
+ raise ValueError("Expected node pattern, got end of query")
209
+
210
+ variable = ""
211
+ file_pattern: str | None = None
212
+
213
+ if tok.kind in ("QUOTED", "QUOTED_SINGLE"):
214
+ self._advance()
215
+ # Extract the inner string (without quotes)
216
+ file_pattern = self._extract_quoted(tok)
217
+ elif tok.kind == "IDENT" and tok.value.upper() not in (
218
+ "MATCH", "WHERE", "RETURN", "AND", "OR", "LIKE", "COUNT",
219
+ ):
220
+ self._advance()
221
+ variable = tok.value
222
+ else:
223
+ raise ValueError(
224
+ f"Expected variable name or quoted file path at position {tok.pos}, "
225
+ f"got '{tok.value}'"
226
+ )
227
+
228
+ self._ast.nodes.append(NodePattern(variable=variable, file_pattern=file_pattern))
229
+
230
+ def _parse_edge(self) -> EdgePattern:
231
+ """Parse an edge pattern: -[TYPE*min..max]-> or <-[TYPE*min..max]-"""
232
+ tok = self._advance()
233
+ is_left = tok.kind == "EDGE_LEFT" # <-[...]- means reverse direction
234
+
235
+ # Parse edge type
236
+ edge_type = "IMPORTS"
237
+ min_depth = 1
238
+ max_depth = 1
239
+
240
+ # Read the edge contents (type and optional depth)
241
+ tok = self._peek()
242
+ if tok is not None and tok.kind == "IDENT":
243
+ self._advance()
244
+ edge_type = tok.value.upper()
245
+ if edge_type not in ("IMPORTS", "IMPORTED_BY"):
246
+ raise ValueError(
247
+ f"Unknown edge type '{edge_type}' at position {tok.pos}. "
248
+ f"Must be IMPORTS or IMPORTED_BY"
249
+ )
250
+
251
+ # Check for depth range: *min..max or *depth
252
+ tok = self._peek()
253
+ if tok is not None and tok.kind == "RANGE":
254
+ self._advance()
255
+ m = re.match(r"\*(\d+)\.\.(\d+)", tok.value)
256
+ if m:
257
+ min_depth = int(m.group(1))
258
+ max_depth = int(m.group(2))
259
+ if min_depth < 0 or max_depth < min_depth:
260
+ raise ValueError(
261
+ f"Invalid depth range *{min_depth}..{max_depth} "
262
+ f"at position {tok.pos}"
263
+ )
264
+ max_depth = min(max_depth, 10) # Safety cap
265
+ elif tok is not None and tok.kind == "DEPTH_SINGLE":
266
+ self._advance()
267
+ m = re.match(r"\*(\d+)", tok.value)
268
+ if m:
269
+ d = int(m.group(1))
270
+ min_depth = d
271
+ max_depth = d
272
+ max_depth = min(max_depth, 10)
273
+
274
+ # Close the edge
275
+ if is_left:
276
+ self._expect("EDGE_LEFT_CLOSE") # ]-
277
+ else:
278
+ self._expect("EDGE_RIGHT_CLOSE") # ]->
279
+
280
+ # For left-pointing edges (<-[TYPE]-), swap the semantics:
281
+ # <-[IMPORTS]- means "is imported by" = follow reverse graph
282
+ # <-[IMPORTED_BY]- means "imports" = follow forward graph
283
+ if is_left:
284
+ if edge_type == "IMPORTS":
285
+ edge_type = "IMPORTED_BY"
286
+ elif edge_type == "IMPORTED_BY":
287
+ edge_type = "IMPORTS"
288
+
289
+ return EdgePattern(
290
+ source=-1, # Will be set by caller
291
+ target=-1,
292
+ edge_type=edge_type,
293
+ min_depth=min_depth,
294
+ max_depth=max_depth,
295
+ )
296
+
297
+ # -- WHERE clause --
298
+
299
+ def _parse_optional_where(self) -> None:
300
+ tok = self._peek()
301
+ if tok is None or tok.kind != "KEYWORD" or tok.value.upper() != "WHERE":
302
+ return
303
+ self._advance() # consume WHERE
304
+
305
+ self._parse_where_condition()
306
+ while self._pos < len(self._tokens):
307
+ tok = self._peek()
308
+ if tok is not None and tok.kind == "KEYWORD" and tok.value.upper() == "AND":
309
+ self._advance()
310
+ self._parse_where_condition()
311
+ else:
312
+ break
313
+
314
+ def _parse_where_condition(self) -> None:
315
+ """Parse: variable.field op value"""
316
+ var_tok = self._advance()
317
+ if var_tok.kind != "IDENT":
318
+ raise ValueError(
319
+ f"Expected variable name in WHERE clause at position {var_tok.pos}, "
320
+ f"got '{var_tok.value}'"
321
+ )
322
+ variable = var_tok.value
323
+
324
+ self._expect("DOT")
325
+
326
+ field_tok = self._advance()
327
+ if field_tok.kind != "IDENT":
328
+ raise ValueError(
329
+ f"Expected field name after '.' at position {field_tok.pos}, "
330
+ f"got '{field_tok.value}'"
331
+ )
332
+ field_name = field_tok.value
333
+
334
+ # Operator: =, !=, >, <, >=, <=, or LIKE
335
+ op_tok = self._peek()
336
+ if op_tok is not None and op_tok.kind == "KEYWORD" and op_tok.value.upper() == "LIKE":
337
+ self._advance()
338
+ operator = "LIKE"
339
+ elif op_tok is not None and op_tok.kind == "OP":
340
+ self._advance()
341
+ operator = op_tok.value
342
+ else:
343
+ raise ValueError(
344
+ f"Expected comparison operator at position "
345
+ f"{op_tok.pos if op_tok else 'end'}, "
346
+ f"got '{op_tok.value if op_tok else 'end of query'}'"
347
+ )
348
+
349
+ # Value: quoted string or number or identifier
350
+ val_tok = self._advance()
351
+ if val_tok.kind in ("QUOTED", "QUOTED_SINGLE"):
352
+ value = self._extract_quoted(val_tok)
353
+ elif val_tok.kind == "NUMBER":
354
+ value = val_tok.value
355
+ elif val_tok.kind == "IDENT":
356
+ value = val_tok.value
357
+ else:
358
+ raise ValueError(
359
+ f"Expected value in WHERE clause at position {val_tok.pos}, "
360
+ f"got '{val_tok.value}'"
361
+ )
362
+
363
+ valid_fields = {
364
+ "language", "line_count", "importance", "path",
365
+ "fan_in", "fan_out", "is_test", "is_config",
366
+ }
367
+ if field_name not in valid_fields:
368
+ raise ValueError(
369
+ f"Unknown field '{field_name}' in WHERE clause. "
370
+ f"Valid fields: {', '.join(sorted(valid_fields))}"
371
+ )
372
+
373
+ self._ast.where_clauses.append(WhereClause(
374
+ variable=variable,
375
+ field_name=field_name,
376
+ operator=operator,
377
+ value=value,
378
+ ))
379
+
380
+ # -- RETURN clause --
381
+
382
+ def _parse_return(self) -> None:
383
+ self._expect_keyword("RETURN")
384
+
385
+ self._parse_return_item()
386
+ while self._pos < len(self._tokens):
387
+ tok = self._peek()
388
+ if tok is not None and tok.kind == "COMMA":
389
+ self._advance()
390
+ self._parse_return_item()
391
+ else:
392
+ break
393
+
394
+ def _parse_return_item(self) -> None:
395
+ """Parse: variable or variable.field or COUNT(variable)"""
396
+ tok = self._peek()
397
+
398
+ # COUNT(variable)
399
+ if tok is not None and tok.kind == "KEYWORD" and tok.value.upper() == "COUNT":
400
+ self._advance()
401
+ # We treat COUNT as a special return — just return "count" field
402
+ self._ast.return_specs.append(ReturnSpec(variable="*", field_name="count"))
403
+ return
404
+
405
+ var_tok = self._advance()
406
+ if var_tok.kind != "IDENT":
407
+ raise ValueError(
408
+ f"Expected variable name in RETURN clause at position {var_tok.pos}, "
409
+ f"got '{var_tok.value}'"
410
+ )
411
+
412
+ field_name: str | None = None
413
+ dot_tok = self._peek()
414
+ if dot_tok is not None and dot_tok.kind == "DOT":
415
+ self._advance()
416
+ field_tok = self._advance()
417
+ if field_tok.kind != "IDENT":
418
+ raise ValueError(
419
+ f"Expected field name after '.' at position {field_tok.pos}, "
420
+ f"got '{field_tok.value}'"
421
+ )
422
+ field_name = field_tok.value
423
+
424
+ self._ast.return_specs.append(ReturnSpec(
425
+ variable=var_tok.value,
426
+ field_name=field_name,
427
+ ))
428
+
429
+ # -- Utilities --
430
+
431
+ @staticmethod
432
+ def _extract_quoted(tok: Token) -> str:
433
+ """Extract the inner value from a QUOTED or QUOTED_SINGLE token."""
434
+ v = tok.value
435
+ if v.startswith('"') and v.endswith('"'):
436
+ return v[1:-1]
437
+ if v.startswith("'") and v.endswith("'"):
438
+ return v[1:-1]
439
+ return v
440
+
441
+
442
+ # ---------------------------------------------------------------------------
443
+ # Executor
444
+ # ---------------------------------------------------------------------------
445
+
446
+ class GraphQueryExecutor:
447
+ """Execute a parsed ``GraphQueryAST`` against a dependency graph.
448
+
449
+ Uses the same ``DependencyGraph`` (forward/reverse dicts) and
450
+ ``FileInfo`` list that the existing graph_query tool uses.
451
+ """
452
+
453
+ MAX_RESULTS = 100
454
+
455
+ def execute(
456
+ self,
457
+ ast: GraphQueryAST,
458
+ dep_graph: Any, # DependencyGraph with forward/reverse dicts
459
+ files: list[Any], # list[FileInfo]
460
+ ) -> list[dict[str, Any]]:
461
+ """Execute the query and return a list of result dicts.
462
+
463
+ Each result dict maps variable names to file paths (or field values
464
+ if a specific field is requested in RETURN).
465
+ """
466
+ # Build file metadata lookup
467
+ file_meta: dict[str, dict[str, Any]] = {}
468
+ for fi in files:
469
+ rel = fi.relative_path
470
+ file_meta[rel] = {
471
+ "path": rel,
472
+ "language": fi.language,
473
+ "line_count": fi.line_count,
474
+ "importance": fi.importance,
475
+ "is_test": fi.is_test,
476
+ "is_config": fi.is_config,
477
+ }
478
+
479
+ # Compute fan_in / fan_out lazily
480
+ all_known_files = set(file_meta.keys())
481
+ if dep_graph is not None:
482
+ for src in dep_graph.forward:
483
+ all_known_files.add(src)
484
+ all_known_files.update(dep_graph.forward[src])
485
+ for tgt in dep_graph.reverse:
486
+ all_known_files.add(tgt)
487
+ all_known_files.update(dep_graph.reverse[tgt])
488
+
489
+ for f in all_known_files:
490
+ if f not in file_meta:
491
+ file_meta[f] = {
492
+ "path": f,
493
+ "language": "",
494
+ "line_count": 0,
495
+ "importance": 0.0,
496
+ "is_test": False,
497
+ "is_config": False,
498
+ }
499
+ if dep_graph is not None:
500
+ file_meta[f]["fan_in"] = len(dep_graph.reverse.get(f, set()))
501
+ file_meta[f]["fan_out"] = len(dep_graph.forward.get(f, set()))
502
+ else:
503
+ file_meta[f]["fan_in"] = 0
504
+ file_meta[f]["fan_out"] = 0
505
+
506
+ # Resolve the query as a chain of node-edge-node patterns
507
+ # Start with bindings for the first node
508
+ bindings = self._resolve_start_node(ast.nodes[0], file_meta)
509
+
510
+ # For each edge, expand bindings via BFS
511
+ for edge in ast.edges:
512
+ bindings = self._traverse_edge(edge, ast.nodes, bindings, dep_graph, file_meta)
513
+ if not bindings:
514
+ break
515
+
516
+ # Apply WHERE filters
517
+ for clause in ast.where_clauses:
518
+ bindings = self._apply_where(clause, bindings, file_meta)
519
+
520
+ # Build return dicts
521
+ results = self._build_results(ast.return_specs, ast.nodes, bindings, file_meta)
522
+
523
+ # Deduplicate
524
+ seen: set[str] = set()
525
+ unique_results: list[dict[str, Any]] = []
526
+ for r in results:
527
+ key = str(sorted(r.items()))
528
+ if key not in seen:
529
+ seen.add(key)
530
+ unique_results.append(r)
531
+
532
+ return unique_results[:self.MAX_RESULTS]
533
+
534
+ # -- Internal --
535
+
536
+ def _resolve_start_node(
537
+ self,
538
+ node: NodePattern,
539
+ file_meta: dict[str, dict[str, Any]],
540
+ ) -> list[dict[str, str]]:
541
+ """Resolve the starting node to a list of variable bindings.
542
+
543
+ Each binding is a dict mapping variable names to file paths.
544
+ """
545
+ if node.file_pattern is not None:
546
+ # Concrete file or glob pattern
547
+ matched = self._match_files(node.file_pattern, file_meta)
548
+ if not matched:
549
+ return []
550
+ var = node.variable or "_anon_0"
551
+ return [{var: f} for f in matched]
552
+ else:
553
+ # Unbound variable — starts from all known files
554
+ var = node.variable or "_anon_0"
555
+ return [{var: f} for f in file_meta]
556
+
557
+ def _traverse_edge(
558
+ self,
559
+ edge: EdgePattern,
560
+ nodes: list[NodePattern],
561
+ bindings: list[dict[str, str]],
562
+ dep_graph: Any,
563
+ file_meta: dict[str, dict[str, Any]],
564
+ ) -> list[dict[str, str]]:
565
+ """Expand each binding by traversing the edge via BFS."""
566
+ if dep_graph is None:
567
+ return []
568
+
569
+ src_node = nodes[edge.source]
570
+ tgt_node = nodes[edge.target]
571
+ src_var = src_node.variable or f"_anon_{edge.source}"
572
+ tgt_var = tgt_node.variable or f"_anon_{edge.target}"
573
+
574
+ use_reverse = edge.edge_type == "IMPORTED_BY"
575
+
576
+ new_bindings: list[dict[str, str]] = []
577
+ for binding in bindings:
578
+ src_file = binding.get(src_var)
579
+ if src_file is None:
580
+ continue
581
+
582
+ # BFS from src_file with depth constraints
583
+ reachable = self._bfs(
584
+ src_file, dep_graph, use_reverse,
585
+ edge.min_depth, edge.max_depth,
586
+ )
587
+
588
+ # If target node has a file_pattern, filter reachable
589
+ if tgt_node.file_pattern is not None:
590
+ pattern_matches = set(self._match_files(tgt_node.file_pattern, file_meta))
591
+ reachable = reachable & pattern_matches
592
+
593
+ for target_file in reachable:
594
+ new_binding = dict(binding)
595
+ new_binding[tgt_var] = target_file
596
+ new_bindings.append(new_binding)
597
+
598
+ return new_bindings
599
+
600
+ def _bfs(
601
+ self,
602
+ start: str,
603
+ dep_graph: Any,
604
+ use_reverse: bool,
605
+ min_depth: int,
606
+ max_depth: int,
607
+ ) -> set[str]:
608
+ """BFS traversal from *start*, returning files at depth [min_depth, max_depth]."""
609
+ graph = dep_graph.reverse if use_reverse else dep_graph.forward
610
+
611
+ visited: set[str] = set()
612
+ result: set[str] = set()
613
+ queue: deque[tuple[str, int]] = deque([(start, 0)])
614
+
615
+ while queue:
616
+ current, depth = queue.popleft()
617
+ if current in visited:
618
+ continue
619
+ visited.add(current)
620
+
621
+ if min_depth <= depth <= max_depth and current != start:
622
+ result.add(current)
623
+
624
+ if depth < max_depth:
625
+ neighbors = graph.get(current, set())
626
+ for neighbor in neighbors:
627
+ if neighbor not in visited:
628
+ queue.append((neighbor, depth + 1))
629
+
630
+ return result
631
+
632
+ def _match_files(
633
+ self,
634
+ pattern: str,
635
+ file_meta: dict[str, dict[str, Any]],
636
+ ) -> list[str]:
637
+ """Match files against a literal path or glob pattern."""
638
+ # Exact match first
639
+ if pattern in file_meta:
640
+ return [pattern]
641
+
642
+ # Try glob matching
643
+ matched = [f for f in file_meta if fnmatch.fnmatch(f, pattern)]
644
+ if matched:
645
+ return sorted(matched)
646
+
647
+ # Try suffix matching (user might omit leading path components)
648
+ suffix_matched = [f for f in file_meta if f.endswith(pattern)]
649
+ if suffix_matched:
650
+ return sorted(suffix_matched)
651
+
652
+ return []
653
+
654
+ def _apply_where(
655
+ self,
656
+ clause: WhereClause,
657
+ bindings: list[dict[str, str]],
658
+ file_meta: dict[str, dict[str, Any]],
659
+ ) -> list[dict[str, str]]:
660
+ """Filter bindings by a WHERE condition."""
661
+ result: list[dict[str, str]] = []
662
+ for binding in bindings:
663
+ file_path = binding.get(clause.variable)
664
+ if file_path is None:
665
+ continue
666
+ meta = file_meta.get(file_path, {})
667
+ field_val = meta.get(clause.field_name)
668
+ if field_val is None:
669
+ continue
670
+
671
+ if self._eval_condition(field_val, clause.operator, clause.value):
672
+ result.append(binding)
673
+
674
+ return result
675
+
676
+ @staticmethod
677
+ def _eval_condition(field_val: Any, operator: str, raw_value: str) -> bool:
678
+ """Evaluate a single comparison condition."""
679
+ if operator == "LIKE":
680
+ # LIKE uses fnmatch-style globbing
681
+ return fnmatch.fnmatch(str(field_val), raw_value)
682
+
683
+ # Try numeric comparison
684
+ if isinstance(field_val, (int, float)):
685
+ try:
686
+ cmp_val = float(raw_value)
687
+ except ValueError:
688
+ return str(field_val) == raw_value
689
+ if operator == "=":
690
+ return field_val == cmp_val
691
+ if operator == "!=":
692
+ return field_val != cmp_val
693
+ if operator == ">":
694
+ return field_val > cmp_val
695
+ if operator == "<":
696
+ return field_val < cmp_val
697
+ if operator == ">=":
698
+ return field_val >= cmp_val
699
+ if operator == "<=":
700
+ return field_val <= cmp_val
701
+
702
+ # Boolean comparison
703
+ if isinstance(field_val, bool):
704
+ cmp_val_bool = raw_value.lower() in ("true", "1", "yes")
705
+ if operator == "=":
706
+ return field_val == cmp_val_bool
707
+ if operator == "!=":
708
+ return field_val != cmp_val_bool
709
+
710
+ # String comparison
711
+ str_val = str(field_val)
712
+ if operator == "=":
713
+ return str_val == raw_value
714
+ if operator == "!=":
715
+ return str_val != raw_value
716
+ if operator == ">":
717
+ return str_val > raw_value
718
+ if operator == "<":
719
+ return str_val < raw_value
720
+ if operator == ">=":
721
+ return str_val >= raw_value
722
+ if operator == "<=":
723
+ return str_val <= raw_value
724
+
725
+ return False
726
+
727
+ def _build_results(
728
+ self,
729
+ return_specs: list[ReturnSpec],
730
+ nodes: list[NodePattern],
731
+ bindings: list[dict[str, str]],
732
+ file_meta: dict[str, dict[str, Any]],
733
+ ) -> list[dict[str, Any]]:
734
+ """Build the final result dicts from bindings and return specs."""
735
+ # Handle COUNT special case
736
+ if any(r.field_name == "count" and r.variable == "*" for r in return_specs):
737
+ return [{"count": len(bindings)}]
738
+
739
+ results: list[dict[str, Any]] = []
740
+ for binding in bindings:
741
+ row: dict[str, Any] = {}
742
+ for spec in return_specs:
743
+ file_path = binding.get(spec.variable)
744
+ if file_path is None:
745
+ # Try to find the variable by node index
746
+ for i, node in enumerate(nodes):
747
+ if node.variable == spec.variable:
748
+ file_path = binding.get(node.variable)
749
+ break
750
+ if file_path is None:
751
+ row[spec.variable] = None
752
+ continue
753
+
754
+ if spec.field_name is not None:
755
+ meta = file_meta.get(file_path, {})
756
+ row[f"{spec.variable}.{spec.field_name}"] = meta.get(spec.field_name)
757
+ else:
758
+ row[spec.variable] = file_path
759
+ results.append(row)
760
+
761
+ return results