expressir 2.4.31-arm-linux → 2.4.36-arm-linux

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +34 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +1 -1
  5. data/TODO.eengine/00-overview.md +39 -0
  6. data/TODO.eengine/01-resolution-diagnostics.md +30 -0
  7. data/TODO.eengine/02-check-catalog-completion.md +53 -0
  8. data/TODO.eengine/03-entity-computations.md +22 -0
  9. data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
  10. data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
  11. data/TODO.eengine/06-population-p21.md +34 -0
  12. data/TODO.eengine/07-schema-compare-patch.md +25 -0
  13. data/TODO.eengine/08-official-corpus-gate.md +7 -0
  14. data/TODO.eengine/09-mim-mapping-parser.md +9 -0
  15. data/TODO.parity-ee/00-overview.md +1 -1
  16. data/TODO.sota/00-overview.md +57 -0
  17. data/TODO.sota/01-type-checking.md +115 -0
  18. data/TODO.sota/02-evaluator.md +86 -0
  19. data/TODO.sota/03-conformance.md +53 -0
  20. data/TODO.sota/04-lsp.md +70 -0
  21. data/TODO.sota/05-interop.md +55 -0
  22. data/TODO.sota/06-dialects.md +57 -0
  23. data/TODO.sota/07-reference-artifacts.md +55 -0
  24. data/TODO.suma-improvements/07-part28-xml.md +10 -13
  25. data/docs/_guides/interop/index.adoc +25 -0
  26. data/docs/_guides/interop/json-schema.adoc +88 -0
  27. data/docs/_guides/interop/owl-rdf.adoc +87 -0
  28. data/docs/_guides/reference/checker-notes.adoc +55 -0
  29. data/docs/_guides/reference/checker.adoc +36 -0
  30. data/docs/_guides/reference/evaluator.adoc +40 -0
  31. data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
  32. data/docs/_guides/reference/index.adoc +22 -0
  33. data/docs/_guides/reference/lsp.adoc +41 -0
  34. data/docs/_guides/reference/p21.adoc +30 -0
  35. data/docs/_guides/reference/shtolo.adoc +36 -0
  36. data/exe/expressir-lsp +6 -0
  37. data/expressir.gemspec +2 -2
  38. data/lib/expressir/3.2/expressir_core.so +0 -0
  39. data/lib/expressir/3.3/expressir_core.so +0 -0
  40. data/lib/expressir/3.4/expressir_core.so +0 -0
  41. data/lib/expressir/4.0/expressir_core.so +0 -0
  42. data/lib/expressir/cli.rb +25 -0
  43. data/lib/expressir/commands/check.rb +3 -1
  44. data/lib/expressir/commands/export_json_schema.rb +29 -0
  45. data/lib/expressir/commands/export_owl.rb +26 -0
  46. data/lib/expressir/commands/smrl_xml_index.rb +25 -0
  47. data/lib/expressir/commands/validate.rb +3 -0
  48. data/lib/expressir/commands.rb +3 -0
  49. data/lib/expressir/conformance/trend.rb +100 -0
  50. data/lib/expressir/conformance.rb +8 -0
  51. data/lib/expressir/eval/environment.rb +83 -0
  52. data/lib/expressir/eval/expression.rb +610 -0
  53. data/lib/expressir/eval/interpreter.rb +316 -0
  54. data/lib/expressir/eval/rules.rb +88 -0
  55. data/lib/expressir/eval/values.rb +74 -0
  56. data/lib/expressir/eval.rb +17 -0
  57. data/lib/expressir/express/builder.rb +17 -5
  58. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
  59. data/lib/expressir/express/builders/expression_builder.rb +9 -1
  60. data/lib/expressir/express/checker.rb +970 -5
  61. data/lib/expressir/express/dialect.rb +44 -0
  62. data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
  63. data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
  64. data/lib/expressir/express/parser.rb +9 -4
  65. data/lib/expressir/express/pretty_formatter.rb +9 -14
  66. data/lib/expressir/express/schema_compare.rb +194 -0
  67. data/lib/expressir/express/shtolo.rb +194 -63
  68. data/lib/expressir/express.rb +3 -0
  69. data/lib/expressir/interop/json_schema.rb +195 -0
  70. data/lib/expressir/interop/owl.rb +216 -0
  71. data/lib/expressir/interop.rb +13 -0
  72. data/lib/expressir/lsp/document.rb +226 -0
  73. data/lib/expressir/lsp/server.rb +184 -0
  74. data/lib/expressir/lsp/workspace.rb +402 -0
  75. data/lib/expressir/lsp.rb +11 -0
  76. data/lib/expressir/model/declarations/entity.rb +132 -0
  77. data/lib/expressir/model/declarations/schema.rb +37 -6
  78. data/lib/expressir/p21/builder.rb +92 -0
  79. data/lib/expressir/p21/grammar.rb +107 -0
  80. data/lib/expressir/p21/parser.rb +16 -0
  81. data/lib/expressir/p21/population.rb +35 -0
  82. data/lib/expressir/p21/validator.rb +213 -0
  83. data/lib/expressir/p21.rb +17 -0
  84. data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
  85. data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
  86. data/lib/expressir/reference/note_catalog.rb +54 -0
  87. data/lib/expressir/reference.rb +10 -0
  88. data/lib/expressir/version.rb +1 -1
  89. data/lib/expressir.rb +5 -0
  90. data/lib/tasks/conformance.rake +27 -0
  91. data/lib/tasks/reference.rake +25 -0
  92. metadata +68 -4
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Lsp
5
+ # The cross-document view used by the server: open documents, schema
6
+ # resolution through USE/REFERENCE interfaces, diagnostics, navigation
7
+ # and rename computation.
8
+ class Workspace
9
+ SEVERITIES = { error: 1, warning: 2, info: 3, hint: 4 }.freeze
10
+
11
+ attr_reader :documents
12
+
13
+ def initialize
14
+ @documents = {}
15
+ end
16
+
17
+ def open(uri:, text:, path: nil)
18
+ @documents[uri] = Document.new(uri: uri, text: text, path: path)
19
+ end
20
+
21
+ def change(uri:, text:)
22
+ document = @documents[uri]
23
+ return document.update(text) if document
24
+
25
+ self.open(uri: uri, text: text)
26
+ end
27
+
28
+ def close(uri)
29
+ @documents.delete(uri)
30
+ end
31
+
32
+ def document(uri)
33
+ @documents[uri]
34
+ end
35
+
36
+ def symbols(uri)
37
+ document(uri)&.symbols || []
38
+ end
39
+
40
+ def diagnostics(uri)
41
+ document = @documents[uri]
42
+ return [] unless document
43
+
44
+ return [parse_diagnostic(document)] if document.parse_error
45
+
46
+ checker_notes.filter_map { |note| diagnostic_for(document, note) }
47
+ end
48
+
49
+ def definition(uri, position)
50
+ target = target_at(uri, position)
51
+ return nil unless target
52
+
53
+ location(target[:doc], target[:node])
54
+ end
55
+
56
+ def references(uri, position, include_declaration: true)
57
+ target = target_at(uri, position)
58
+ return [] unless target
59
+
60
+ declaration = target[:node]
61
+ results = reference_locations(declaration)
62
+ results << location(target[:doc], declaration) if include_declaration
63
+ dedupe_locations(results)
64
+ end
65
+
66
+ def hover(uri, position)
67
+ target = target_at(uri, position)
68
+ return nil unless target
69
+
70
+ value = Express::PrettyFormatter.new.format(target[:node])
71
+ {
72
+ "contents" => {
73
+ "kind" => "markdown",
74
+ "value" => "```express\n#{value}\n```",
75
+ },
76
+ }
77
+ rescue StandardError
78
+ nil
79
+ end
80
+
81
+ def prepare_rename(uri, position)
82
+ target = target_at(uri, position)
83
+ return nil unless target
84
+
85
+ document = target[:doc]
86
+ node = target[:node]
87
+ { "range" => document.range_for(node),
88
+ "placeholder" => node.id.to_s }
89
+ end
90
+
91
+ # Returns the URI→edits map for a rename, after applying the edits
92
+ # in memory and verifying that every affected document still parses.
93
+ def rename(uri, position, new_name)
94
+ target = target_at(uri, position)
95
+ return nil unless target
96
+
97
+ edits = rename_edits(target[:doc], target[:node], new_name)
98
+ verify_rename!(edits, target[:node], new_name)
99
+ { "changes" => edits }
100
+ end
101
+
102
+ private
103
+
104
+ def checker_notes
105
+ key = @documents.each_value.map { |doc| doc.text.hash }
106
+ return @checker_notes if key == @checker_notes_key
107
+
108
+ files = @documents.each_value.filter_map(&:file)
109
+ notes = if files.empty?
110
+ []
111
+ else
112
+ repository = Model::Repository.new(files: files)
113
+ Express::Checker.new(repository).check.notes
114
+ end
115
+ @checker_notes = notes
116
+ @checker_notes_key = key
117
+ notes
118
+ rescue StandardError
119
+ []
120
+ end
121
+
122
+ def diagnostic_for(document, note)
123
+ schema = document.schemas.find do |candidate|
124
+ candidate.id.to_s.casecmp?(note.schema.to_s)
125
+ end
126
+ return nil unless schema
127
+ return nil if resolved_note?(document, note)
128
+
129
+ node = note.node
130
+ range = if node&.source_offset
131
+ document.range(node.source_offset,
132
+ node.respond_to?(:id) && node.id ? node.id.to_s.bytesize : 1)
133
+ else
134
+ document.range_for(schema)
135
+ end
136
+ {
137
+ "range" => range,
138
+ "severity" => SEVERITIES.fetch(note.severity, 3),
139
+ "code" => note.id,
140
+ "source" => "expressir",
141
+ "message" => note.message.to_s,
142
+ }
143
+ end
144
+
145
+ # The checker sees one file at a time; an open document that supplies
146
+ # the referenced schema or interfaced declaration is not unresolved.
147
+ def resolved_note?(document, note)
148
+ return false unless note.id == :check_unresolved_ref
149
+ return false unless note.node.is_a?(Model::References::SimpleReference)
150
+
151
+ !resolve_reference(document, note.node).nil? ||
152
+ !schema_by_id(note.node.id).nil?
153
+ rescue StandardError
154
+ false
155
+ end
156
+
157
+ def parse_diagnostic(document)
158
+ error = document.parse_error
159
+ message = error.message.to_s
160
+ line = message[/line (\d+)/, 1].to_i
161
+ character = message[/char (\d+)/, 1].to_i
162
+ line = 1 if line.zero?
163
+ character = 1 if character.zero?
164
+ start = { "line" => line - 1, "character" => character - 1 }
165
+ {
166
+ "range" => { "start" => start,
167
+ "end" => start.merge("character" => start["character"] + 1) },
168
+ "severity" => 1,
169
+ "source" => "expressir",
170
+ "message" => message.lines.first.to_s.strip,
171
+ }
172
+ end
173
+
174
+ def target_at(uri, position)
175
+ document = @documents[uri]
176
+ return nil unless document&.file
177
+
178
+ token = document.token_at(position)
179
+ return nil unless token
180
+
181
+ declaration = document.declaration_nodes.find do |node|
182
+ document.name_offset(node) == token.start_byte &&
183
+ node.id.to_s.casecmp?(token.text)
184
+ end
185
+ return { doc: document, node: declaration, token: token } if declaration
186
+
187
+ reference = document.reference_nodes.find do |node|
188
+ node.source_offset == token.start_byte
189
+ end
190
+ return nil unless reference
191
+
192
+ resolved = resolve_reference(document, reference)
193
+ resolved && { doc: resolved[:doc], node: resolved[:node], token: token }
194
+ end
195
+
196
+ def resolve_reference(document, node)
197
+ schema = schema_of(node)
198
+ return nil unless schema
199
+
200
+ local = node.find(node.id)
201
+ if local
202
+ target_doc = document_for_node(local) || document
203
+ return { doc: target_doc, node: local }
204
+ end
205
+
206
+ resolve_interface(schema, node.id)
207
+ rescue StandardError
208
+ nil
209
+ end
210
+
211
+ def resolve_interface(schema, name)
212
+ Array(schema.interfaces).each do |interface|
213
+ interface_schema = schema_by_id(interface.schema&.id)
214
+ next unless interface_schema
215
+
216
+ items = Array(interface.items)
217
+ if items.empty?
218
+ node = interface_schema[:schema].find(name)
219
+ return { doc: interface_schema[:doc], node: node } if node
220
+ else
221
+ items.each do |item|
222
+ local_name = item.id || item.ref&.id
223
+ next unless local_name.to_s.casecmp?(name.to_s)
224
+
225
+ node = interface_schema[:schema].find(item.ref&.id)
226
+ return { doc: interface_schema[:doc], node: node } if node
227
+ end
228
+ end
229
+ end
230
+ nil
231
+ end
232
+
233
+ def schema_by_id(id)
234
+ return nil unless id
235
+
236
+ @documents.each_value do |document|
237
+ document.schemas.each do |schema|
238
+ next unless schema.id.to_s.casecmp?(id.to_s)
239
+
240
+ return { doc: document, schema: schema }
241
+ end
242
+ end
243
+ nil
244
+ end
245
+
246
+ def document_for_node(node)
247
+ schema = schema_of(node)
248
+ return nil unless schema
249
+
250
+ @documents.each_value do |document|
251
+ return document if document.schemas.any? { |candidate| candidate.equal?(schema) }
252
+ end
253
+ nil
254
+ end
255
+
256
+ def schema_of(node)
257
+ current = node
258
+ current = current.parent while current && !current.is_a?(Model::Declarations::Schema)
259
+ current
260
+ end
261
+
262
+ def location(document, node)
263
+ { "uri" => document.uri, "range" => document.range_for(node) }
264
+ end
265
+
266
+ def reference_locations(declaration)
267
+ declaring_schema = schema_of(declaration)
268
+ @documents.each_value.flat_map do |document|
269
+ document.reference_nodes.filter_map do |reference|
270
+ resolved = resolve_reference(document, reference)
271
+ next unless resolved && same_declaration?(resolved[:node], declaration)
272
+
273
+ location(document, reference)
274
+ end + interface_locations(document, declaring_schema, declaration)
275
+ end
276
+ end
277
+
278
+ def interface_locations(document, declaring_schema, declaration)
279
+ document.schemas.flat_map do |schema|
280
+ Array(schema.interfaces).flat_map do |interface|
281
+ next [] unless interface.schema&.id.to_s.casecmp?(declaring_schema.id.to_s)
282
+
283
+ Array(interface.items).filter_map do |item|
284
+ next unless item.ref&.id.to_s.casecmp?(declaration.id.to_s)
285
+
286
+ location(document, item.ref)
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ def same_declaration?(left, right)
293
+ left.path.to_s.casecmp?(right.path.to_s)
294
+ end
295
+
296
+ def rename_edits(declaring_document, declaration, new_name)
297
+ edits = {}
298
+ add_edit(edits, declaring_document, declaration, new_name)
299
+
300
+ @documents.each_value do |document|
301
+ document.schemas.each do |schema|
302
+ rename_references(document, schema, declaration, new_name, edits)
303
+ rename_interface_items(document, schema, declaration, new_name, edits)
304
+ end
305
+ end
306
+ edits
307
+ end
308
+
309
+ def rename_references(document, schema, declaration, new_name, edits)
310
+ document.reference_nodes.each do |reference|
311
+ next unless schema_of(reference).equal?(schema)
312
+ next unless reference.id.to_s.casecmp?(declaration.id.to_s)
313
+
314
+ resolved = resolve_reference(document, reference)
315
+ next unless resolved && same_declaration?(resolved[:node], declaration)
316
+
317
+ add_edit(edits, document, reference, new_name)
318
+ end
319
+ end
320
+
321
+ def rename_interface_items(document, schema, declaration, new_name, edits)
322
+ Array(schema.interfaces).each do |interface|
323
+ next unless interface.schema&.id.to_s.casecmp?(schema_of(declaration).id.to_s)
324
+
325
+ Array(interface.items).each do |item|
326
+ next unless item.ref&.id.to_s.casecmp?(declaration.id.to_s)
327
+
328
+ add_edit(edits, document, item.ref, new_name)
329
+ end
330
+ end
331
+ end
332
+
333
+ def add_edit(edits, document, node, new_name)
334
+ range = document.range_for(node)
335
+ list = (edits[document.uri] ||= [])
336
+ return if list.any? { |edit| edit["range"] == range }
337
+
338
+ list << { "range" => range, "newText" => new_name }
339
+ end
340
+
341
+ def verify_rename!(edits, declaration, new_name)
342
+ declaring_schema = schema_of(declaration)
343
+ edits.each do |uri, document_edits|
344
+ document = @documents[uri]
345
+ text = apply_edits(document.text, document_edits)
346
+ probe = Document.new(uri: uri, text: text, path: document.path)
347
+ raise "rename produced invalid EXPRESS in #{uri}" if probe.parse_error
348
+
349
+ verify_schema_after_rename(document, probe, declaring_schema,
350
+ declaration, new_name, uri)
351
+ end
352
+ end
353
+
354
+ # The renamed declaration may disappear under its old path; every
355
+ # other removal is a structure loss and aborts the rename.
356
+ def verify_schema_after_rename(document, probe, declaring_schema,
357
+ declaration, _new_name, uri)
358
+ index = document.schemas.index(declaring_schema)
359
+ return unless index
360
+
361
+ probe_schema = probe.schemas[index]
362
+ return unless probe_schema
363
+
364
+ old_path = "#{declaring_schema.id}.#{declaration.id}".downcase
365
+ unexpected = Express::SchemaCompare
366
+ .compare(declaring_schema, probe_schema)
367
+ .select do |delta|
368
+ delta.op == :removed && delta.path.to_s.downcase != old_path
369
+ end
370
+ raise "rename removed unrelated declarations in #{uri}" if unexpected.any?
371
+
372
+ formatted = Express::Formatter.format(probe_schema)
373
+ formatted_probe = Document.new(uri: uri, text: formatted,
374
+ path: document.path)
375
+ raise "renamed schema does not format in #{uri}" if formatted_probe.parse_error
376
+ end
377
+
378
+ def apply_edits(text, edits)
379
+ scratch = Document.new(uri: "file:///nonexistent", text: text)
380
+ source = text.b
381
+ offsets = edits.map do |edit|
382
+ start_byte = scratch.byte_offset(edit["range"]["start"]["line"],
383
+ edit["range"]["start"]["character"])
384
+ end_byte = scratch.byte_offset(edit["range"]["end"]["line"],
385
+ edit["range"]["end"]["character"])
386
+ [start_byte, end_byte, edit["newText"].b]
387
+ end
388
+ offsets.sort_by { |start_byte, _, _| -start_byte }.each do |start_byte, end_byte, replacement|
389
+ source[start_byte...end_byte] = replacement
390
+ end
391
+ source.force_encoding(Encoding::UTF_8)
392
+ end
393
+
394
+ def dedupe_locations(locations)
395
+ locations.uniq do |location|
396
+ [location["uri"], location["range"]["start"]["line"],
397
+ location["range"]["start"]["character"]]
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ # Language Server Protocol support: stdio JSON-RPC transport, open
5
+ # documents, and the workspace view that resolves across schemas.
6
+ module Lsp
7
+ autoload :Document, "#{__dir__}/lsp/document"
8
+ autoload :Workspace, "#{__dir__}/lsp/workspace"
9
+ autoload :Server, "#{__dir__}/lsp/server"
10
+ end
11
+ end
@@ -56,6 +56,138 @@ module Expressir
56
56
  *remark_items,
