expressir 2.4.9 → 2.4.10

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: 1dac0cba9583d9371198d313d26da56bb31fa9b62392acd9548981eda41a115a
4
- data.tar.gz: e0b069f89d8358e1244d930ddac24f56be78bfe2aa6e3bf9ab91b895049326fc
3
+ metadata.gz: 4a8a349971d0d91880df982145a16571130609ca66be9082aa512dad577e2156
4
+ data.tar.gz: 19c5b335acdf93eb742096e0405424f7a45fa271aeaa9fd6e7f000787e0574e4
5
5
  SHA512:
6
- metadata.gz: c2f3cf0c7f2d375454ebcd182eb0bfcbffbf3b1725b484b9c8e81a92fbef9de65384509eff26aea2462adf4888c375d0d7e7bd5228ac59cff974cdc7c7c78d1a
7
- data.tar.gz: d61dcd5186a1c52bf1a259fa3146bc6e3ca6701459785a3854d5a2e3c430bb1ded81267ad90fba4ee345531f6822baee5ebe2535d848bc3d5f19d7d4ddaa49f6
6
+ metadata.gz: 431c803fc5c99f82c513734225fb7cf37f039c7dae5da6f85b130e778cf1b2f2b34bd37a5288b86ad2fd8a2c6861441263e6313501dc484d88ca15b80ece156f
7
+ data.tar.gz: 873c8f40bc8c08970258106903cccc04e18b758a5af3a3cd48bf685c909f9cefe3231c4570c64fe87f96bb4e74aced28883e5be2174d3f50683905a731459549
@@ -0,0 +1,97 @@
1
+ # 19 — End-state architecture: Expressir in Rust, bound to metanorma via lutaml-model
2
+
3
+ **Question (2026-09-23): should Expressir ultimately be in Rust, bound to
4
+ metanorma via lutaml-model?**
5
+
6
+ **Answer: yes as a direction, no as a rewrite.** The end state is
7
+ *Rust engine, Ruby presentation* — reached in three milestones, not by
8
+ porting everything at once. The binding to metanorma goes through
9
+ lutaml-model's Liquid Drop layer, which is exactly the seam that
10
+ already works (expressir#255, #319, #421, #426).
11
+
12
+ ## Current state (v2.4.9, measured)
13
+
14
+ ```
15
+ .exp ──► parsanol-rs 0.8.8 (parse, wire tree)
16
+ └─► expressir_core ext (batch, EXSCS1 artifact, warm start)
17
+ └─► instantiate → Expressir::Model (lutaml-model, Ruby)
18
+ └─► Checker / Shtolo / formatters (Ruby semantics)
19
+ └─► Liquid Drops ──► metanorma / suma
20
+ ```
21
+
22
+ - Parse is Rust and fast (SRL cold 36 s CPU, SMRL 558 s CPU / 1448
23
+ schemas); parsanol 0.8.8's selective memoization is adopted.
24
+ - The measured floor is now the **wire→Ruby-object hydration**
25
+ (71 % of warm CPU) and the overlays (29 %). Rendering (metanorma)
26
+ touches a small fraction of the model.
27
+ - Semantics (Checker subset, SHTOLO with the 50-module eengine
28
+ differential, PrettyFormatter, SMRL XML) live in Ruby, eeng-parity
29
+ validated, with Ruby CI harnesses.
30
+
31
+ ## Options considered
32
+
33
+ **A — Status quo+.** Rust keeps parse+artifact; all new semantics
34
+ (Checker catalog, mapping validation) land in Ruby. No rewrite risk,
35
+ parity harnesses stay, but the hydration floor remains and the Rust
36
+ surface stays private.
37
+
38
+ **B — Lazy binding (the recommended milestone).** Keep the Ruby model
39
+ as the canonical object model for feature work, and add a READ path
40
+ that serves metanorma directly from Rust-held compiled-set data:
41
+ expressir_core exposes accessors over the loaded set (schema by id,
42
+ kind listings, field reads), and a lutaml-model Drop proxies them on
43
+ demand — rendering never instantiates the full object graph.
44
+ lutaml-model keeps ownership of mappings/serialization/Drop
45
+ integration; the Drop becomes the literal "Expressir in Rust bound to
46
+ metanorma via lutaml-model".
47
+
48
+ **C — Full Rust Expressir.** Model + semantics in Rust; Ruby keeps
49
+ only Drops and CLI glue; publish expressir-rs on crates.io; unlock
50
+ non-Ruby consumers (standalone binary, WASM). Rejected for now: it
51
+ re-implements eeng-validated semantics with parity risk, doubles CI,
52
+ and metanorma still needs a Drop/FFI layer — the boundary cost moves,
53
+ it does not disappear.
54
+
55
+ **Decision: B now; C deferred behind explicit triggers**
56
+ (a non-Ruby consumer materializes; the Ruby semantic layers become a
57
+ measured bottleneck — they are not today; or crate publishing is
58
+ wanted for supply-chain reasons independent of consumers).
59
+
60
+ ## Milestones
61
+
62
+ ### M1 — rev-pinned core dependency (supply chain, one line)
63
+ The ext pins `expressir-rs = { git …, branch = "main" }`; a released
64
+ gem can pick up a shifted core. Pin `rev = "<sha>"` per expressir
65
+ release (2.4.8 ↔ today's expressir-rs main), bump per release.
66
+
67
+ ### M2 — lazy Drops for the metanorma hot path
68
+ 1. expressir-rs: read accessors over the loaded compiled set
69
+ (`schema(id)`, kind listings, per-declaration field reads,
70
+ remark/refs overlay application in place).
71
+ 2. expressir_core: magnus wrappers returning primitives.
72
+ 3. Ruby: `LazySchemaDrop` (lutaml-model #319 custom Drop, the same
73
+ seam as #255/#426) implementing id / declaration listings / source
74
+ faces via the accessors.
75
+ 4. Gates: suma-style schema-doc pages render output-comparable with
76
+ the eager path on a fixture corpus; warm page-render benchmark;
77
+ SMRL warm-start CPU target ≤ 30 % of today's 128 s for the
78
+ read-only path.
79
+
80
+ ### M3 — semantics placement rule (standing)
81
+ New semantic features land in Ruby **unless** they are
82
+ parse-adjacent or graph-scale (then Rust in expressir-rs). The
83
+ Checker catalog (12) and mapping validation (18) stay Ruby; SHTOLO
84
+ stays Ruby with its eengine differential as the gate.
85
+
86
+ ### M4 — trigger review for option C
87
+ Reassess on any of: a non-Ruby consumer request; hydrate/overlay CPU
88
+ re-emerging as the top cost after M2; a crates.io publication ask.
89
+ M2's accessors are exactly the seed of C's public API — nothing in B
90
+ is throwaway.
91
+
92
+ ## What lutaml-model owns in every option
93
+
94
+ The mapping DSL (yaml/json faces — incl. the #88 `mapping.yaml`
95
+ reader), Liquid integration (#319 Drops), artifact serialization —
96
+ the binding contract between the engine and metanorma. Rust never
97
+ duplicates it.
@@ -93,6 +93,7 @@ module Expressir
93
93
  schema.entities.each { |e| check_entity(schema, e) }
94
94
  schema.types.each do |t|
95
95
  check_type(schema, t)
96
+ check_select_items(schema, t)
96
97
  check_where_rules(schema, t, t.where_rules)
97
98
  end
98
99
  schema.rules.each { |r| check_where_rules(schema, r, r.where_rules) }
@@ -208,7 +209,15 @@ module Expressir
208
209
 
209
210
  note!(:check_subtype_ref, :error, schema,
210
211
  "ENTITY #{entity.id}: subtype '#{id}' not found", entity)
212
+ next unless declared_kind(schema, id) == :type
213
+
214
+ # eeng check-subtypeof-invalid: the name exists but is not
215
+ # an entity, so it cannot be a supertype.
216
+ note!(:check_subtypeof_invalid, :error, schema,
217
+ "ENTITY #{entity.id}: subtype '#{id}' is a TYPE, " \
218
+ "not an entity", entity)
211
219
  end
220
+ check_supertype_refs(schema, entity)
212
221
  check_subtype_cycles(schema, entity)
213
222
 
214
223
  check_where_rules(schema, entity, entity.where_rules)
@@ -261,6 +270,66 @@ module Expressir
261
270
  cyclic
262
271
  end
263
272
 
273
+ # eeng check-supertype-ref: every entity named in the SUPERTYPE
274
+ # expression must resolve to an entity of this schema.
275
+ def check_supertype_refs(schema, entity)
276
+ expr = entity.supertype_expression
277
+ return unless expr
278
+
279
+ each_reference(expr) do |ref|
280
+ id = ref.id
281
+ next if id.nil? || find_entity(schema, id)
282
+
283
+ note!(:check_supertype_ref, :error, schema,
284
+ "ENTITY #{entity.id}: supertype '#{id}' not found", entity)
285
+ end
286
+ end
287
+
288
+ def each_reference(node, &)
289
+ case node
290
+ when Model::References::SimpleReference then yield node
291
+ when Model::ModelElement
292
+ node.class.attributes.each_key do |attr|
293
+ next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr) || attr == :parent
294
+
295
+ value = node.public_send(attr)
296
+ case value
297
+ when Array then value.each { |item| each_reference(item, &) }
298
+ when Model::ModelElement then each_reference(value, &)
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ # eeng check-select-named-type: SELECT items must name types.
305
+ def check_select_items(schema, type)
306
+ return unless type.underlying_type.is_a?(Model::DataTypes::Select)
307
+
308
+ Array(type.underlying_type.items).each do |item|
309
+ next if item.id.nil?
310
+ next if find_type(schema, item.id)
311
+ next if find_entity(schema, item.id).nil? # unresolved handled elsewhere
312
+
313
+ note!(:check_select_named_type, :error, schema,
314
+ "TYPE #{type.id}: select item '#{item.id}' is an entity, " \
315
+ "not a named type", type)
316
+ end
317
+ end
318
+
319
+ def find_type(schema, id)
320
+ key = id.safe_downcase
321
+ return schema.types.find { |t| t.id.safe_downcase == key } if schema
322
+
323
+ nil
324
+ end
325
+
326
+ # eeng declared-kind probe: :type when a TYPE of that name is
327
+ # visible in the schema (used to separate not-found from
328
+ # not-an-entity in subtype diagnostics).
329
+ def declared_kind(schema, id)
330
+ :type if find_type(schema, id)
331
+ end
332
+
264
333
  def check_where_rules(schema, owner, rules)
265
334
  Array(rules).each do |wr|
266
335
  next if wr.id.nil? || wr.id.match?(WHERE_LABEL)
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # EXPRESS → XML Schema writer (expressir #276 step 2,
6
+ # TODO.parity-ee 15) — the express2xsd counterpart, built on
7
+ # moxml.
8
+ #
9
+ # Mapping (v1):
10
+ # ENTITY → xs:element + xs:complexType (explicit attributes
11
+ # in declaration order); SUBTYPE OF a schema-local
12
+ # entity becomes a substitutionGroup reference.
13
+ # ENUMERATION → xs:simpleType with enumeration facets.
14
+ # SELECT → xs:element of xs:anyType (full Part 28 select
15
+ # mapping is a documented v1 limitation).
16
+ # defined TYPE over a simple type → xs:simpleType restriction.
17
+ # attributes → xs:element inside the complexType sequence;
18
+ # LIST/SET/BAG/ARRAY → maxOccurs="unbounded".
19
+ #
20
+ # Built-in EXPRESS types map to XML Schema built-ins (STRING→
21
+ # xs:string, INTEGER→xs:integer, REAL/NUMBER→xs:double,
22
+ # BOOLEAN→xs:boolean, LOGICAL→string restriction, BINARY→
23
+ # xs:hexBinary).
24
+ module Xsd
25
+ BUILTIN_TYPES = {
26
+ "string" => "xs:string",
27
+ "integer" => "xs:integer",
28
+ "real" => "xs:double",
29
+ "number" => "xs:double",
30
+ "boolean" => "xs:boolean",
31
+ "binary" => "xs:hexBinary",
32
+ }.freeze
33
+
34
+ AGGREGATES = %w[array list set bag].freeze
35
+
36
+ module_function
37
+
38
+ # @param schema [Model::Declarations::Schema]
39
+ # @return [String] the serialized xs:schema document
40
+ def format(schema)
41
+ ctx = Moxml::Context.new
42
+ doc = ctx.create_document
43
+ root = doc.create_element("xs:schema")
44
+ root.set_attributes("xmlns:xs" => "http://www.w3.org/2001/XMLSchema",
45
+ "elementFormDefault" => "qualified")
46
+ doc.add_child(root)
47
+ schema.types.to_a.each { |type| root.add_child(type_node(doc, schema, type)) }
48
+ schema.entities.to_a.each do |entity|
49
+ root.add_child(entity_element(doc, schema, entity))
50
+ root.add_child(entity_type(doc, schema, entity))
51
+ end
52
+ doc.to_xml
53
+ end
54
+
55
+ def type_node(doc, schema, type)
56
+ underlying = type.underlying_type
57
+ node = doc.create_element("xs:simpleType")
58
+ node.set_attributes("name" => type.id)
59
+ restriction = doc.create_element("xs:restriction")
60
+ node.add_child(restriction)
61
+ if underlying.is_a?(Expressir::Model::DataTypes::Enumeration)
62
+ restriction.set_attributes("base" => "xs:string")
63
+ underlying.items.to_a.each do |item|
64
+ facet = doc.create_element("xs:enumeration")
65
+ facet.set_attributes("value" => item.id)
66
+ restriction.add_child(facet)
67
+ end
68
+ else
69
+ restriction.set_attributes("base" => xs_type(underlying, schema))
70
+ end
71
+ node
72
+ end
73
+
74
+ def entity_element(doc, schema, entity)
75
+ node = doc.create_element("xs:element")
76
+ node.set_attributes("name" => entity.id)
77
+ node.set_attributes("type" => entity.id)
78
+ parent = supertype_name(schema, entity)
79
+ node.set_attributes("substitutionGroup" => parent) if parent
80
+ node
81
+ end
82
+
83
+ def entity_type(doc, schema, entity)
84
+ node = doc.create_element("xs:complexType")
85
+ node.set_attributes("name" => entity.id)
86
+ sequence = doc.create_element("xs:sequence")
87
+ node.add_child(sequence)
88
+ entity.attributes.to_a.each do |attr|
89
+ skip = attr.kind == Expressir::Model::Declarations::Attribute::INVERSE ||
90
+ (attr.respond_to?(:derive?) && attr.derive?)
91
+ next if skip
92
+
93
+ sequence.add_child(attribute_element(doc, schema, attr))
94
+ end
95
+ node
96
+ end
97
+
98
+ def attribute_element(doc, schema, attr)
99
+ node = doc.create_element("xs:element")
100
+ node.set_attributes("name" => attr.id)
101
+ node.set_attributes("type" => xs_type(attr.type, schema))
102
+ kind = attr.type.class.name.split("::").last.downcase
103
+ node.set_attributes("maxOccurs" => "unbounded") if AGGREGATES.include?(kind)
104
+ node
105
+ end
106
+
107
+ def xs_type(type, schema)
108
+ if type.is_a?(Expressir::Model::References::SimpleReference)
109
+ id = type.respond_to?(:base_path) && type.base_path ? base_id(type.base_path) : type.id
110
+ return schema.types.to_a.any? { |t| t.id.safe_downcase == id.safe_downcase } ? id : "xs:anyType"
111
+ end
112
+
113
+ if type.is_a?(Expressir::Model::DataTypes::Enumeration)
114
+ return type.id || "xs:string"
115
+ end
116
+
117
+ if type.respond_to?(:base_type)
118
+ inner = type.base_type
119
+ return xs_type(inner, schema) if inner
120
+
121
+ return "xs:anyType"
122
+ end
123
+
124
+ name = type.class.name.split("::").last.downcase
125
+ BUILTIN_TYPES.fetch(name, "xs:anyType")
126
+ end
127
+
128
+ def supertype_name(schema, entity)
129
+ ref = Array(entity.subtype_of).first
130
+ return nil unless ref
131
+
132
+ id = ref.is_a?(String) ? ref : ref.id
133
+ return nil unless id
134
+ return nil unless schema.entities.to_a.any? { |e| e.id.safe_downcase == id.safe_downcase }
135
+
136
+ schema.entities.find { |e| e.id.safe_downcase == id.safe_downcase }.id
137
+ end
138
+
139
+ def base_id(base_path)
140
+ parts = base_path.split(".")
141
+ parts[-1]
142
+ end
143
+ end
144
+ end
145
+ end
@@ -45,6 +45,7 @@ module Expressir
45
45
  autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
46
46
  autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
47
47
  autoload :SmrlXml, "#{__dir__}/express/smrl_xml"
48
+ autoload :Xsd, "#{__dir__}/express/xsd"
48
49
  autoload :SourceFormatter, "#{__dir__}/express/source_formatter"
49
50
  autoload :StreamingBuilder, "#{__dir__}/express/streaming_builder"
50
51
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ # MIM application-element mapping model (expressir#88,
5
+ # TODO.parity-ee 18): the typed reader for the `mapping.yaml` files
6
+ # each STEP module carries, and validation of the
7
+ # `<<express:SCHEMA.ITEM,ITEM>>` links they contain against a
8
+ # resolved ARM/MIM repository.
9
+ #
10
+ # The model is lutaml-model Serializable — the yaml face is the
11
+ # module corpus format (top-level `ae` + `sc` keys); fields the
12
+ # corpus carries that v1 does not model (alt_map, descriptions)
13
+ # are ignored by the mapping and survive round-trips untouched in
14
+ # the source files.
15
+ module Mapping
16
+ EXPRESS_LINK = /<<express:([^,>]+)(?:,([^>]+))?>>/
17
+
18
+ class ReferencePath < Lutaml::Model::Serializable
19
+ attribute :content, :string
20
+
21
+ key_value do
22
+ map "content", to: :content
23
+ end
24
+ end
25
+
26
+ class AttributeMapping < Lutaml::Model::Serializable
27
+ attribute :attribute, :string
28
+ attribute :aimelt, :string
29
+ attribute :inherited_from_entity, :string
30
+ attribute :assertion_to, :string
31
+ attribute :refpath, ReferencePath
32
+
33
+ key_value do
34
+ map "attribute", to: :attribute
35
+ map "aimelt", to: :aimelt
36
+ map "inherited_from_entity", to: :inherited_from_entity
37
+ map "assertion_to", to: :assertion_to
38
+ map "refpath", to: :refpath
39
+ end
40
+ end
41
+
42
+ class ApplicationElement < Lutaml::Model::Serializable
43
+ attribute :entity, :string
44
+ attribute :aimelt, :string
45
+ attribute :extensible, :string
46
+ attribute :original_module, :string
47
+ attribute :refpath, ReferencePath
48
+ attribute :aa, AttributeMapping, collection: true
49
+
50
+ key_value do
51
+ map "entity", to: :entity
52
+ map "aimelt", to: :aimelt
53
+ map "extensible", to: :extensible
54
+ map "original_module", to: :original_module
55
+ map "refpath", to: :refpath
56
+ map "aa", to: :aa
57
+ end
58
+ end
59
+
60
+ class SubtypeConstraintMapping < Lutaml::Model::Serializable
61
+ attribute :constraint, :string
62
+ attribute :entity, :string
63
+
64
+ key_value do
65
+ map "constraint", to: :constraint
66
+ map "entity", to: :entity
67
+ end
68
+ end
69
+
70
+ class Document < Lutaml::Model::Serializable
71
+ attribute :ae, ApplicationElement, collection: true
72
+ attribute :sc, SubtypeConstraintMapping, collection: true
73
+
74
+ key_value do
75
+ map "ae", to: :ae
76
+ map "sc", to: :sc
77
+ end
78
+ end
79
+
80
+ UnknownLink = Struct.new(:text, :schema, :item, keyword_init: true)
81
+
82
+ module_function
83
+
84
+ # @param path [String] path to a module mapping.yaml
85
+ # @return [Document]
86
+ def load_file(path)
87
+ Document.from_yaml(File.read(path))
88
+ end
89
+
90
+ # Every `<<express:...>>` link in the document, as
91
+ # [schema_id, item_id] pairs (duplicates preserved).
92
+ def links(document)
93
+ values(document).flat_map do |value|
94
+ value.to_s.scan(EXPRESS_LINK).map do |path, _id|
95
+ parts = path.split(".")
96
+ [parts[-2], parts[-1]]
97
+ end
98
+ end
99
+ end
100
+
101
+ # Links whose schema or item cannot be found in +repository+.
102
+ # A link with no item part resolves against schema presence only.
103
+ def unknown_links(document, repository)
104
+ known = repository.schemas.to_h do |schema|
105
+ [schema.id.safe_downcase, schema]
106
+ end
107
+ links(document).filter_map do |schema_id, item_id|
108
+ schema = known[schema_id&.safe_downcase]
109
+ if schema.nil?
110
+ next UnknownLink.new(text: "#{schema_id}.#{item_id}",
111
+ schema: schema_id, item: item_id)
112
+ end
113
+ next if item_id.nil?
114
+
115
+ unless item_declared?(schema, item_id)
116
+ next UnknownLink.new(text: "#{schema_id}.#{item_id}",
117
+ schema: schema_id, item: item_id)
118
+ end
119
+
120
+ nil
121
+ end
122
+ end
123
+
124
+ def item_declared?(schema, item_id)
125
+ key = item_id.safe_downcase
126
+ %i[types entities functions procedures rules constants
127
+ subtype_constraints].any? do |coll|
128
+ Array(schema.public_send(coll)).any? do |decl|
129
+ decl.respond_to?(:id) && decl.id&.safe_downcase == key
130
+ end
131
+ end
132
+ end
133
+
134
+ def values(document)
135
+ document.ae.to_a.flat_map do |element|
136
+ [element.entity, element.aimelt] +
137
+ element.aa.to_a.flat_map { |aa| [aa.attribute, aa.aimelt] }
138
+ end.compact
139
+ end
140
+ end
141
+ end
@@ -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.9".freeze
6
+ VERSION = "2.4.10".freeze
7
7
  end
8
8
  end
data/lib/expressir.rb CHANGED
@@ -29,6 +29,7 @@ module Expressir
29
29
  extend Config
30
30
 
31
31
  autoload :Version, "#{__dir__}/expressir/version"
32
+ autoload :Mapping, "#{__dir__}/expressir/mapping"
32
33
 
33
34
  # Error classes — autoload the same errors.rb file from each name.
34
35
  # The first reference triggers the load; the file defines all 21 classes.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressir
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.9
4
+ version: 2.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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/19-architecture.md
321
322
  - TODO.parity-ee/parity-matrix.md
322
323
  - benchmark/srl_benchmark.rb
323
324
  - benchmark/srl_native_benchmark.rb
@@ -526,10 +527,12 @@ files:
526
527
  - lib/expressir/express/smrl_xml.rb
527
528
  - lib/expressir/express/source_formatter.rb
528
529
  - lib/expressir/express/streaming_builder.rb
530
+ - lib/expressir/express/xsd.rb
529
531
  - lib/expressir/liquid.rb
530
532
  - lib/expressir/manifest.rb
531
533
  - lib/expressir/manifest/resolver.rb
532
534
  - lib/expressir/manifest/validator.rb
535
+ - lib/expressir/mapping.rb
533
536
  - lib/expressir/model.rb
534
537
  - lib/expressir/model/cache.rb
535
538
  - lib/expressir/model/concerns.rb