expressir 2.4.15 → 2.4.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 714b13d808744a329002460e108fdb35f4a8fcf0d3095517cd241d411bce215e
4
- data.tar.gz: 8cf66567212cc463ab351cd6e0cbb70ee63ec11ae2c0b31036cd3d0aff264384
3
+ metadata.gz: 6fb8953f2f2e9d76be3693148c8951ae36f562bbaf8214a8bc510b68888700d2
4
+ data.tar.gz: dfbcc336cc1a580501ca69632d6e6bb348e196be470cbc712a637131ffe1993c
5
5
  SHA512:
6
- metadata.gz: 1c13e1c4cdb0a551e86e2d557c6df01fc2a1b36e1ff536cd4afc6a2255c922d0bcb8252debdc600479514963b38b8482d41de118fb55933472ed173cca77e2aa
7
- data.tar.gz: 3f4107ac13feea2a5fdc9d5659312e44af1c7288d957132a119fb1799df396f709ed5c9d82c970669ceda5e64238506abe412a4850c310b4e173f347c67cc31d
6
+ metadata.gz: 2e5259fb143ac5419ab85b8a97e8ab50a3a809826f799b8cd74242a673599c5cac0bd21cfe1dc33af562c16a806afc95f9f43f75f8eba43e18b68377104d5406
7
+ data.tar.gz: 0d2520a7a817f1c8d72a6f768887e803c91a6070f3cf8d4230bde9e66dcb6a1580ff7ee39ac6e9c886106cde64db14e175db57e81be0b798ea8f45af57bfaaa7
@@ -0,0 +1,47 @@
1
+ # 18 — EXPRESS MIM mapping parser (expressir#88)
2
+
3
+ ## Goal
4
+
5
+ Parse and validate MIM "reference path" mappings — the application ↔
6
+ interpretation mapping every STEP module carries — into a typed model,
7
+ with the mapping paths validated against the ARM/MIM schemas.
8
+
9
+ ## Sources
10
+
11
+ - This checkout's canonical form: `schemas/modules/*/mapping.yaml` —
12
+ structured ae/aimelt/aa records whose values use the
13
+ `<<express:SCHEMA.ITEM,ITEM>>` link markup (the same markup
14
+ `Suma::LinkValidation` validates today). Generated upstream from
15
+ module.xml.
16
+ - Upstream SMRL module.xml: `mapping`/`ae`/`aimelt` elements with
17
+ `reference_path` bodies — the ISO 10303-1x mapping methodology
18
+ grammar: entity chains `A -> B -> C`, qualifiers `[attr => val]`,
19
+ `=>` derived references, `->>` subtype traversal, `<>` select
20
+ member. Port the grammar from the Part 1x mapping annex text
21
+ (detached docs) + existing module corpus as the oracle.
22
+ - eeng has no counterpart — this is an expressir-first capability.
23
+
24
+ ## Design
25
+
26
+ 1. `Expressir::Model::Mapping` — MappingDocument →
27
+ ApplicationElements (entity, aimelt, attributes) and
28
+ ReferencePath nodes (path-expression AST: steps, qualifiers,
29
+ subtype traversals).
30
+ 2. `mapping.yaml` reader (lutaml-model typed) — yaml → MappingDocument.
31
+ 3. Reference-path expression parser — parsanol grammar over the path
32
+ syntax; AST reuses Model::References where shapes coincide.
33
+ 4. Validation: every path resolves against the resolved ARM+MIM
34
+ repository (rides SchemaIndex / ItemGraph); unknown step = error
35
+ note (Checker id `check-mapping-path`).
36
+ 5. CLI: `expressir mapping validate DIR|FILE` over module dirs.
37
+
38
+ ## Acceptance
39
+
40
+ - All module mapping.yaml files in the SMRL parse; path validation
41
+ reports zero unknown steps (or a classified, explained residue).
42
+ - Round-trip: MappingDocument → yaml byte-stable.
43
+
44
+ ## Test cases
45
+
46
+ - module corpus (603 modules); hand fixtures for each path grammar
47
+ production; invalid-path fixtures.
@@ -61,15 +61,18 @@ module Expressir
61
61
  end
62
62
  end
63
63
 
64
- # Schema-name → path index from the ELF schema manifest.
64
+ # Schema-name → path index from the ELF schema manifest. An
65
+ # explicitly given manifest that cannot be loaded is fatal (#455):
66
+ # when the root has no interface dependencies the lookup would
67
+ # never consult the index, so a typo'd --manifest would exit 0.
65
68
  def manifest_index(manifest_path)