57
57
  ]
58
58
  end
59
+
60
+ # eeng compute-inheritance: the full supertype closure, nearest
61
+ # supertype first, cycle-safe. Cross-schema supertypes resolve
62
+ # through the enclosing repository.
63
+ # @return [Array<Entity>]
64
+ def supertype_closure
65
+ closure = []
66
+ visited = { id&.safe_downcase => true }
67
+ queue = Array(subtype_of).map { |ref| ref.respond_to?(:id) ? ref.id : ref }
68
+ while (name = queue.shift)
69
+ key = name&.safe_downcase
70
+ next if key.nil? || visited.key?(key)
71
+
72
+ supertype = find_supertype_entity(key)
73
+ next unless supertype
74
+
75
+ visited[key] = true
76
+ closure << supertype
77
+ queue.concat(Array(supertype.subtype_of).map do |ref|
78
+ ref.respond_to?(:id) ? ref.id : ref
79
+ end)
80
+ end
81
+ closure
82
+ end
83
+
84
+ # Explicit attributes inherited from the supertype closure,
85
+ # nearest supertype first, skipping ids this entity (or a
86
+ # nearer ancestor) redeclares.
87
+ # @return [Array<ExplicitAttribute>]
88
+ def inherited_explicit_attributes
89
+ claimed = explicit_only(Array(attributes)).filter_map(&:id).map(&:safe_downcase)
90
+ closure = supertype_closure
91
+ closure.flat_map do |supertype|
92
+ explicit_only(Array(supertype.attributes)).filter_map do |attr|
93
+ next nil unless attr.id
94
+
95
+ key = attr.id.safe_downcase
96
+ next nil if claimed.include?(key)
97
+
98
+ claimed << key
99
+ attr
100
+ end
101
+ end
102
+ end
103
+
104
+ # Derived attributes accumulated from the supertype closure.
105
+ # @return [Array<DerivedAttribute>]
106
+ def inherited_derived_attributes
107
+ closure = supertype_closure
108
+ seen = {}
109
+ closure.flat_map do |supertype|
110
+ derived_attribute_items(supertype).filter_map do |attr|
111
+ next nil unless attr.id
112
+
113
+ key = attr.id.safe_downcase
114
+ next nil if seen.key?(key)
115
+
116
+ seen[key] = true
117
+ attr
118
+ end
119
+ end
120
+ end
121
+
122
+ # Inverse attributes accumulated from the supertype closure.
123
+ # @return [Array<InverseAttribute>]
124
+ def inherited_inverse_attributes
125
+ closure = supertype_closure
126
+ seen = {}
127
+ closure.flat_map do |supertype|
128
+ inverse_attribute_items(supertype).filter_map do |attr|
129
+ next nil unless attr.id
130
+
131
+ key = attr.id.safe_downcase
132
+ next nil if seen.key?(key)
133
+
134
+ seen[key] = true
135
+ attr
136
+ end
137
+ end
138
+ end
139
+
140
+ # All explicit attributes: own + inherited (eeng
141
+ # explicit-attrs materialization).
142
+ # @return [Array<ExplicitAttribute>]
143
+ def all_explicit_attributes
144
+ [*explicit_only(Array(attributes)), *inherited_explicit_attributes]
145
+ end
146
+
147
+ private
148
+
149
+ # Entity#attributes carries every attribute kind; the explicit
150
+ # kind is Expressir::Model::Declarations::Attribute.
151
+ # DerivedAttribute/InverseAttribute subclass Attribute, so the
152
+ # explicit kind is the exact base class.
153
+ def explicit_only(attrs)
154
+ attrs.select { |a| a.instance_of?(Expressir::Model::Declarations::Attribute) }
155
+ end
156
+
157
+ # The Ruby parse path parks every attribute kind in `attributes`
158
+ # (typed by class); the core path fills derived_attributes/
159
+ # inverse_attributes instead. Both layouts resolve to the same
160
+ # items here.
161
+ def derived_attribute_items(entity)
162
+ own = Array(entity.derived_attributes)
163
+ return own if own.any?
164
+
165
+ Array(entity.attributes).grep(Expressir::Model::Declarations::DerivedAttribute)
166
+ end
167
+
168
+ def inverse_attribute_items(entity)
169
+ own = Array(entity.inverse_attributes)
170
+ return own if own.any?
171
+
172
+ Array(entity.attributes).grep(Expressir::Model::Declarations::InverseAttribute)
173
+ end
174
+
175
+ def find_supertype_entity(key)
176
+ container = parent
177
+ container = container.parent while container &&
178
+ !container.is_a?(Model::Repository) &&
179
+ !container.is_a?(Model::ExpFile)
180
+ return nil unless container.respond_to?(:schemas)
181
+
182
+ container.schemas.each do |foreign|
183
+ next unless foreign.respond_to?(:entities)
184
+
185
+ foreign.entities.each do |candidate|
186
+ return candidate if candidate.id&.safe_downcase == key
187
+ end
188
+ end
189
+ nil
190
+ end
59
191
  end
