mxrb 0.1.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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,623 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require_relative "vec_store"
6
+
7
+ module Mxrb
8
+ module Semantic
9
+ Artifact = Data.define(
10
+ :id, :qualified_name, :kind, :module_name, :name, :unit_id, :path, :metadata
11
+ )
12
+
13
+ Reference = Data.define(
14
+ :source, :target, :relation, :path, :value
15
+ )
16
+ UnresolvedReference = Data.define(
17
+ :source, :qualified_name, :expected_kinds, :path, :value
18
+ )
19
+
20
+ Impact = Data.define(:root, :artifacts, :references) do
21
+ def empty? = artifacts.empty?
22
+ end
23
+ ArtifactDetails = Data.define(:artifact, :incoming, :outgoing)
24
+ SemanticSearchHit = Data.define(:artifact, :distance)
25
+
26
+ # Builds a semantic index directly from an opened MPR. Ruby is the public
27
+ # API; the CLI only formats these results.
28
+ class Index
29
+ CACHE_VERSION = 3
30
+ CACHE_METADATA_KEYS = %i[
31
+ allowed_roles bson_type documentation excluded exposed from mark_as_used to
32
+ ].freeze
33
+
34
+ FIELD_RELATIONS = {
35
+ "microflow" => :calls,
36
+ "nanoflow" => :calls,
37
+ "javaaction" => :calls,
38
+ "javascriptaction" => :calls,
39
+ "workflow" => :calls,
40
+ "form" => :opens,
41
+ "page" => :opens,
42
+ "layout" => :uses_layout,
43
+ "snippet" => :uses_snippet,
44
+ "entity" => :uses_entity,
45
+ "attribute" => :uses_attribute,
46
+ "association" => :uses_association,
47
+ "enumeration" => :uses_enumeration
48
+ }.freeze
49
+
50
+ CALL_KINDS = %i[microflow nanoflow java_action javascript_action workflow].freeze
51
+ TOKEN = /[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){1,2}/
52
+ MEMBER_TOKEN = /[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*/
53
+
54
+ attr_reader :artifacts, :references, :unresolved_references
55
+
56
+ def self.fingerprint(project)
57
+ rows = project.query(
58
+ "SELECT UnitID, ContainerID, ContainmentName, ContentsHash " \
59
+ "FROM Unit ORDER BY UnitID"
60
+ )
61
+ digest = Digest::SHA256.new
62
+ digest << CACHE_VERSION.to_s
63
+ rows.each do |row|
64
+ row.each do |value|
65
+ bytes = value.to_s.b
66
+ digest << [bytes.bytesize].pack("Q>") << bytes
67
+ end
68
+ end
69
+ digest.hexdigest
70
+ rescue StandardError
71
+ nil
72
+ end
73
+
74
+ def initialize(project)
75
+ @project = project
76
+ reset_state
77
+ @fingerprint = compute_fingerprint
78
+ json = @fingerprint && @project.mpr.read_index_cache(@fingerprint)
79
+ unless json && restore_from_cache(json)
80
+ reset_state
81
+ build
82
+ @project.mpr.write_index_cache(@fingerprint, serialize_to_cache) if @fingerprint
83
+ end
84
+ end
85
+
86
+ def find(name, kind: nil)
87
+ return name if name.is_a?(Artifact)
88
+
89
+ matches = @by_name[name.to_s]
90
+ matches = matches.select { _1.kind == kind.to_sym } if kind
91
+ matches.one? ? matches.first : nil
92
+ end
93
+
94
+ def find_all(name)
95
+ @by_name[name.to_s].dup
96
+ end
97
+
98
+ def search(query, kind: nil, module_name: nil)
99
+ matcher = query.is_a?(Regexp) ? query : /#{Regexp.escape(query.to_s)}/i
100
+ @artifacts.select do |artifact|
101
+ matcher.match?(artifact.qualified_name) &&
102
+ (!kind || artifact.kind == kind.to_sym) &&
103
+ (!module_name || artifact.module_name == module_name.to_s)
104
+ end.sort_by { [_1.module_name.to_s, _1.kind.to_s, _1.qualified_name] }
105
+ end
106
+
107
+ # Semantic similarity search with optional sqlite-vec acceleration.
108
+ def semantic_search(query, limit: 10, backend: :auto)
109
+ semantic_search_hits(query, limit:, backend:).map(&:artifact).freeze
110
+ end
111
+ alias semantic_search_artifacts semantic_search
112
+
113
+ def semantic_search_hits(query, limit: 10, backend: :auto)
114
+ size = Integer(limit)
115
+ raise ArgumentError, "semantic search limit must be positive" unless size.positive?
116
+
117
+ embedder = semantic_embedder(backend)
118
+ store = vec_store(embedder)
119
+ return ruby_semantic_hits(query, embedder, size) unless store
120
+
121
+ begin
122
+ store.rebuild!(@artifacts) unless store.compatible?
123
+ store.search(query, limit: size).filter_map do |hit|
124
+ artifact = @by_id[hit[:id]]
125
+ SemanticSearchHit.new(artifact, cosine_distance(hit[:distance])) if artifact
126
+ end.freeze
127
+ rescue SQLite3::Exception, LoadError
128
+ ruby_semantic_hits(query, embedder, size)
129
+ end
130
+ end
131
+
132
+ def describe(artifact)
133
+ resolved = resolve_argument(artifact)
134
+ ArtifactDetails.new(
135
+ resolved,
136
+ references_to(resolved).freeze,
137
+ references_from(resolved).freeze
138
+ )
139
+ end
140
+
141
+ def references_to(target)
142
+ artifact = resolve_argument(target)
143
+ @references.select { _1.target.id == artifact.id }
144
+ end
145
+
146
+ def references_from(source)
147
+ artifact = resolve_argument(source)
148
+ @references.select { _1.source.id == artifact.id }
149
+ end
150
+
151
+ def callers_of(target)
152
+ artifact = resolve_argument(target)
153
+ references_to(artifact)
154
+ .select { _1.relation == :calls }
155
+ .map(&:source)
156
+ .uniq(&:id)
157
+ end
158
+
159
+ def callees_of(source)
160
+ artifact = resolve_argument(source)
161
+ references_from(artifact)
162
+ .select { _1.relation == :calls }
163
+ .map(&:target)
164
+ .uniq(&:id)
165
+ end
166
+
167
+ def impact_of(target, transitive: true)
168
+ root = resolve_argument(target)
169
+ seen = Set[root.id]
170
+ queue = [root]
171
+ affected = []
172
+ traversed = []
173
+
174
+ until queue.empty?
175
+ current = queue.shift
176
+ incoming = references_to(current)
177
+ traversed.concat(incoming)
178
+ incoming.each do |reference|
179
+ source = reference.source
180
+ next if seen.include?(source.id)
181
+
182
+ seen << source.id
183
+ affected << source
184
+ queue << source if transitive
185
+ end
186
+ end
187
+
188
+ Impact.new(root, affected.freeze, traversed.uniq.freeze)
189
+ end
190
+
191
+ private
192
+
193
+ def ruby_semantic_hits(query, embedder, limit)
194
+ Embedder.rank_with_distance(@artifacts, query, embedder:, limit:).map do |ranked|
195
+ SemanticSearchHit.new(ranked.artifact, ranked.distance)
196
+ end.freeze
197
+ end
198
+
199
+ # sqlite-vec returns Euclidean distance. Embeddings are normalized, so
200
+ # d² / 2 is the equivalent cosine distance used by the Ruby fallback.
201
+ def cosine_distance(euclidean) = (Float(euclidean)**2) / 2.0
202
+
203
+ def build
204
+ index_modules_and_domain_models
205
+ index_units
206
+ scan_documents
207
+ @artifacts.freeze
208
+ @references.freeze
209
+ @unresolved_references.freeze
210
+ end
211
+
212
+ def index_modules_and_domain_models
213
+ @project.modules.each do |mod|
214
+ add_artifact(
215
+ id: "module:#{mod.id}", qualified_name: mod.name, kind: :module,
216
+ module_name: mod.name, name: mod.name, unit_id: mod.id,
217
+ path: [], metadata: {}
218
+ )
219
+
220
+ entity_by_id = mod.entities.to_h { [_1.id, _1] }
221
+ mod.entities.each do |entity|
222
+ qualified = entity.qualified_name.to_s
223
+ qualified = "#{mod.name}.#{entity.name}" if qualified.empty?
224
+ artifact = add_artifact(
225
+ id: "entity:#{entity.id}", qualified_name: qualified, kind: :entity,
226
+ module_name: mod.name, name: entity.name, unit_id: mod.domain_model&.id,
227
+ path: ["entities", entity.name], metadata: { model: entity }
228
+ )
229
+ entity.attributes.each do |attribute|
230
+ add_artifact(
231
+ id: "attribute:#{attribute.id}",
232
+ qualified_name: "#{qualified}.#{attribute.name}", kind: :attribute,
233
+ module_name: mod.name, name: attribute.name, unit_id: mod.domain_model&.id,
234
+ path: artifact.path + ["attributes", attribute.name],
235
+ metadata: { model: attribute, entity: artifact }
236
+ )
237
+ end
238
+ end
239
+
240
+ mod.associations.each do |association|
241
+ from_entity = entity_by_id[association.from_entity_id]
242
+ to_entity = entity_by_id[association.to_entity_id]
243
+ add_artifact(
244
+ id: "association:#{association.id}",
245
+ qualified_name: "#{mod.name}.#{association.name}", kind: :association,
246
+ module_name: mod.name, name: association.name, unit_id: mod.domain_model&.id,
247
+ path: ["associations", association.name],
248
+ metadata: {
249
+ model: association,
250
+ from: qualified_entity_name(mod, from_entity),
251
+ to: qualified_entity_name(mod, to_entity)
252
+ }
253
+ )
254
+ end
255
+
256
+ domain_raw = mod.domain_model && @project.raw_unit(mod.domain_model.id)
257
+ next unless domain_raw
258
+
259
+ domain_doc = @project.parse_bson(domain_raw)
260
+ domain_entities(domain_doc).each do |entity_doc|
261
+ entity_id = Mxrb::IO::BsonCodec.extract_id(entity_doc["$ID"])
262
+ source = @by_id["entity:#{entity_id}"]
263
+ @documents << [source, entity_doc] if source
264
+ end
265
+ domain_associations(domain_doc).each do |association_doc|
266
+ association_id = Mxrb::IO::BsonCodec.extract_id(association_doc["$ID"])
267
+ source = @by_id["association:#{association_id}"]
268
+ @documents << [source, association_doc] if source
269
+ end
270
+ end
271
+ end
272
+
273
+ def index_units
274
+ units = @project.all_units
275
+ units_by_id = units.to_h { [_1["UnitID"], _1] }
276
+ module_by_id = @project.modules.to_h { [_1.id, _1.name] }
277
+
278
+ units.each do |raw|
279
+ doc = @project.parse_bson(raw)
280
+ type = doc["$Type"].to_s
281
+ next if type.empty? || %w[Projects$Module Projects$ModuleImpl].include?(type) ||
282
+ type.include?("DomainModel")
283
+
284
+ name = doc["Name"] || doc["name"]
285
+ name = "Navigation" if name.to_s.empty? && type == "Navigation$NavigationDocument"
286
+ next if name.to_s.empty?
287
+
288
+ module_name = ancestor_module(raw, units_by_id, module_by_id)
289
+ qualified = if type == "Navigation$NavigationDocument"
290
+ "Project.Navigation"
291
+ elsif module_name
292
+ "#{module_name}.#{name}"
293
+ else
294
+ name.to_s
295
+ end
296
+ artifact = add_artifact(
297
+ id: "unit:#{raw['UnitID']}", qualified_name: qualified,
298
+ kind: kind_for(type), module_name: module_name, name: name.to_s,
299
+ unit_id: raw["UnitID"], path: [], metadata: {
300
+ bson_type: type,
301
+ documentation: (doc["Documentation"] || doc["documentation"]).to_s,
302
+ allowed_roles: semantic_array(doc["AllowedModuleRoles"] || doc["AllowedRoles"]),
303
+ excluded: doc["Excluded"] == true,
304
+ mark_as_used: doc["MarkAsUsed"] == true,
305
+ exposed: exposed_document?(doc)
306
+ }.freeze
307
+ )
308
+ @documents << [artifact, doc]
309
+ end
310
+ end
311
+
312
+ def scan_documents
313
+ seen = Set.new
314
+ unresolved_seen = Set.new
315
+ @documents.each do |source, document|
316
+ walk(document) do |path, value|
317
+ next if metadata_path?(path)
318
+
319
+ candidates_for(value, source).each do |candidate|
320
+ target = resolve_candidate(candidate, source, path)
321
+ unless target
322
+ add_unresolved(
323
+ source, candidate, path, value, unresolved_seen
324
+ )
325
+ next
326
+ end
327
+ next if target.id == source.id && self_identity_path?(path)
328
+
329
+ relation = relation_for(path, target)
330
+ key = [source.id, target.id, relation, path]
331
+ next if seen.include?(key)
332
+
333
+ seen << key
334
+ @references << Reference.new(
335
+ source, target, relation, path.map(&:to_s).freeze, value
336
+ )
337
+ end
338
+ end
339
+ end
340
+ end
341
+
342
+ def add_unresolved(source, candidate, path, value, seen)
343
+ kinds = expected_kinds(path.last)
344
+ return if kinds.empty? || !semantic_name?(candidate)
345
+
346
+ key = [source.id, candidate, kinds, path]
347
+ return if seen.include?(key)
348
+
349
+ seen << key
350
+ @unresolved_references << UnresolvedReference.new(
351
+ source, candidate, kinds.freeze, path.map(&:to_s).freeze, value
352
+ )
353
+ end
354
+
355
+ def qualified_entity_name(mod, entity)
356
+ return unless entity
357
+ return entity.qualified_name unless entity.qualified_name.to_s.empty?
358
+
359
+ "#{mod.name}.#{entity.name}"
360
+ end
361
+
362
+ def semantic_name?(candidate)
363
+ candidate.match?(/\A[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){1,2}\z/)
364
+ end
365
+
366
+ def add_artifact(**attributes)
367
+ artifact = Artifact.new(**attributes)
368
+ existing = @by_id[artifact.id]
369
+ return existing if existing
370
+
371
+ @artifacts << artifact
372
+ @by_id[artifact.id] = artifact
373
+ @by_name[artifact.qualified_name] << artifact
374
+ artifact
375
+ end
376
+
377
+ def walk(value, path = [], &block)
378
+ case value
379
+ when Hash
380
+ value.each { |key, child| walk(child, path + [key], &block) }
381
+ when Array
382
+ value.each_with_index { |child, index| walk(child, path + [index], &block) }
383
+ when String
384
+ yield(path, value)
385
+ end
386
+ end
387
+
388
+ def candidates_for(value, source)
389
+ direct = [value]
390
+ direct << "#{source.module_name}.#{value}" if source.module_name && !value.include?(".")
391
+ direct.concat(value.scan(TOKEN))
392
+ direct.concat(value.scan(MEMBER_TOKEN).map { _1.tr("/", ".") })
393
+ direct.uniq
394
+ end
395
+
396
+ def resolve_candidate(candidate, source, path)
397
+ matches = @by_name[candidate]
398
+ return matches.first if matches.one?
399
+ return nil if matches.empty?
400
+
401
+ expected = expected_kinds(path.last)
402
+ narrowed = matches.select { expected.include?(_1.kind) }
403
+ return narrowed.first if narrowed.one?
404
+
405
+ local = matches.select { _1.module_name == source.module_name }
406
+ local.one? ? local.first : nil
407
+ end
408
+
409
+ def resolve_argument(value)
410
+ artifact = find(value)
411
+ return artifact if artifact
412
+
413
+ matches = find_all(value)
414
+ if matches.empty?
415
+ raise KeyError, "unknown Mendix artifact #{value.inspect}"
416
+ end
417
+
418
+ kinds = matches.map(&:kind).uniq.join(", ")
419
+ raise ArgumentError, "ambiguous Mendix artifact #{value.inspect} (#{kinds})"
420
+ end
421
+
422
+ def relation_for(path, target)
423
+ FIELD_RELATIONS.fetch(normalize_field(path.last)) do
424
+ CALL_KINDS.include?(target.kind) ? :references : :"uses_#{target.kind}"
425
+ end
426
+ end
427
+
428
+ def expected_kinds(field)
429
+ case normalize_field(field)
430
+ when "microflow" then [:microflow]
431
+ when "nanoflow" then [:nanoflow]
432
+ when "javaaction" then [:java_action]
433
+ when "javascriptaction" then [:javascript_action]
434
+ when "workflow" then [:workflow]
435
+ when "form", "page" then %i[page layout snippet]
436
+ when "layout" then [:layout]
437
+ when "snippet" then [:snippet]
438
+ when "entity" then [:entity]
439
+ when "attribute" then [:attribute]
440
+ when "association" then [:association]
441
+ when "enumeration" then [:enumeration]
442
+ else []
443
+ end
444
+ end
445
+
446
+ def normalize_field(field)
447
+ field.to_s.gsub(/[^A-Za-z]/, "").downcase
448
+ end
449
+
450
+ def metadata_path?(path)
451
+ %w[$Type $ID ContentsHash Documentation Caption Text].include?(path.last.to_s)
452
+ end
453
+
454
+ def self_identity_path?(path)
455
+ %w[$QualifiedName QualifiedName Name name].include?(path.last.to_s)
456
+ end
457
+
458
+ def ancestor_module(raw, units_by_id, module_by_id)
459
+ current = raw
460
+ visited = Set.new
461
+ loop do
462
+ container = current["ContainerID"]
463
+ return module_by_id[container] if module_by_id.key?(container)
464
+ return nil if container.nil? || visited.include?(container)
465
+
466
+ visited << container
467
+ current = units_by_id[container]
468
+ return nil unless current
469
+ end
470
+ end
471
+
472
+ def kind_for(type)
473
+ suffix = type.split("$", 2).last.to_s
474
+ {
475
+ "Microflow" => :microflow,
476
+ "Nanoflow" => :nanoflow,
477
+ "Page" => :page,
478
+ "Layout" => :layout,
479
+ "Snippet" => :snippet,
480
+ "Enumeration" => :enumeration,
481
+ "Constant" => :constant,
482
+ "ScheduledEvent" => :scheduled_event,
483
+ "MenuDocument" => :menu,
484
+ "JavaAction" => :java_action,
485
+ "JavaScriptAction" => :javascript_action,
486
+ "Workflow" => :workflow
487
+ }.fetch(suffix) do
488
+ suffix.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
489
+ end
490
+ end
491
+
492
+ def exposed_document?(doc)
493
+ doc.any? do |key, value|
494
+ key.to_s.match?(/\A(?:Expose|Exposed|Published)/) && value != false && !value.nil?
495
+ end
496
+ end
497
+
498
+ def semantic_array(value)
499
+ Mxrb::IO::BsonCodec.parse_array(value)[:items].map(&:to_s).freeze
500
+ end
501
+
502
+ def domain_entities(doc)
503
+ Mxrb::IO::BsonCodec.parse_array(doc["entities"] || doc["Entities"])[:items]
504
+ end
505
+
506
+ def domain_associations(doc)
507
+ local = doc["associations"] || doc["Associations"]
508
+ cross = doc["crossAssociations"] || doc["CrossAssociations"]
509
+ Mxrb::IO::BsonCodec.parse_array(local)[:items] +
510
+ Mxrb::IO::BsonCodec.parse_array(cross)[:items]
511
+ end
512
+
513
+ # ── Index cache ──────────────────────────────────────────────────────────
514
+
515
+ def reset_state
516
+ @artifacts = []
517
+ @references = []
518
+ @unresolved_references = []
519
+ @by_id = {}
520
+ @by_name = Hash.new { |hash, key| hash[key] = [] }
521
+ @documents = []
522
+ @vec_store = nil
523
+ @semantic_embedders = {}
524
+ end
525
+
526
+ def semantic_embedder(backend)
527
+ @semantic_embedders[backend] ||= Embedder.build(backend:)
528
+ end
529
+
530
+ def vec_store(embedder)
531
+ return nil if @project.mpr.readonly? || !@fingerprint || !VecStore.available?
532
+ return @vec_store if @vec_store&.backend == embedder.backend
533
+
534
+ @vec_store = VecStore.new(@project.mpr, embedder:, fingerprint: @fingerprint)
535
+ rescue SQLite3::Exception, LoadError
536
+ nil
537
+ end
538
+
539
+ def compute_fingerprint
540
+ self.class.fingerprint(@project)
541
+ end
542
+
543
+ def serialize_to_cache
544
+ JSON.generate(
545
+ version: CACHE_VERSION,
546
+ artifacts: @artifacts.map { |a|
547
+ { id: a.id, qualified_name: a.qualified_name, kind: a.kind.to_s,
548
+ module_name: a.module_name, name: a.name, unit_id: a.unit_id, path: a.path,
549
+ metadata: a.metadata.slice(*CACHE_METADATA_KEYS) }
550
+ },
551
+ references: @references.map { |r|
552
+ { source_id: r.source.id, target_id: r.target.id,
553
+ relation: r.relation.to_s, path: r.path, value: r.value }
554
+ },
555
+ unresolved: @unresolved_references.map { |u|
556
+ { source_id: u.source.id, qualified_name: u.qualified_name,
557
+ expected_kinds: u.expected_kinds.map(&:to_s), path: u.path, value: u.value }
558
+ }
559
+ )
560
+ end
561
+
562
+ def restore_from_cache(json)
563
+ data = JSON.parse(json, symbolize_names: true)
564
+ raise TypeError, "unsupported semantic index cache" unless data[:version] == CACHE_VERSION
565
+
566
+ data.fetch(:artifacts).each do |a|
567
+ artifact = Artifact.new(
568
+ id: a.fetch(:id), qualified_name: a.fetch(:qualified_name),
569
+ kind: a.fetch(:kind).to_sym,
570
+ module_name: a[:module_name], name: a[:name], unit_id: a[:unit_id],
571
+ path: Array(a[:path]), metadata: a.fetch(:metadata, {})
572
+ )
573
+ @artifacts << artifact
574
+ @by_id[artifact.id] = artifact
575
+ @by_name[artifact.qualified_name] << artifact
576
+ end
577
+ hydrate_cached_domain_metadata
578
+ data.fetch(:references).each do |r|
579
+ source = @by_id[r[:source_id]]
580
+ target = @by_id[r[:target_id]]
581
+ raise TypeError, "invalid cached reference" unless source && target
582
+
583
+ @references << Reference.new(source, target, r[:relation].to_sym,
584
+ Array(r[:path]), r[:value])
585
+ end
586
+ data.fetch(:unresolved).each do |u|
587
+ source = @by_id[u[:source_id]]
588
+ raise TypeError, "invalid cached unresolved reference" unless source
589
+
590
+ @unresolved_references << UnresolvedReference.new(
591
+ source, u[:qualified_name],
592
+ u[:expected_kinds].map(&:to_sym).freeze,
593
+ Array(u[:path]), u[:value]
594
+ )
595
+ end
596
+ @artifacts.freeze
597
+ @references.freeze
598
+ @unresolved_references.freeze
599
+ true
600
+ rescue JSON::ParserError, KeyError, TypeError, NoMethodError
601
+ false
602
+ end
603
+
604
+ def hydrate_cached_domain_metadata
605
+ @project.modules.each do |mod|
606
+ mod.entities.each do |entity|
607
+ entity_artifact = @by_id.fetch("entity:#{entity.id}")
608
+ entity_artifact.metadata[:model] = entity
609
+ entity.attributes.each do |attribute|
610
+ attribute_artifact = @by_id.fetch("attribute:#{attribute.id}")
611
+ attribute_artifact.metadata[:model] = attribute
612
+ attribute_artifact.metadata[:entity] = entity_artifact
613
+ end
614
+ end
615
+ mod.associations.each do |association|
616
+ artifact = @by_id.fetch("association:#{association.id}")
617
+ artifact.metadata[:model] = association
618
+ end
619
+ end
620
+ end
621
+ end
622
+ end
623
+ end