66
69
  manifest = Expressir::SchemaManifest.from_file(manifest_path)
67
70
  manifest.schemas.to_h do |entry|
68
71
  [entry.id.downcase, entry.path]
69
72
  end
70
73
  rescue StandardError => e
71
- warn "expressir: could not load schema manifest #{manifest_path}: #{e.message}"
72
- nil
74
+ raise Thor::Error,
75
+ "expressir: could not load schema manifest #{manifest_path}: #{e.message}"
73
76
  end
74
77
 
75
78
  # Schema-name → path index over a STEPmod checkout root (the
@@ -94,10 +97,16 @@ module Expressir
94
97
 
95
98
  # Default: the file's own directory plus, when the root sits
96
99
  # inside a STEPmod checkout, its enclosing `schemas/` tree —
97
- # the layout wg12-step modules live in.
100
+ # the layout wg12-step modules live in. Same-directory
101
+ # declarations win over tree hits for the same schema name:
102
+ # the file next to the root is the intentional one (#455).
98
103
  index = same_dir_index(root_path)
99
104
  ancestor = ancestor_schemas_dir(root_path)
100
- index.update(stepmod_index(File.dirname(ancestor))) if ancestor
105
+ if ancestor
106
+ tree = stepmod_index(File.dirname(ancestor))
107
+ tree.update(index)
108
+ index = tree
109
+ end
101
110
  index
102
111
  end