60
192
  end
61
193
  end
@@ -127,6 +127,19 @@ module Expressir
127
127
  @formatted_hyperlinked_adoc ||= Expressir::Express::AdocSourceFormatter.format(self)
128
128
  end
129
129
 
130
+ USE_ITEM_KINDS = [
131
+ Expressir::Model::Declarations::Entity,
132
+ Expressir::Model::Declarations::Type,
133
+ ].freeze
134
+
135
+ REFERENCE_ITEM_KINDS = [
136
+ Expressir::Model::Declarations::Constant,
137
+ Expressir::Model::Declarations::Entity,
138
+ Expressir::Model::Declarations::Function,
139
+ Expressir::Model::Declarations::Procedure,
140
+ Expressir::Model::Declarations::Type,
141
+ ].freeze
142
+
130
143
  private
131
144
 
132
145
  # @param [String] id
@@ -142,6 +155,7 @@ module Expressir
142
155
  end
143
156
 
144
157
  # @return [Array<InterfacedItem>]
158
+
145
159
  def interfaced_items
146
160
  return [] unless parent
147
161
 
@@ -151,6 +165,11 @@ module Expressir
151
165
  end.compact
152
166
  end
153
167
 
168
+ # ISO 10303-11 implicit interfacing: USE exposes entities and
169
+ # types only; REFERENCE has no implicit interfacing. Listed
170
+ # items follow eeng resolve-interface: use-from resolves via
171
+ # find-type/entity, reference-from via the 5-kind or-chain.
172
+
154
173
  # Direct items of one interface plus the items visible through
