codebase_index 0.3.2 → 0.4.0

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/codebase_index.rb +3 -243
  3. metadata +28 -223
  4. data/CHANGELOG.md +0 -89
  5. data/CODE_OF_CONDUCT.md +0 -83
  6. data/CONTRIBUTING.md +0 -65
  7. data/LICENSE.txt +0 -21
  8. data/README.md +0 -325
  9. data/exe/codebase-console +0 -59
  10. data/exe/codebase-console-mcp +0 -22
  11. data/exe/codebase-index-mcp +0 -34
  12. data/exe/codebase-index-mcp-http +0 -37
  13. data/exe/codebase-index-mcp-start +0 -58
  14. data/lib/codebase_index/ast/call_site_extractor.rb +0 -106
  15. data/lib/codebase_index/ast/method_extractor.rb +0 -71
  16. data/lib/codebase_index/ast/node.rb +0 -116
  17. data/lib/codebase_index/ast/parser.rb +0 -614
  18. data/lib/codebase_index/ast.rb +0 -6
  19. data/lib/codebase_index/builder.rb +0 -200
  20. data/lib/codebase_index/cache/cache_middleware.rb +0 -199
  21. data/lib/codebase_index/cache/cache_store.rb +0 -264
  22. data/lib/codebase_index/cache/redis_cache_store.rb +0 -116
  23. data/lib/codebase_index/cache/solid_cache_store.rb +0 -111
  24. data/lib/codebase_index/chunking/chunk.rb +0 -84
  25. data/lib/codebase_index/chunking/semantic_chunker.rb +0 -295
  26. data/lib/codebase_index/console/adapters/cache_adapter.rb +0 -58
  27. data/lib/codebase_index/console/adapters/good_job_adapter.rb +0 -33
  28. data/lib/codebase_index/console/adapters/job_adapter.rb +0 -68
  29. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +0 -33
  30. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +0 -33
  31. data/lib/codebase_index/console/audit_logger.rb +0 -75
  32. data/lib/codebase_index/console/bridge.rb +0 -177
  33. data/lib/codebase_index/console/confirmation.rb +0 -90
  34. data/lib/codebase_index/console/connection_manager.rb +0 -173
  35. data/lib/codebase_index/console/console_response_renderer.rb +0 -74
  36. data/lib/codebase_index/console/embedded_executor.rb +0 -373
  37. data/lib/codebase_index/console/model_validator.rb +0 -81
  38. data/lib/codebase_index/console/rack_middleware.rb +0 -87
  39. data/lib/codebase_index/console/safe_context.rb +0 -82
  40. data/lib/codebase_index/console/server.rb +0 -612
  41. data/lib/codebase_index/console/sql_validator.rb +0 -172
  42. data/lib/codebase_index/console/tools/tier1.rb +0 -118
  43. data/lib/codebase_index/console/tools/tier2.rb +0 -117
  44. data/lib/codebase_index/console/tools/tier3.rb +0 -110
  45. data/lib/codebase_index/console/tools/tier4.rb +0 -79
  46. data/lib/codebase_index/coordination/pipeline_lock.rb +0 -109
  47. data/lib/codebase_index/cost_model/embedding_cost.rb +0 -88
  48. data/lib/codebase_index/cost_model/estimator.rb +0 -128
  49. data/lib/codebase_index/cost_model/provider_pricing.rb +0 -67
  50. data/lib/codebase_index/cost_model/storage_cost.rb +0 -52
  51. data/lib/codebase_index/cost_model.rb +0 -22
  52. data/lib/codebase_index/db/migrations/001_create_units.rb +0 -38
  53. data/lib/codebase_index/db/migrations/002_create_edges.rb +0 -35
  54. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +0 -37
  55. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +0 -45
  56. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +0 -40
  57. data/lib/codebase_index/db/migrator.rb +0 -71
  58. data/lib/codebase_index/db/schema_version.rb +0 -73
  59. data/lib/codebase_index/dependency_graph.rb +0 -236
  60. data/lib/codebase_index/embedding/indexer.rb +0 -140
  61. data/lib/codebase_index/embedding/openai.rb +0 -126
  62. data/lib/codebase_index/embedding/provider.rb +0 -162
  63. data/lib/codebase_index/embedding/text_preparer.rb +0 -112
  64. data/lib/codebase_index/evaluation/baseline_runner.rb +0 -115
  65. data/lib/codebase_index/evaluation/evaluator.rb +0 -139
  66. data/lib/codebase_index/evaluation/metrics.rb +0 -79
  67. data/lib/codebase_index/evaluation/query_set.rb +0 -148
  68. data/lib/codebase_index/evaluation/report_generator.rb +0 -90
  69. data/lib/codebase_index/extracted_unit.rb +0 -145
  70. data/lib/codebase_index/extractor.rb +0 -1028
  71. data/lib/codebase_index/extractors/action_cable_extractor.rb +0 -201
  72. data/lib/codebase_index/extractors/ast_source_extraction.rb +0 -46
  73. data/lib/codebase_index/extractors/behavioral_profile.rb +0 -309
  74. data/lib/codebase_index/extractors/caching_extractor.rb +0 -261
  75. data/lib/codebase_index/extractors/callback_analyzer.rb +0 -246
  76. data/lib/codebase_index/extractors/concern_extractor.rb +0 -292
  77. data/lib/codebase_index/extractors/configuration_extractor.rb +0 -219
  78. data/lib/codebase_index/extractors/controller_extractor.rb +0 -404
  79. data/lib/codebase_index/extractors/database_view_extractor.rb +0 -278
  80. data/lib/codebase_index/extractors/decorator_extractor.rb +0 -253
  81. data/lib/codebase_index/extractors/engine_extractor.rb +0 -223
  82. data/lib/codebase_index/extractors/event_extractor.rb +0 -211
  83. data/lib/codebase_index/extractors/factory_extractor.rb +0 -289
  84. data/lib/codebase_index/extractors/graphql_extractor.rb +0 -892
  85. data/lib/codebase_index/extractors/i18n_extractor.rb +0 -117
  86. data/lib/codebase_index/extractors/job_extractor.rb +0 -374
  87. data/lib/codebase_index/extractors/lib_extractor.rb +0 -218
  88. data/lib/codebase_index/extractors/mailer_extractor.rb +0 -269
  89. data/lib/codebase_index/extractors/manager_extractor.rb +0 -188
  90. data/lib/codebase_index/extractors/middleware_extractor.rb +0 -133
  91. data/lib/codebase_index/extractors/migration_extractor.rb +0 -469
  92. data/lib/codebase_index/extractors/model_extractor.rb +0 -988
  93. data/lib/codebase_index/extractors/phlex_extractor.rb +0 -252
  94. data/lib/codebase_index/extractors/policy_extractor.rb +0 -191
  95. data/lib/codebase_index/extractors/poro_extractor.rb +0 -229
  96. data/lib/codebase_index/extractors/pundit_extractor.rb +0 -223
  97. data/lib/codebase_index/extractors/rails_source_extractor.rb +0 -473
  98. data/lib/codebase_index/extractors/rake_task_extractor.rb +0 -343
  99. data/lib/codebase_index/extractors/route_extractor.rb +0 -181
  100. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +0 -331
  101. data/lib/codebase_index/extractors/serializer_extractor.rb +0 -339
  102. data/lib/codebase_index/extractors/service_extractor.rb +0 -217
  103. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +0 -91
  104. data/lib/codebase_index/extractors/shared_utility_methods.rb +0 -281
  105. data/lib/codebase_index/extractors/state_machine_extractor.rb +0 -398
  106. data/lib/codebase_index/extractors/test_mapping_extractor.rb +0 -225
  107. data/lib/codebase_index/extractors/validator_extractor.rb +0 -211
  108. data/lib/codebase_index/extractors/view_component_extractor.rb +0 -311
  109. data/lib/codebase_index/extractors/view_template_extractor.rb +0 -261
  110. data/lib/codebase_index/feedback/gap_detector.rb +0 -89
  111. data/lib/codebase_index/feedback/store.rb +0 -119
  112. data/lib/codebase_index/filename_utils.rb +0 -32
  113. data/lib/codebase_index/flow_analysis/operation_extractor.rb +0 -206
  114. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +0 -154
  115. data/lib/codebase_index/flow_assembler.rb +0 -290
  116. data/lib/codebase_index/flow_document.rb +0 -191
  117. data/lib/codebase_index/flow_precomputer.rb +0 -102
  118. data/lib/codebase_index/formatting/base.rb +0 -30
  119. data/lib/codebase_index/formatting/claude_adapter.rb +0 -98
  120. data/lib/codebase_index/formatting/generic_adapter.rb +0 -56
  121. data/lib/codebase_index/formatting/gpt_adapter.rb +0 -64
  122. data/lib/codebase_index/formatting/human_adapter.rb +0 -78
  123. data/lib/codebase_index/graph_analyzer.rb +0 -374
  124. data/lib/codebase_index/mcp/bootstrapper.rb +0 -96
  125. data/lib/codebase_index/mcp/index_reader.rb +0 -394
  126. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +0 -81
  127. data/lib/codebase_index/mcp/renderers/json_renderer.rb +0 -17
  128. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +0 -353
  129. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +0 -240
  130. data/lib/codebase_index/mcp/server.rb +0 -961
  131. data/lib/codebase_index/mcp/tool_response_renderer.rb +0 -85
  132. data/lib/codebase_index/model_name_cache.rb +0 -51
  133. data/lib/codebase_index/notion/client.rb +0 -217
  134. data/lib/codebase_index/notion/exporter.rb +0 -219
  135. data/lib/codebase_index/notion/mapper.rb +0 -40
  136. data/lib/codebase_index/notion/mappers/column_mapper.rb +0 -57
  137. data/lib/codebase_index/notion/mappers/migration_mapper.rb +0 -39
  138. data/lib/codebase_index/notion/mappers/model_mapper.rb +0 -161
  139. data/lib/codebase_index/notion/mappers/shared.rb +0 -22
  140. data/lib/codebase_index/notion/rate_limiter.rb +0 -68
  141. data/lib/codebase_index/observability/health_check.rb +0 -79
  142. data/lib/codebase_index/observability/instrumentation.rb +0 -34
  143. data/lib/codebase_index/observability/structured_logger.rb +0 -57
  144. data/lib/codebase_index/operator/error_escalator.rb +0 -81
  145. data/lib/codebase_index/operator/pipeline_guard.rb +0 -92
  146. data/lib/codebase_index/operator/status_reporter.rb +0 -80
  147. data/lib/codebase_index/railtie.rb +0 -38
  148. data/lib/codebase_index/resilience/circuit_breaker.rb +0 -99
  149. data/lib/codebase_index/resilience/index_validator.rb +0 -167
  150. data/lib/codebase_index/resilience/retryable_provider.rb +0 -108
  151. data/lib/codebase_index/retrieval/context_assembler.rb +0 -261
  152. data/lib/codebase_index/retrieval/query_classifier.rb +0 -133
  153. data/lib/codebase_index/retrieval/ranker.rb +0 -277
  154. data/lib/codebase_index/retrieval/search_executor.rb +0 -316
  155. data/lib/codebase_index/retriever.rb +0 -152
  156. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +0 -170
  157. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +0 -77
  158. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +0 -18
  159. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +0 -280
  160. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +0 -143
  161. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +0 -143
  162. data/lib/codebase_index/ruby_analyzer.rb +0 -87
  163. data/lib/codebase_index/session_tracer/file_store.rb +0 -104
  164. data/lib/codebase_index/session_tracer/middleware.rb +0 -143
  165. data/lib/codebase_index/session_tracer/redis_store.rb +0 -106
  166. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +0 -254
  167. data/lib/codebase_index/session_tracer/session_flow_document.rb +0 -223
  168. data/lib/codebase_index/session_tracer/solid_cache_store.rb +0 -139
  169. data/lib/codebase_index/session_tracer/store.rb +0 -81
  170. data/lib/codebase_index/storage/graph_store.rb +0 -120
  171. data/lib/codebase_index/storage/metadata_store.rb +0 -196
  172. data/lib/codebase_index/storage/pgvector.rb +0 -195
  173. data/lib/codebase_index/storage/qdrant.rb +0 -205
  174. data/lib/codebase_index/storage/vector_store.rb +0 -167
  175. data/lib/codebase_index/temporal/json_snapshot_store.rb +0 -245
  176. data/lib/codebase_index/temporal/snapshot_store.rb +0 -345
  177. data/lib/codebase_index/token_utils.rb +0 -19
  178. data/lib/codebase_index/version.rb +0 -5
  179. data/lib/generators/codebase_index/install_generator.rb +0 -32
  180. data/lib/generators/codebase_index/pgvector_generator.rb +0 -37
  181. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +0 -15
  182. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +0 -43
  183. data/lib/tasks/codebase_index.rake +0 -597
  184. data/lib/tasks/codebase_index_evaluation.rake +0 -115
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CodebaseIndex
4
- module MCP
5
- # Base class for rendering MCP tool responses in different output formats.
6
- #
7
- # Subclasses implement tool-specific render methods (render_lookup, render_search, etc.)
8
- # and a render_default fallback. The dispatch uses convention: tool name maps to method name.
9
- #
10
- # @example
11
- # renderer = ToolResponseRenderer.for(:markdown)
12
- # renderer.render(:lookup, unit_data)
13
- #
14
- class ToolResponseRenderer
15
- VALID_FORMATS = %i[claude markdown plain json].freeze
16
-
17
- # Factory method to build the appropriate renderer for a format.
18
- #
19
- # @param format [Symbol] One of :claude, :markdown, :plain, :json
20
- # @return [ToolResponseRenderer] A renderer instance
21
- # @raise [ArgumentError] if format is unknown
22
- def self.for(format)
23
- require_relative 'renderers/markdown_renderer'
24
- require_relative 'renderers/claude_renderer'
25
- require_relative 'renderers/plain_renderer'
26
- require_relative 'renderers/json_renderer'
27
-
28
- case format
29
- when :claude then Renderers::ClaudeRenderer.new
30
- when :markdown then Renderers::MarkdownRenderer.new
31
- when :plain then Renderers::PlainRenderer.new
32
- when :json then Renderers::JsonRenderer.new
33
- else raise ArgumentError, "Unknown format: #{format.inspect}. Valid: #{VALID_FORMATS.inspect}"
34
- end
35
- end
36
-
37
- # Render a tool response. Dispatches to render_<tool_name> if defined,
38
- # otherwise falls back to render_default.
39
- #
40
- # @param tool_name [Symbol, String] The tool name
41
- # @param data [Object] The tool result data
42
- # @param opts [Hash] Additional rendering options
43
- # @return [String] Rendered response text
44
- def render(tool_name, data, **opts)
45
- method_name = :"render_#{tool_name}"
46
- if respond_to?(method_name, true)
47
- send(method_name, data, **opts)
48
- else
49
- render_default(data)
50
- end
51
- end
52
-
53
- # Default rendering — subclasses must implement.
54
- #
55
- # @param data [Object] The data to render
56
- # @return [String] Rendered text
57
- def render_default(data)
58
- raise NotImplementedError, "#{self.class}#render_default must be implemented"
59
- end
60
-
61
- private
62
-
63
- # Fetch a value from a hash by symbol or string key, falling back to a default.
64
- #
65
- # Handles data hashes that may use either symbol or string keys (e.g., data
66
- # assembled from JSON parsing vs. direct Hash literals).
67
- #
68
- # @param data [Hash] The source hash
69
- # @param key [Symbol, String] The key to look up
70
- # @param default [Object] Value to return when key is absent (default: nil)
71
- # @return [Object]
72
- def fetch_key(data, key, default = nil)
73
- sym_key = key.to_sym
74
- str_key = key.to_s
75
- if data.key?(sym_key)
76
- data[sym_key]
77
- elsif data.key?(str_key)
78
- data[str_key]
79
- else
80
- default
81
- end
82
- end
83
- end
84
- end
85
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CodebaseIndex
4
- # Caches ActiveRecord model names and builds a precompiled regex
5
- # for scanning source code for model references.
6
- #
7
- # Avoids O(n*m) per-extractor iteration of ActiveRecord::Base.descendants.
8
- # Invalidated per extraction run (call .reset! before a new run).
9
- #
10
- # @example
11
- # CodebaseIndex::ModelNameCache.model_names
12
- # # => ["User", "Order", "Product", ...]
13
- #
14
- # CodebaseIndex::ModelNameCache.model_names_regex
15
- # # => /\b(?:User|Order|Product|...)\b/
16
- #
17
- module ModelNameCache
18
- class << self
19
- # @return [Array<String>] All named AR model descendant names
20
- def model_names
21
- @model_names ||= compute_model_names
22
- end
23
-
24
- # @return [Regexp] Precompiled regex matching any model name as a whole word
25
- def model_names_regex
26
- @model_names_regex ||= build_regex
27
- end
28
-
29
- # Clear cache (call at the start of each extraction run)
30
- def reset!
31
- @model_names = nil
32
- @model_names_regex = nil
33
- end
34
-
35
- private
36
-
37
- def compute_model_names
38
- return [] unless defined?(ActiveRecord::Base)
39
-
40
- ActiveRecord::Base.descendants.filter_map(&:name).uniq
41
- end
42
-
43
- def build_regex
44
- names = model_names
45
- return /(?!)/ if names.empty? # never-matching regex
46
-
47
- /\b(?:#{names.map { |n| Regexp.escape(n) }.join('|')})\b/
48
- end
49
- end
50
- end
51
- end
@@ -1,217 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'net/http'
5
- require 'uri'
6
- require 'codebase_index'
7
- require_relative 'rate_limiter'
8
-
9
- module CodebaseIndex
10
- module Notion
11
- # Thin wrapper around the Notion REST API (v2022-06-28).
12
- #
13
- # Uses Net::HTTP (stdlib) for zero external dependencies. All requests are
14
- # throttled through a {RateLimiter} to respect Notion's 3 req/sec limit.
15
- #
16
- # @example
17
- # client = Client.new(api_token: "secret_...")
18
- # client.create_page(database_id: "db-uuid", properties: { ... })
19
- # client.query_database(database_id: "db-uuid", filter: { ... })
20
- #
21
- class Client # rubocop:disable Metrics/ClassLength
22
- BASE_URL = 'https://api.notion.com/v1'
23
- NOTION_VERSION = '2022-06-28'
24
- MAX_RETRIES = 3
25
- DEFAULT_TIMEOUT = 30
26
-
27
- # @param api_token [String] Notion integration API token
28
- # @param rate_limiter [RateLimiter] Rate limiter instance (default: 3 req/sec)
29
- # @raise [ArgumentError] if api_token is nil or empty
30
- def initialize(api_token:, rate_limiter: RateLimiter.new)
31
- raise ArgumentError, 'api_token is required' if api_token.nil? || api_token.to_s.empty?
32
-
33
- @api_token = api_token
34
- @rate_limiter = rate_limiter
35
- end
36
-
37
- # Create a page in a Notion database.
38
- #
39
- # @param database_id [String] Target database UUID
40
- # @param properties [Hash] Page properties in Notion API format
41
- # @param children [Array<Hash>] Optional page content blocks
42
- # @return [Hash] Created page data
43
- def create_page(database_id:, properties:, children: [])
44
- body = {
45
- parent: { database_id: database_id },
46
- properties: properties
47
- }
48
- body[:children] = children if children.any?
49
-
50
- request(:post, 'pages', body)
51
- end
52
-
53
- # Update an existing page's properties.
54
- #
55
- # @param page_id [String] Page UUID to update
56
- # @param properties [Hash] Properties to update
57
- # @return [Hash] Updated page data
58
- def update_page(page_id:, properties:)
59
- request(:patch, "pages/#{page_id}", { properties: properties })
60
- end
61
-
62
- # Query a database with optional filter and sort.
63
- #
64
- # @param database_id [String] Database UUID
65
- # @param filter [Hash, nil] Notion filter object
66
- # @param sorts [Array<Hash>, nil] Notion sort objects
67
- # @return [Hash] Query results with 'results', 'has_more', 'next_cursor'
68
- def query_database(database_id:, filter: nil, sorts: nil)
69
- body = {}
70
- body[:filter] = filter if filter
71
- body[:sorts] = sorts if sorts
72
-
73
- request(:post, "databases/#{database_id}/query", body)
74
- end
75
-
76
- # Query all pages from a database, auto-paginating.
77
- #
78
- # @param database_id [String] Database UUID
79
- # @param filter [Hash, nil] Notion filter object
80
- # @return [Array<Hash>] All matching pages
81
- def query_all(database_id:, filter: nil)
82
- all_results = []
83
- cursor = nil
84
-
85
- loop do
86
- body = {}
87
- body[:filter] = filter if filter
88
- body[:start_cursor] = cursor if cursor
89
-
90
- response = request(:post, "databases/#{database_id}/query", body)
91
- all_results.concat(response['results'] || [])
92
-
93
- break unless response['has_more']
94
-
95
- cursor = response['next_cursor']
96
- end
97
-
98
- all_results
99
- end
100
-
101
- # Find a page by its title property value.
102
- #
103
- # @param database_id [String] Database UUID
104
- # @param title [String] Title text to search for
105
- # @return [Hash, nil] First matching page or nil
106
- def find_page_by_title(database_id:, title:)
107
- response = query_database(
108
- database_id: database_id,
109
- filter: {
110
- property: 'title',
111
- title: { equals: title }
112
- }
113
- )
114
-
115
- results = response['results'] || []
116
- results.first
117
- end
118
-
119
- private
120
-
121
- # Execute an HTTP request against the Notion API.
122
- #
123
- # @param method [Symbol] HTTP method (:post, :patch, :get)
124
- # @param path [String] API path (appended to BASE_URL)
125
- # @param body [Hash, nil] Request body
126
- # @return [Hash] Parsed JSON response
127
- # @raise [CodebaseIndex::Error] on non-success responses (after retries for 429)
128
- def request(method, path, body = nil)
129
- retries = 0
130
-
131
- loop do
132
- response = execute_with_retry(method, path, body)
133
-
134
- return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
135
-
136
- if response.code == '429' && retries < MAX_RETRIES
137
- retries += 1
138
- wait_time = (response['Retry-After'] || retries).to_f
139
- sleep(wait_time)
140
- next
141
- end
142
-
143
- raise_api_error(response)
144
- end
145
- end
146
-
147
- # Execute HTTP with rate limiting and network error retry.
148
- #
149
- # @return [Net::HTTPResponse]
150
- # @raise [CodebaseIndex::Error] on persistent network failures
151
- def execute_with_retry(method, path, body)
152
- attempts = 0
153
- begin
154
- @rate_limiter.throttle { execute_http(method, path, body) }
155
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED => e
156
- attempts += 1
157
- raise CodebaseIndex::Error, "Network error after #{attempts} retries: #{e.message}" if attempts >= MAX_RETRIES
158
-
159
- sleep(2**attempts)
160
- retry
161
- end
162
- end
163
-
164
- # Raise a descriptive error from a non-success Notion response.
165
- #
166
- # @raise [CodebaseIndex::Error]
167
- def raise_api_error(response)
168
- parsed = begin
169
- JSON.parse(response.body)
170
- rescue JSON::ParserError
171
- { 'message' => "Unparseable response body: #{response.body&.slice(0, 200)}" }
172
- end
173
- message = parsed['message'] || 'Unknown error'
174
- raise CodebaseIndex::Error, "Notion API error #{response.code}: #{message}"
175
- end
176
-
177
- # Perform the raw HTTP request.
178
- #
179
- # @param method [Symbol] HTTP method
180
- # @param path [String] API path
181
- # @param body [Hash, nil] Request body
182
- # @return [Net::HTTPResponse]
183
- def execute_http(method, path, body)
184
- uri = URI("#{BASE_URL}/#{path}")
185
- http = Net::HTTP.new(uri.host, uri.port)
186
- http.use_ssl = true
187
- http.open_timeout = DEFAULT_TIMEOUT
188
- http.read_timeout = DEFAULT_TIMEOUT
189
-
190
- req = build_request(method, uri, body)
191
- http.request(req)
192
- end
193
-
194
- # Build an HTTP request object with headers.
195
- #
196
- # @param method [Symbol] HTTP method
197
- # @param uri [URI] Full request URI
198
- # @param body [Hash, nil] Request body
199
- # @return [Net::HTTPRequest]
200
- def build_request(method, uri, body)
201
- req = case method
202
- when :post then Net::HTTP::Post.new(uri)
203
- when :patch then Net::HTTP::Patch.new(uri)
204
- when :get then Net::HTTP::Get.new(uri)
205
- else raise ArgumentError, "Unsupported HTTP method: #{method}"
206
- end
207
-
208
- req['Authorization'] = "Bearer #{@api_token}"
209
- req['Notion-Version'] = NOTION_VERSION
210
- req['Content-Type'] = 'application/json'
211
- req.body = JSON.generate(body) if body
212
-
213
- req
214
- end
215
- end
216
- end
217
- end
@@ -1,219 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'codebase_index'
4
- require_relative 'client'
5
- require_relative 'mapper'
6
- require_relative 'rate_limiter'
7
-
8
- module CodebaseIndex
9
- module Notion
10
- # Orchestrates syncing CodebaseIndex extraction data to Notion databases.
11
- #
12
- # Reads extraction output from disk via IndexReader, maps model and column data
13
- # to Notion page properties, and pushes via the Notion API. All syncs are idempotent —
14
- # existing pages are updated, new pages are created.
15
- #
16
- # @example
17
- # exporter = Exporter.new(index_dir: "tmp/codebase_index")
18
- # stats = exporter.sync_all
19
- # # => { data_models: 10, columns: 45, errors: [] }
20
- #
21
- class Exporter # rubocop:disable Metrics/ClassLength
22
- # @param index_dir [String] Path to extraction output directory
23
- # @param config [Configuration] CodebaseIndex configuration (default: global config)
24
- # @param client [Client, nil] Notion API client (auto-created from config if nil)
25
- # @param reader [Object, nil] IndexReader instance (auto-created from index_dir if nil)
26
- # @raise [ConfigurationError] if notion_api_token is not configured
27
- def initialize(index_dir:, config: CodebaseIndex.configuration, client: nil, reader: nil)
28
- api_token = config.notion_api_token
29
- raise ConfigurationError, 'notion_api_token is required for Notion export' unless api_token
30
-
31
- @database_ids = config.notion_database_ids || {}
32
- @client = client || Client.new(api_token: api_token)
33
- @reader = reader || build_reader(index_dir)
34
- @page_id_cache = {}
35
- end
36
-
37
- # Sync all configured databases. Idempotent — safe to re-run.
38
- #
39
- # @return [Hash] { data_models: Integer, columns: Integer, errors: Array<String> }
40
- def sync_all
41
- model_stats = @database_ids[:data_models] ? sync_data_models : empty_stats
42
- column_stats = @database_ids[:columns] && @database_ids[:data_models] ? sync_columns : empty_stats
43
-
44
- all_errors = model_stats[:errors] + column_stats[:errors]
45
-
46
- {
47
- data_models: model_stats[:synced],
48
- columns: column_stats[:synced],
49
- errors: cap_errors(all_errors)
50
- }
51
- end
52
-
53
- # Sync model units to the Data Models Notion database.
54
- #
55
- # @return [Hash] { synced: Integer, errors: Array<String> }
56
- def sync_data_models
57
- database_id = @database_ids[:data_models]
58
- return empty_stats unless database_id
59
-
60
- migration_dates = load_migration_dates
61
- sync_units('model', database_id, 'Table Name') do |unit_data|
62
- properties = Mappers::ModelMapper.new.map(unit_data)
63
- enrich_with_migration_date(properties, migration_dates)
64
- properties
65
- end
66
- end
67
-
68
- # Sync column data to the Columns Notion database.
69
- #
70
- # @return [Hash] { synced: Integer, errors: Array<String> }
71
- def sync_columns
72
- database_id = @database_ids[:columns]
73
- return empty_stats unless database_id
74
-
75
- synced = 0
76
- errors = []
77
-
78
- each_model_unit do |entry, unit_data|
79
- synced_count, unit_errors = sync_model_columns(entry, unit_data, database_id)
80
- synced += synced_count
81
- errors.concat(unit_errors)
82
- end
83
-
84
- { synced: synced, errors: errors }
85
- end
86
-
87
- MAX_ERRORS = 100
88
-
89
- private
90
-
91
- # Sync all units of a type, yielding each for property mapping.
92
- #
93
- # @param type [String] Unit type to list
94
- # @param database_id [String] Notion database UUID
95
- # @param title_property [String] Name of the title property
96
- # @yield [Hash] Unit data hash, expects Notion properties hash back
97
- # @return [Hash] { synced: Integer, errors: Array<String> }
98
- def sync_units(type, database_id, title_property)
99
- synced = 0
100
- errors = []
101
-
102
- @reader.list_units(type: type).each do |entry|
103
- unit_data = @reader.find_unit(entry['identifier'])
104
- next unless unit_data
105
-
106
- begin
107
- properties = yield(unit_data)
108
- title_value = extract_title_text(properties[title_property])
109
- page_id = upsert_page(database_id: database_id, title_value: title_value, properties: properties)
110
- @page_id_cache[entry['identifier']] = page_id
111
- synced += 1
112
- rescue StandardError => e
113
- errors << "#{entry['identifier']}: #{e.message}"
114
- end
115
- end
116
-
117
- { synced: synced, errors: errors }
118
- end
119
-
120
- # Iterate over loaded model units.
121
- #
122
- # @yield [Hash, Hash] Index entry and full unit data
123
- def each_model_unit
124
- @reader.list_units(type: 'model').each do |entry|
125
- unit_data = @reader.find_unit(entry['identifier'])
126
- next unless unit_data
127
-
128
- yield(entry, unit_data)
129
- end
130
- end
131
-
132
- # Sync columns for a single model.
133
- #
134
- # @return [Array(Integer, Array<String>)] Count of synced columns and errors
135
- def sync_model_columns(entry, unit_data, database_id)
136
- parent_page_id = @page_id_cache[entry['identifier']]
137
- columns = unit_data.dig('metadata', 'columns') || []
138
- validations = unit_data.dig('metadata', 'validations') || []
139
- mapper = Mappers::ColumnMapper.new
140
- synced = 0
141
- errors = []
142
-
143
- columns.each do |column|
144
- properties = mapper.map(column, model_identifier: entry['identifier'],
145
- validations: validations, parent_page_id: parent_page_id)
146
- upsert_page(database_id: database_id, title_value: column['name'], properties: properties)
147
- synced += 1
148
- rescue StandardError => e
149
- errors << "#{entry['identifier']}.#{column['name']}: #{e.message}"
150
- end
151
-
152
- [synced, errors]
153
- end
154
-
155
- # Enrich model properties with migration date if available.
156
- #
157
- # @param properties [Hash] Notion properties hash (mutated)
158
- # @param migration_dates [Hash] { table_name => date_string }
159
- def enrich_with_migration_date(properties, migration_dates)
160
- table_name = extract_title_text(properties['Table Name'])
161
- return unless migration_dates[table_name]
162
-
163
- properties['Last Schema Change'] = { date: { start: migration_dates[table_name] } }
164
- end
165
-
166
- # Load migration units and compute latest change dates per table.
167
- #
168
- # @return [Hash<String, String>] { table_name => latest_date }
169
- def load_migration_dates
170
- mapper = Mappers::MigrationMapper.new
171
- units = @reader.list_units(type: 'migration').filter_map { |e| @reader.find_unit(e['identifier']) }
172
- mapper.latest_changes(units)
173
- rescue StandardError
174
- {}
175
- end
176
-
177
- # Upsert a Notion page: find by title, update if exists, create if not.
178
- #
179
- # @return [String] Notion page ID
180
- def upsert_page(database_id:, title_value:, properties:)
181
- existing = @client.find_page_by_title(database_id: database_id, title: title_value)
182
-
183
- if existing
184
- @client.update_page(page_id: existing['id'], properties: properties)
185
- existing['id']
186
- else
187
- result = @client.create_page(database_id: database_id, properties: properties)
188
- result['id']
189
- end
190
- end
191
-
192
- # @return [Hash]
193
- def empty_stats
194
- { synced: 0, errors: [] }
195
- end
196
-
197
- # Cap errors to prevent unbounded memory growth.
198
- #
199
- # @param errors [Array<String>]
200
- # @return [Array<String>]
201
- def cap_errors(errors)
202
- return errors if errors.size <= MAX_ERRORS
203
-
204
- errors.first(MAX_ERRORS) + ["... and #{errors.size - MAX_ERRORS} more errors"]
205
- end
206
-
207
- # @return [String]
208
- def extract_title_text(title_prop)
209
- title_prop&.dig(:title, 0, :text, :content) || ''
210
- end
211
-
212
- # @return [Object] IndexReader
213
- def build_reader(index_dir)
214
- require_relative '../mcp/index_reader'
215
- CodebaseIndex::MCP::IndexReader.new(index_dir)
216
- end
217
- end
218
- end
219
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'mappers/shared'
4
- require_relative 'mappers/model_mapper'
5
- require_relative 'mappers/column_mapper'
6
- require_relative 'mappers/migration_mapper'
7
-
8
- module CodebaseIndex
9
- module Notion
10
- # Dispatcher for Notion mappers. Returns the appropriate mapper for a unit type.
11
- #
12
- # @example
13
- # mapper = Mapper.for("model")
14
- # properties = mapper.map(unit_data)
15
- #
16
- class Mapper
17
- REGISTRY = {
18
- 'model' => Mappers::ModelMapper,
19
- 'column' => Mappers::ColumnMapper,
20
- 'migration' => Mappers::MigrationMapper
21
- }.freeze
22
-
23
- # Get a mapper instance for a unit type.
24
- #
25
- # @param type [String] Unit type name (e.g. "model", "column", "migration")
26
- # @return [Object, nil] Mapper instance or nil if type is not supported
27
- def self.for(type)
28
- klass = REGISTRY[type]
29
- klass&.new
30
- end
31
-
32
- # List all supported unit types.
33
- #
34
- # @return [Array<String>]
35
- def self.supported_types
36
- REGISTRY.keys
37
- end
38
- end
39
- end
40
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'shared'
4
-
5
- module CodebaseIndex
6
- module Notion
7
- module Mappers
8
- # Maps individual column metadata to Notion page properties for the Columns database.
9
- #
10
- # Each column from a model's metadata becomes a separate Notion page, optionally
11
- # linked to its parent Data Models page via a relation property.
12
- #
13
- # @example
14
- # mapper = ColumnMapper.new
15
- # properties = mapper.map(column, model_identifier: "User", validations: [...], parent_page_id: "page-123")
16
- #
17
- class ColumnMapper
18
- include Shared
19
-
20
- # Map a single column to Notion Columns page properties.
21
- #
22
- # @param column [Hash] Column hash from metadata["columns"] (name, type, null, default)
23
- # @param model_identifier [String] Parent model name (for context)
24
- # @param validations [Array<Hash>] Model-level validations to match against this column
25
- # @param parent_page_id [String, nil] Notion page ID of the Data Models parent page
26
- # @return [Hash] Notion page properties hash
27
- def map(column, model_identifier: nil, validations: [], parent_page_id: nil) # rubocop:disable Lint/UnusedMethodArgument
28
- properties = {
29
- 'Column Name' => { title: [{ text: { content: column['name'] } }] },
30
- 'Data Type' => { select: { name: column['type'] } },
31
- 'Nullable' => { checkbox: column['null'] == true },
32
- 'Default Value' => rich_text_property(column['default'].to_s),
33
- 'Validation Rules' => rich_text_property(format_validation_rules(column['name'], validations))
34
- }
35
-
36
- properties['Table'] = { relation: [{ id: parent_page_id }] } if parent_page_id
37
-
38
- properties
39
- end
40
-
41
- private
42
-
43
- # Find and format validations matching this column name.
44
- #
45
- # @param column_name [String]
46
- # @param validations [Array<Hash>]
47
- # @return [String]
48
- def format_validation_rules(column_name, validations)
49
- matched = validations.select { |v| v['attribute'] == column_name }
50
- return 'None' if matched.empty?
51
-
52
- matched.map { |v| v['type'] }.join(', ')
53
- end
54
- end
55
- end
56
- end
57
- end