103
112
 
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+
5
+ module Expressir
6
+ module Express
7
+ # Lazy read path over a compiled-set artifact (M2 slice 2,
8
+ # TODO.parity-ee/19): schemas hydrate individually, on first touch,
9
+ # instead of the whole repository at once. Metanorma schema-doc
10
+ # rendering touches one schema per page — the lazy repository makes
11
+ # that pay: one hydration per page, not one per schema in the set.
12
+ #
13
+ # Duck-types the read surface of Model::Repository used by
14
+ # rendering (schemas, files, each, size, children). Anything beyond
15
+ # the read surface should wait for eager hydration via
16
+ # #to_eager (returns a real Model::Repository).
17
+ class LazyRepository
18
+ attr_reader :set_path
19
+
20
+ # @param set_path [String] compiled-set artifact path
21
+ # @param read_paths [Hash{String => String}] wire path => source path
22
+ def initialize(set_path, read_paths = {})
23
+ @set_path = set_path
24
+ @set = Core.lazy_set(set_path)
25
+ @read_paths = read_paths
26
+ @wire_paths = @set.wire_paths
27
+ @mutex = Mutex.new
28
+ @file_cache = {}
29
+ @hydration_count = 0
30
+ end
31
+
32
+ # Wire paths in artifact order — no hydration.
33
+ # @return [Array<String>]
34
+ def wire_paths
35
+ @wire_paths.dup
36
+ end
37
+
38
+ # Files in artifact order, each wrapped so it hydrates on first
39
+ # use. The file is the hydration grain: an artifact stores wire
40
+ # JSON per source file.
41
+ # @return [Array<LazyFile>]
42
+ def files
43
+ @wire_paths.map { |wire_path| file_by_wire_path(wire_path) }
44
+ end
45
+
46
+ # Hydrated (or hydrating) file for one wire path.
47
+ def file_by_wire_path(wire_path)
48
+ @mutex.synchronize do
49
+ @file_cache[wire_path] ||= LazyFile.new(wire_path) do
50
+ hydrate(wire_path)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Schemas across all files. EAGER by definition: it hydrates
56
+ # every file. Lazy callers iterate #files, or render through
57
+ # #to_liquid, which keeps per-file hydration lazy.
58
+ # @return [Array<Declarations::Schema>]
59
+ def schemas
60
+ files.map(&:__target__).flat_map(&:schemas).compact
61
+ end
62
+
63
+ # Liquid binding for metanorma (M2): returns a Drop whose
64
+ # `schemas` is a lazy per-file list — each entry hydrates only
65
+ # its own file when the template touches it. Schema count per
66
+ # file is unknown until hydration, so the Drop presents one
67
+ # entry per FILE; single-schema files (the corpus norm) render
68
+ # identically to the eager path.
69
+ def to_liquid
70
+ LazyRepositoryDrop.new(self)
71
+ end
72
+
73
+ # How many files have actually been hydrated (observability for
74
+ # the laziness contract).
75
+ def hydration_count
76
+ @hydration_count
77
+ end
78
+
79
+ # Hydrate everything and return a real Model::Repository — the
80
+ # escape hatch for callers needing the full read surface.
81
+ def to_eager
82
+ repo = Model::Repository.new
83
+ repo.files = files.map(&:__target__)
84
+ repo
85
+ end
86
+
87
+ def each(&)
88
+ files.each(&)
89
+ end
90
+
91
+ def size
92
+ @wire_paths.size
93
+ end
94
+
95
+ def children
96
+ schemas
97
+ end
98
+
99
+ # The read path on disk for a wire path (may be nil when the
100
+ # caller did not supply source paths).
101
+ def read_path_for(wire_path)
102
+ @read_paths[wire_path]
103
+ end
104
+
105
+ def matches_sources?
106
+ @set.matches_sources(@read_paths.dup) == true
107
+ end
108
+
109
+ private
110
+
111
+ def hydrate(wire_path)
112
+ @mutex.synchronize do
113
+ _wire, model = @set.hydrate_one(wire_path)
114
+ @hydration_count += 1
115
+ model
116
+ end
117
+ end
118
+ end
119
+
120
+ # Liquid::Drop over a LazyRepository: `schemas` enumerates lazy
121
+ # per-file entries; touching an entry hydrates that file and
122
+ # presents its schemas (single-schema files present the schema
123
+ # itself, matching the eager Drop's element contract).
124
+ class LazyRepositoryDrop < ::Liquid::Drop
125
+ def initialize(repo)
126
+ @repo = repo
127
+ super()
128
+ end
129
+
130
+ def schemas
131
+ @repo.files.map do |file|
132
+ LazyFileDrop.new(file)
133
+ end
134
+ end
135
+
136
+ def size
137
+ @repo.size
138
+ end
139
+ end
140
+
141
+ # Per-file lazy drop: hydrates on first access, then presents the
142
+ # file's single schema directly (or the schema list for
143
+ # multi-schema files).
144
+ class LazyFileDrop < ::Liquid::Drop
145
+ def initialize(file)
146
+ @file = file
147
+ super()
148
+ end
149
+
150
+ def __file__
151
+ @file.__target__
152
+ end
153
+
154
+ def method_missing(name, *, &)
155
+ schemas = __file__.schemas
156
+ if schemas.size == 1
157
+ schemas.first.public_send(name, *, &)
158
+ else
159
+ __file__.public_send(name, *, &)
160
+ end
161
+ end
162
+
163
+ def respond_to_missing?(name, include_private = false)
164
+ __file__.respond_to?(name, include_private) ||
165
+ __file__.schemas.first.respond_to?(name, include_private)
166
+ end
167
+ end
168
+
169
+ # Stands in for a Model::ExpFile until first method access, then
170
+ # hydrates through the block and forwards everything.
171
+ class LazyFile
172
+ def initialize(wire_path, &hydrator)
173
+ @wire_path = wire_path
174
+ @hydrator = hydrator
175
+ @mutex = Mutex.new
176
+ @target = nil
177
+ end
178
+
179
+ attr_reader :wire_path
180
+
181
+ # The real Schema, hydrating on first call.
182
+ def __target__
183
+ @mutex.synchronize do
184
+ @__target__ ||= @hydrator.call
185
+ end
186
+ end
187
+
188
+ def method_missing(name, *, &)
189
+ __target__.public_send(name, *, &)
190
+ end
191
+
192
+ def respond_to_missing?(name, include_private = false)
193
+ __target__.respond_to?(name, include_private)
194
+ end
195
+
196
+ # Identity through the wrapper: rendering compares and groups by
197
+ # class/is_a?, which the real file answers.
198
+ def is_a?(klass)
199
+ super || __target__.is_a?(klass)
200
+ end
201
+
202
+ def instance_of?(klass)
203
+ __target__.instance_of?(klass)
204
+ end
205
+
206
+ def class
207
+ __target__.class
208
+ end
209
+ end
210
+ end
211
+ end
@@ -44,6 +44,7 @@ module Expressir
44
44
  "#{__dir__}/express/schema_plain_source_formatter"
45
45
  autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
46
46
  autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
47
+ autoload :LazyRepository, "#{__dir__}/express/lazy_repository"
47
48
  autoload :SmrlXml, "#{__dir__}/express/smrl_xml"