155
174
  # the foreign schema's own USE interfaces (transitive USE
156
175
  # visibility, eeng :use-only → :use-from recursion).
@@ -171,16 +190,23 @@ module Expressir
171
190
  end
172
191
 
173
192
  own = if interface.items.empty?
174
- safe_children.map do |base_item|
175
- create_interfaced_item(base_item.id, base_item)
193
+ if interface.kind == Interface::REFERENCE
194
+ []
195
+ else
196
+ safe_children.filter_map do |base_item|
197
+ next nil unless USE_ITEM_KINDS.any? { |k| base_item.is_a?(k) }
198
+
199
+ create_interfaced_item(base_item.id, base_item)
200
+ end
176
201
  end
177
202
  else
178
203
  interface.items.filter_map do |item|
179
204
  base_item = children_by_id[item.ref.id.safe_downcase]
180
- if base_item
181
- create_interfaced_item(item.id || base_item.id,
182
- base_item)
183
- end
205
+ next nil unless base_item
206
+ next nil unless item_kind_interfacable?(interface.kind, base_item)
207
+
208
+ create_interfaced_item(item.id || base_item.id,
209
+ base_item)
184
210
  end
185
211
  end
186
212
 
@@ -197,6 +223,11 @@ module Expressir
197
223
  end.flatten
198
224
  end
199
225
 
226
+ def item_kind_interfacable?(kind, node)
227
+ kinds = kind == Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS
228
+ kinds.any? { |k| node.is_a?(k) }
229
+ end
230
+
200
231
  # Find a foreign schema by name: same file first, then the
201
232
  # enclosing repository (expressir's compiled/batch repository).
202
233
  def foreign_schema(name)