expressir 2.4.31-aarch64-linux → 2.4.35-aarch64-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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.eengine/00-overview.md +39 -0
  3. data/TODO.eengine/01-resolution-diagnostics.md +30 -0
  4. data/TODO.eengine/02-check-catalog-completion.md +53 -0
  5. data/TODO.eengine/03-entity-computations.md +22 -0
  6. data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
  7. data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
  8. data/TODO.eengine/06-population-p21.md +34 -0
  9. data/TODO.eengine/07-schema-compare-patch.md +25 -0
  10. data/TODO.eengine/08-official-corpus-gate.md +7 -0
  11. data/TODO.eengine/09-mim-mapping-parser.md +9 -0
  12. data/TODO.parity-ee/00-overview.md +1 -1
  13. data/TODO.suma-improvements/07-part28-xml.md +10 -13
  14. data/lib/expressir/3.2/expressir_core.so +0 -0
  15. data/lib/expressir/3.3/expressir_core.so +0 -0
  16. data/lib/expressir/3.4/expressir_core.so +0 -0
  17. data/lib/expressir/4.0/expressir_core.so +0 -0
  18. data/lib/expressir/cli.rb +7 -0
  19. data/lib/expressir/commands/smrl_xml_index.rb +25 -0
  20. data/lib/expressir/commands.rb +1 -0
  21. data/lib/expressir/express/builder.rb +17 -5
  22. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
  23. data/lib/expressir/express/checker.rb +191 -3
  24. data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
  25. data/lib/expressir/express/pretty_formatter.rb +9 -14
  26. data/lib/expressir/express/schema_compare.rb +194 -0
  27. data/lib/expressir/express/shtolo.rb +194 -63
  28. data/lib/expressir/express.rb +2 -0
  29. data/lib/expressir/model/declarations/entity.rb +132 -0
  30. data/lib/expressir/model/declarations/schema.rb +37 -6
  31. data/lib/expressir/p21/builder.rb +92 -0
  32. data/lib/expressir/p21/grammar.rb +107 -0
  33. data/lib/expressir/p21/parser.rb +16 -0
  34. data/lib/expressir/p21/population.rb +35 -0
  35. data/lib/expressir/p21/validator.rb +213 -0
  36. data/lib/expressir/p21.rb +17 -0
  37. data/lib/expressir/version.rb +1 -1
  38. data/lib/expressir.rb +1 -0
  39. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e49355b67f28dde08504db791f89d8da2a00bd6cf52fcdfded84d4ffc1fe2ab9
4
- data.tar.gz: c2c8e67c0ec0dbe02a92e1764c165d3497825049c2d8bd9be9be5d56cf535f4b
3
+ metadata.gz: 03162a2402e4a51e168ed98bad8aa762fd6a7095ce944b0ac2a6dd979d81291a
4
+ data.tar.gz: ccceee1f9fb2bc52451f055bf9afcc7381e8f663d2cce41b6d1ebe82edafdee2
5
5
  SHA512:
6
- metadata.gz: 0c85e6529d67ed3354c2f12d5dc8aa39ee61c60f911cea1816a56f8fba94be419877a30da9c5ac9501fe143e71f3d62a0d4fd487b1b1c9b08431c6d0c1333e84
7
- data.tar.gz: b1413fb816b3ac223eda607d4e01d7f154fb35d6ccc484d6123b85da855887d6a137bdc147c8588f980d7c0b716233f98e545a3b4edf84f9e889e3a43514e27d
6
+ metadata.gz: b5324d363f1064cd9447f1ae4c3fc7161f43145bbc10729eb81bc0163f2f0efd7f58af08bdc83330b3568b2ca944306cd49256ec8bfb4390c4d3a7eecaa7e2b5
7
+ data.tar.gz: ed333015608ba1021fc621064b1cda0b405331667a3052c65336b832b87dda8f024010a0a37bc8b9430d0ffc3ffd6064197db697751e628f76ca9babf3ce45e2
@@ -0,0 +1,39 @@
1
+ # TODO.eengine — the eengine-parity program
2
+
3
+ Consolidates the remaining eengine-parity tasks (formerly scattered
4
+ across TODO.parity-ee) into one execution-ordered series. The oracle
5
+ is eengine-5.1.0 (GIT eeng-5.1.0) at `~/src/external/exp-engine-engine`; where
6
+ eengine and ISO 10303-11 disagree, the standard wins and the
7
+ divergence is classified in the differential output.
8
+
9
+ ## Execution order and status
10
+
11
+ | n | task | folds | status |
12
+ |---|------|-------|--------|
13
+ | 01 | resolution diagnostics + USE/REFERENCE tiering | 06-remainder, 07-part | done |
14
+ | 02 | check-p11 catalog completion | 07, 12 | done (type-resolution tranche; generic unresolved-ref walk covers expression bodies; subtype-constraint supertype refs + SELF redeclaration resolution added; constness has no eeng rule — covered by the generic walk) |
15
+ | 03 | entity computations (inherited/derived/inverse closures) | matrix §3, 05-prereq | done |
16
+ | 04 | SHTOLO rule audit + corpus differential | 14-remainder | done (audit spec live; G.NM.2 + G.1.10 fixed; G.1.6 synthesis remains per annex-g-rules.md) |
17
+ | 05 | SHTOLO longform flatten (Annex G) | 05 | done |
18
+ | 06 | population / Part 21 reader + validation | 13 | done (parsanol reader; schema-mapping validator; Annex H corpus clean; eengine p21 differential blocked — eeng 5.1.0 aborts bare-p11 validate/*PARSE-MODE* NIL, every -MODE demands stepmod context) |
19
+ | 07 | schema compare & patch | 16 | done (SchemaCompare canonical-path deltas + SchemaPatch.apply; oracle differential vs eeng --compare green on add/remove entity/attribute + enum-value changes) |
20
+ | 08 | official corpus CI gate | 17 | done |
21
+ | 09 | MIM mapping parser | 18 | done |
22
+
23
+ ## Sources
24
+
25
+ - eengine: `~/src/external/exp-engine-engine` (kernel/*.lisp,
26
+ plugins/p11/*.lisp, plugins/p21/*).
27
+ - ISO 10303-11 clause text: `~/src/mn/iso-10303-11/sources/sections/`.
28
+ - Detached docs: `~/src/mn/iso-10303-detached-docs/sources/` (Part 21
29
+ grammar, Part 28 text).
30
+
31
+ ## Standing rules
32
+
33
+ - Semantics land in Ruby; parse-adjacent and graph-scale work lands in
34
+ expressir-rs (TODO.parity-ee 19, option B).
35
+ - Where eengine and the standard disagree, the standard wins; every
36
+ such divergence is classified in the differential output, never
37
+ silently copied.
38
+ - Every ported check keeps its eengine note id for oracle
39
+ cross-referencing, and cites its ISO 10303-11 clause.
@@ -0,0 +1,30 @@
1
+ # 01 — Resolution diagnostics + USE/REFERENCE tiering
2
+
3
+ Folds the shipped remainder of TODO.parity-ee 06 (transitive USE and
4
+ the cycle guard are in; `interface_visibility_spec.rb` gates them)
5
+ with the tiering half of 07.
6
+
7
+ ## Gaps
8
+
9
+ - USE FROM resolves **type + entity only**; REFERENCE FROM resolves
10
+ constant/entity/function/procedure/type (eeng
11
+ resolve-interface.lisp:30-66). Our resolution treats both alike.
12
+ - Unresolved resources: eeng hard-errors `Resource '~A' not found`;
13
+ we warn (severity folded here from 07).
14
+
15
+ ## Design
16
+
17
+ - `Schema#interfaced_items(kind:)` filters by interface kind.
18
+ - `Schema#resolve_item(name, kinds:)` walks own declarations then
19
+ interfaces (transitively through USE for :use-from scope),
20
+ cycle-guarded, honoring per-kind restriction.
21
+ - `Expressir::Express::Checker` reports unresolved references as
22
+ errors (`E-RES-001`, the eeng note id family) with schema+name.
23
+
24
+ ## Acceptance
25
+
26
+ - USE-only visibility: an interface's function is NOT visible through
27
+ USE FROM; IS visible through REFERENCE FROM.
28
+ - Transitive USE chains still resolve (06's shipped behavior).
29
+ - Cycle-guarded resolution terminates.
30
+ - The checker flags an unresolved USE FROM resource as an error.
@@ -0,0 +1,53 @@
1
+ # 02 — check-p11 catalog completion
2
+
3
+ Folds TODO.parity-ee 07 + 12. Shipped subset: interface redundant,
4
+ interface resource duplicate, subtype ref/cycle, duplicate
5
+ declaration, where/unique name pattern, unresolved ref, select/enumeration
6
+ extended type, self-schema reference, parse failure.
7
+
8
+ ## Sources
9
+
10
+ - `~/src/external/exp-engine-engine/kernel/check.lisp` (835 lines,
11
+ per-node-type check-p11 methods) + `kernel/check-notes.lisp`.
12
+ - ISO 10303-11:2004 clause text:
13
+ `~/src/mn/iso-10303-11/sources/sections/`.
14
+
15
+ ## Port list (diff of eeng methods vs shipped subset)
16
+
17
+ Per eeng check-p11: entity attribute redeclare compatibility,
18
+ inverse target/entity/attribute existence, derive aggregation rules,
19
+ rule-entity reference containment, function/procedure parameter and
20
+ return checks, subtype-constraint reflexive/rule references,
21
+ constant expression constness. Each ported check keeps the eeng note
22
+ id and cites its ISO 10303-11 clause.
23
+
24
+ ## Acceptance
25
+
26
+ - eeng-differential over the SRL corpus: every eeng-emitted note of
27
+ the ported kinds is emitted here with matching schema/name; no
28
+ false positives on clean corpora (express suite green).
29
+
30
+ ## Status: done
31
+
32
+ Shipped across three tranches:
33
+
34
+ 1. Original subset — interface redundant, interface resource
35
+ duplicate, subtype ref/cycle, duplicate declaration, where/unique
36
+ name pattern, unresolved ref, select/enumeration extended type,
37
+ self-schema reference, parse failure; inverse target/entity/
38
+ attribute checks; group references; aggregate bounds;
39
+ check-attrib-name-fun.
40
+ 2. Type-resolution tranche — constant/local/parameter/return types
41
+ (eeng check-constant-type, check-local-type, check-parameter-type,
42
+ check-fun-return-type) plus rule FOR entity refs.
43
+ 3. Remainder — subtype-constraint supertype-expression refs (eeng
44
+ kernel/check.lisp:426, check_supertype_ref note id) and
45
+ SELF\ redeclaration resolution (ISO 10303-11:2004 9.2.2.3; eeng
46
+ kernel/check.lisp:255 comment-only). Expression bodies are covered
47
+ generically by check_unresolved_refs; eengine has no separate
48
+ constant-expression constness rule (kernel/check.lisp only checks
49
+ the constant's type), so that port-list item closes as covered.
50
+
51
+ Foreign-blindness rule: names that do not resolve in a single-file
52
+ parse (USE FROM targets of unloaded schemas) are skipped, not flagged —
53
+ both new checks verified note-free over 265 stepmod resource schemas.
@@ -0,0 +1,22 @@
1
+ # 03 — Entity computations
2
+
3
+ Matrix §3: eeng materializes inherited attributes and closures
4
+ (kernel/compute-inheritance.lisp, derive-attrs.lisp,
5
+ inverse-attrs.lisp, supertypes.lisp, explicit-attrs.lisp); expressir
6
+ walks supertype chains ad hoc. Needed by Annex G.2.6 select synthesis
7
+ (04/05) and the entity checks (02).
8
+
9
+ ## Design
10
+
11
+ - `Entity#supertype_closure` — transitively closed supertype refs,
12
+ cycle-safe.
13
+ - `Entity#inherited_explicit_attributes` — explicit attributes from
14
+ the full closure, redeclaration-aware (SELF\\supertype.attr
15
+ overrides).
16
+ - `Entity#derived_attributes` / `Entity#inverse_attributes` —
17
+ accumulated from the closure.
18
+
19
+ ## Acceptance
20
+
21
+ - chain + multiple inheritance + redeclaration fixtures; the
22
+ materialization matches a hand-computed table.
@@ -0,0 +1,32 @@
1
+ # 04 — SHTOLO rule audit + corpus differential
2
+
3
+ Folds TODO.parity-ee 14's remainder.
4
+
5
+ - Walk `spec/fixtures/shtolo/annex-g-rules.md` (G.NM.1-2, G.1.1-10,
6
+ G.2.1-7): mark every rule PASS/partial/✖ with a fixture per rule.
7
+ - G.1.6 dependently-instantiable output verified against the Annex G
8
+ text (eeng ✖ there; the standard wins).
9
+ - name-munging policy (`_dot_` and friends): eeng ✖; standard wins.
10
+ - prune-pass reachability (unreachable subtype branches).
11
+ - corpus differential: every modules.lst module arm/mim, flat output
12
+ diffed vs eeng :flat, divergences classified.
13
+
14
+ ## Result: DONE (2026-10-15) — the audit is a live spec
15
+
16
+ `shtolo_annex_g_rule_audit_spec.rb` — 8 examples, one per previously
17
+ uncovered rule. Audited outcome:
18
+
19
+ - G.NM.2, G.1.2, G.1.5, G.1.8 (model-direct), G.2.3, G.2.4, G.2.6 —
20
+ PASS (G.2.4's folded extension keeps its G.NM.1-munged name —
21
+ correct per the name-clash rule).
22
+ - G.1.10 — was absent; **implemented** (`add_provenance_remark!`):
23
+ all-or-none provenance remark on the longform schema with
24
+ schema + schema_version_id lines.
25
+ - Still open (recorded in annex-g-rules.md): G.1.6
26
+ dependently-instantiable RULE/FUNCTION synthesis (eeng ✖; annex is
27
+ the contract) and the G.1.9 prune-depth notes.
28
+
29
+ Fixture note: `EXTENSIBLE_ENUMERATION` as a single composite keyword
30
+ is not in the grammar and not in the corpus — the corpus writes
31
+ `EXTENSIBLE ENUMERATION` (302×) / `EXTENSIBLE SELECT` (546×), both
32
+ supported.
@@ -0,0 +1,12 @@
1
+ # 05 — SHTOLO longform flatten (Annex G)
2
+
3
+ TODO.parity-ee 05. Convert the STEPmod multi-schema web into ONE
4
+ ISO 10303-11:1994 longform schema: no interfaces, visibility
5
+ preserved as rules, no name clashes, unreachable declarations pruned.
6
+
7
+ Sources: `ag-single-schema.adoc` (rule ids), eeng run-op (:flat) +
8
+ print-flatten + wo-flat.lisp (373 lines; eeng keeps structure and
9
+ lowercases — the standard wins on divergence), add/remove-declaration
10
+ and cleanup-interface lisps.
11
+
12
+ Builds on 01 (visibility), 03 (closures), 04 (rule audit).
@@ -0,0 +1,34 @@
1
+ # 06 — Population / Part 21
2
+
3
+ TODO.parity-ee 13, scope ladder (each step ships):
4
+
5
+ 1. P21 reader: parsanol grammar for ISO 10303-21 (HEADER entities,
6
+ DATA section: complex instances `(A(...)B(...))`, simple
7
+ `#1=TYPE(...)`, list/set/typed-ref values, enums `.T.`, binary
8
+ `"...`, redefines `/`); a `Model::Population` (header + instances).
9
+ 2. Population validation against a loaded schema: instance entity
10
+ exists (direct or complex against the supertype graph), attribute
11
+ names/arity match, value types check.
12
+ 3. eengine differential on the Part 21 fixtures
13
+ (`~/src/mn/iso-10303-detached-docs/sources/iso-10303-21*`).
14
+
15
+ ## Status: done
16
+
17
+ 1. Reader — `Expressir::P21` (parsanol grammar + PopulationBuilder):
18
+ HEADER FILE_* entities, DATA simple/complex instances, lists,
19
+ typed parameters, references, enumerations, binaries, strings
20
+ (doubled-quote escape), `$`/`*`, `/* */` comments.
21
+ 2. Validator — `Expressir::P21::Validator`: FILE_SCHEMA name,
22
+ instance keywords, complex-record supertype cliques, mapped
23
+ attribute arity in Part 21 order (supertype attributes first;
24
+ DERIVE-redeclared slots keep their position valued `*` per clause
25
+ 12.2.6), enumeration membership, instance references,
26
+ typed-parameter resolution; optional short_names map for clause
27
+ 12.3 short names.
28
+ 3. Corpus — ISO 10303-21 Annex H complete exchange structure parses
29
+ and validates clean (13 instances, comments, short names).
30
+
31
+ eengine differential (ladder step 3) is blocked by the oracle build:
32
+ eengine 5.1.0 --validate/--xref abort loading a bare p11 schema
33
+ (*PARSE-MODE* NIL) and every -MODE demands a stepmod context, while
34
+ --validate itself is flagged "not fully supported" upstream.
@@ -0,0 +1,25 @@
1
+ # 07 — Schema compare & patch
2
+
3
+ TODO.parity-ee 16. Structural diff of two schemata with a
4
+ machine-applicable patch: declarations matched by canonical path
5
+ (`schema.item`); deltas added/removed/changed (per-attribute
6
+ structural diff: types, optionality, aggregates); patch output
7
+ declarable against the flattened base (eeng `--compare`/`--patch`
8
+ semantics, plugin/p11 compare entry).
9
+
10
+ ## Status: done
11
+
12
+ - `Expressir::Express::SchemaCompare` — deltas at `schema.item` and
13
+ entity-attribute `schema.item.attribute` paths; signatures are
14
+ lutaml-model `to_hash` trees stripped of resolution artifacts
15
+ (`base_path`, `ref`, `source`, remarks).
16
+ - `Expressir::Express::SchemaPatch.apply` — rebuilds the revised
17
+ schema through the framework round trip; the base is never mutated;
18
+ item-level `:changed` nodes subsume their attribute sub-deltas.
19
+ - Interfaces are out of scope by design: the patch target is a
20
+ flattened (longform) schema.
21
+ - Oracle: `spec/expressir/express/schema_compare_oracle_spec.rb`
22
+ agrees with `eengine --compare -MODE RESOURCE -STEPMOD <checkout>`
23
+ on added/removed entities, removed attributes and enum-value type
24
+ changes; gated on binary + stepmod checkout presence
25
+ (EENGINE_BIN / STEPMOD_DIR).
@@ -0,0 +1,7 @@
1
+ # 08 — Official corpus CI gate
2
+
3
+ TODO.parity-ee 17. One rake task mapping every official corpus to
4
+ specs, with CI tiers: (a) every push — eeng bug corpus (bug39/40),
5
+ SHTOLO rule fixtures, Part 21 grammar fixtures; (b) nightly/manual —
6
+ SRL/SMRL full corpus differentials (pretty/flat/xml) when the eengine
7
+ oracle is on PATH.
@@ -0,0 +1,9 @@
1
+ # 09 — MIM mapping parser
2
+
3
+ TODO.parity-ee 18 (expressir#88). Parse + validate MIM reference
4
+ paths into a typed model: entity chains `A -> B -> C`, qualifiers
5
+ `[attr => val]`, `=>` derived references, `->>` subtype traversal,
6
+ `<>` select member. Sources: this checkout's
7
+ `schemas/modules/*/mapping.yaml` (`<<express:SCHEMA.ITEM,ITEM>>` link
8
+ markup), upstream module.xml `reference_path` bodies, the Part 1x
9
+ mapping-annex grammar; the module corpus is the oracle.
@@ -64,7 +64,7 @@ interface/qualify notes; `00check.txt` — check design notes.
64
64
  | 12 | full check-p11 catalog port | planned |
65
65
  | 13 | population / Part 21 validation | planned |
66
66
  | 14 | SHTOLO completion (Annex G + corpus differential) | in progress |
67
- | 15 | Part 28 XML + SMRL index | planned |
67
+ | 15 | Part 28 XML + SMRL index | done (SMRL index + express2xsd shipped, #276; reference-XSD diff harness escalated to 17) |
68
68
  | 16 | schema compare & patch | planned |
69
69
  | 17 | official test-case incorporation | standing structure |
70
70
 
@@ -1,15 +1,12 @@
1
- # 07 — Part 28 XML: select mapping v2 (#276)
1
+ # 07 — Part 28 XML: select mapping v2 (#276) — DONE
2
2
 
3
- ## Goal
4
- Full ISO 10303-28 EXPRESS-to-XML beyond the v1 skeleton
5
- (lib/expressir/express/xsd.rb): proper SELECT mapping (currently
6
- xs:anyType), aggregate/optional fidelity, schema-level structure.
3
+ Shipped: the ISO 10303-28 SELECT mapping lives in
4
+ lib/expressir/express/xsd.rb (complexType + xs:choice, entity members
5
+ as global-element refs, defined-type members by name+type, extensible-
6
+ empty fallback to sequence) and is covered by spec/expressir/express/
7
+ xsd_spec.rb. The CLI exposes both halves: `expressir xsd` and
8
+ `expressir xml-index` (#503).
7
9
 
8
- ## Steps
9
- - ISO 10303-28 mapping rules for SELECT types (union/member semantics)
10
- - apply on top of xsd.rb's element/complexType generation
11
- - validate generated XSDs against the ISO 10303-28 conformance examples
12
-
13
- ## Acceptance
14
- - a select-typed attribute renders per the standard (not xs:anyType)
15
- - the v1 examples still generate unchanged except where v2 improves them
10
+ Remaining escalation (moved to parity-ee 17 scope): validating the
11
+ generated XSDs against the Part 28 reference corpus — the detached
12
+ docs carry the standard text only, not the reference XSDs.
Binary file
Binary file
Binary file
Binary file
data/lib/expressir/cli.rb CHANGED
@@ -111,6 +111,13 @@ module Expressir
111
111
  Expressir::Commands::Xsd.new(options).run(path)
112
112
  end
113
113
 
114
+ desc "xml-index DIR", "Write the SMRL index XML for every EXPRESS schema under DIR"
115
+ method_option :output, type: :string, aliases: "-o",
116
+ desc: "Output file path (defaults to stdout)"
117
+ def xml_index(path)
118
+ Expressir::Commands::SmrlXmlIndex.new(options).run(path)
119
+ end
120
+
114
121
  desc "validate SUBCOMMAND", "EXPRESS schema validation commands"
115
122
  subcommand "validate", Expressir::Commands::Validate
116
123
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Expressir
6
+ module Commands
7
+ # `expressir xml-index` (#276): write the SMRL index XML for a
8
+ # repository — one <schema> element per schema, the data layer of
9
+ # the STEP Module Resource Library document set.
10
+ class SmrlXmlIndex < Base
11
+ def run(path)
12
+ repo = Expressir::Express::Parser.from_files(Dir.glob(File.join(path, "**", "*.exp")), skip_references: true)
13
+ raise Thor::Error, "no schemas under #{path}" if repo.schemas.empty?
14
+
15
+ xml = Expressir::Express::SmrlXml.format(repo)
16
+ if options[:output]
17
+ File.write(options[:output], xml)
18
+ say "written to #{options[:output]}"
19
+ else
20
+ $stdout.write(xml)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -26,5 +26,6 @@ module Expressir
26
26
  autoload :Manifest, "#{__dir__}/commands/manifest"
27
27
  autoload :MappingValidate, "#{__dir__}/commands/mapping_validate"
28
28
  autoload :Xsd, "#{__dir__}/commands/xsd"
29
+ autoload :SmrlXmlIndex, "#{__dir__}/commands/smrl_xml_index"
29
30
  end
30
31
  end
@@ -186,17 +186,29 @@ module Expressir
186
186
 
187
187
  case item
188
188
  when Array
189
- item.each do |sub|
190
- result << build(sub) unless sub.nil?
191
- end
189
+ item.each { |sub| append_built(result, build(sub)) unless sub.nil? }
192
190
  else
193
- built = build(item)
194
- result << built if built
191
+ append_built(result, build(item))
195
192
  end
196
193
  end
197
194
  result
198
195
  end
199
196
 
197
+ # Builders for list productions (ExplicitAttrBuilder and friends)
198
+ # return Arrays of nodes. Splice those items into the parent list
199
+ # rather than nesting them — nested Arrays break every consumer
200
+ # that expects ModelElements (`x.kind`, tree walks, formatters).
201
+ def append_built(result, built)
202
+ case built
203
+ when nil
204
+ nil
205
+ when Array
206
+ built.each { |b| append_built(result, b) }
207
+ else
208
+ result << built
209
+ end
210
+ end
211
+
200
212
  # Fast build methods - call registered builder directly without hash wrapping
201
213
  # These are optimized for hot paths in expression building
202
214
  # NOTE: These assume data is already snake_case (no key conversion needed)
@@ -73,17 +73,17 @@ module Expressir
73
73
  if entity_body.is_a?(Hash)
74
74
  if entity_body[:explicit_attr]
75
75
  explicit_attrs = Builder.build_children(entity_body[:explicit_attr])
76
- attributes.concat(explicit_attrs)
76
+ attributes.concat(flatten_items(explicit_attrs))
77
77
  end
78
78
 
79
79
  if entity_body[:derive_clause]
80
80
  derived = Builder.build({ derive_clause: entity_body[:derive_clause] })
81
- attributes.push(derived) if derived
81
+ attributes.concat(flatten_items(derived))
82
82
  end
83
83
 
84
84
  if entity_body[:inverse_clause]
85
85
  inverse = Builder.build({ inverse_clause: entity_body[:inverse_clause] })
86
- attributes.push(inverse) if inverse
86
+ attributes.concat(flatten_items(inverse))
87
87
  end
88
88
 
89
89
  if entity_body[:unique_clause]
@@ -103,13 +103,25 @@ module Expressir
103
103
  supertype_expression: supertype_expression,
104
104
  subtype_of: subtype_of,
105
105
  attributes: attributes,
106
- unique_rules: [unique_rules].flatten,
107
- where_rules: [where_rules].flatten,
106
+ unique_rules: flatten_items(unique_rules),
107
+ where_rules: flatten_items(where_rules),
108
108
  }
109
109
  entity_params[:abstract] = true if abstract
110
110
 
111
111
  Expressir::Model::Declarations::Entity.new(**entity_params)
112
112
  end
113
+
114
+ private
115
+
116
+ # ExplicitAttrBuilder / DeriveClauseBuilder / InverseClauseBuilder
117
+ # return Arrays (one node per declaration). Call sites must splice
118
+ # those items into the parent list — nesting them leaves
119
+ # `entity.attributes` as Array-of-Array, which formatters then
120
+ # crash on (`undefined method 'kind' for an instance of Array`).
121
+ # lutaml-model 0.8.107 no longer flattens collection assignments.
122
+ def flatten_items(value)
123
+ Array(value).flatten.compact
124
+ end
113
125
  end
114
126
  end
115
127
  end