48
49
  autoload :Xsd, "#{__dir__}/express/xsd"
49
50
  autoload :SourceFormatter, "#{__dir__}/express/source_formatter"
@@ -3,6 +3,6 @@ module Expressir
3
3
  # autoload it (Ruby autoload only works for module/class constants, not
4
4
  # for string constants like `Expressir::VERSION`).
5
5
  module Version
6
- VERSION = "2.4.15".freeze
6
+ VERSION = "2.4.17".freeze
7
7
  end
8
8
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+
6
+ ORACLE_TAG = "eeng-5.2.8"
7
+ ORACLE_BASE =
8
+ "https://github.com/expresslang/eengine-releases/releases/download/#{ORACLE_TAG}".freeze
9
+ # Release-asset naming convention for the pinned tag: platform prefix
10
+ # and CPU tag used by expresslang/eengine-releases.
11
+ ORACLE_ASSET_NAMES = {
12
+ %w[darwin arm64] => "mac-arm64",
13
+ %w[darwin x86_64] => "mac-x86-64",
14
+ %w[linux x86_64] => "lnx-x86-64",
15
+ %w[linux arm64] => "lnx-arm64",
16
+ %w[mingw x86_64] => "win-x86-64",
17
+ }.freeze
18
+
19
+ namespace :oracle do
20
+ desc "Fetch the eengine oracle (#{ORACLE_TAG}) into tmp/oracle/eengine " \
21
+ "for the SHTOLO corpus differential"
22
+ task :fetch do
23
+ platform = [Gem::Platform.local.os, Gem::Platform.local.cpu]
24
+ tag = ORACLE_ASSET_NAMES[platform]
25
+ abort "no #{ORACLE_TAG} asset for #{platform.join('-')}" unless tag
26
+
27
+ version = ORACLE_TAG.delete_prefix("eeng-")
28
+ asset = "eengine-#{version}-#{tag}-sbcl#{'.exe' if platform.first == 'mingw'}"
29
+ target = File.expand_path("../tmp/oracle/eengine", __dir__)
30
+ digest = "#{target}.sha256"
31
+
32
+ if File.executable?(target)
33
+ expected = File.read(digest).strip.split(/\s+/).first if File.exist?(digest)
34
+ if expected && Digest::SHA256.file(target).hexdigest == expected
35
+ warn "oracle already present: #{target}"
36
+ exit 0
37
+ end
38
+ warn "cached oracle is missing or corrupt; refetching"
39
+ rm_f target
40
+ end
41
+
42
+ dir = File.dirname(target)
43
+ mkdir_p dir
44
+ download = File.join(dir, "#{asset}.part")
45
+ sh "curl", "-fSL", "-o", download, "#{ORACLE_BASE}/#{asset}"
46
+ sha = Digest::SHA256.file(download).hexdigest
47
+ mv download, target
48
+ chmod 0o755, target
49
+ File.write(digest, "#{sha} eengine\n")
50
+ puts "oracle ready: #{target} (#{sha})"
51
+ end
52
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressir
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.15
4
+ version: 2.4.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-23 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -318,6 +318,7 @@ files:
318
318
  - TODO.parity-ee/15-part28-xml.md
319
319
  - TODO.parity-ee/16-compare-patch.md
320
320
  - TODO.parity-ee/17-official-tests.md
321
+ - TODO.parity-ee/18-mim-mapping.md
321
322
  - TODO.parity-ee/19-architecture.md
322
323
  - TODO.parity-ee/parity-matrix.md
323
324
  - benchmark/srl_benchmark.rb
@@ -505,6 +506,7 @@ files:
505
506
  - lib/expressir/express/grammar/parser.rb
506
507
  - lib/expressir/express/hyperlink_formatter.rb
507
508
  - lib/expressir/express/interface_dot.rb
509
+ - lib/expressir/express/lazy_repository.rb
508
510
  - lib/expressir/express/line_map.rb
509
511
  - lib/expressir/express/listing.rb
510
512
  - lib/expressir/express/model_traversal.rb
@@ -639,6 +641,7 @@ files:
639
641
  - lib/expressir/schema_manifest_entry.rb
640
642
  - lib/expressir/version.rb
641
643
  - lib/tasks/verify_remarks.rake
644
+ - rakelib/oracle.rake
642
645
  - rakelib/verify_remarks.rake
643
646
  homepage: https://github.com/lutaml/expressir
644
647
  licenses: