expressir 2.4.24 → 2.4.25
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 +4 -4
- data/.github/scripts/test_installed_gem.rb +29 -0
- data/.github/workflows/native-gems.yml +183 -0
- data/.gitignore +5 -0
- data/Cargo.toml +1 -1
- data/Gemfile +1 -0
- data/TODO.suma-improvements/00-overview.md +23 -0
- data/TODO.suma-improvements/01-prebuilt-platform-gems.md +33 -0
- data/TODO.suma-improvements/02-suma-e2e.md +39 -0
- data/TODO.suma-improvements/03-item-graph.md +22 -0
- data/TODO.suma-improvements/04-compiled-set-default.md +16 -0
- data/TODO.suma-improvements/05-validation-in-docs.md +17 -0
- data/TODO.suma-improvements/06-memory-remeasure.md +36 -0
- data/TODO.suma-improvements/07-part28-xml.md +15 -0
- data/TODO.suma-improvements/08-windows-gnu-toolchain.md +15 -0
- data/TODO.suma-improvements/09-checker-tranche3.md +13 -0
- data/TODO.suma-improvements/10-expressdoc.md +8 -0
- data/ext/expressir_core/Cargo.toml +0 -3
- data/ext/expressir_core/extconf.rb +4 -1
- data/lib/expressir/express/checker.rb +192 -0
- data/lib/expressir/express/parser.rb +45 -0
- data/lib/expressir/express/xsd.rb +131 -17
- data/lib/expressir/model/indexes/item_graph.rb +50 -0
- data/lib/expressir/version.rb +1 -1
- data/rakelib/native.rake +45 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da722d6f42601529fb1f7798255f6f5e8fed696c4c64998ed7e8c5b47aacb598
|
|
4
|
+
data.tar.gz: d9ead75080ad649480d7665cc526d500e9a228f18f941e2d0efbeab03d7871f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 194e26d86f8043557120fb8935491376420600e8fecbde5ad99ceef352f3fe77251a1320c16ddb25f2ef293be33ca85c7e2daf1c02df1bcba16346d4067d1550
|
|
7
|
+
data.tar.gz: 68da6ffb076de9b604107ac2129d1b11224c57102037f84f526dcbd5efc4fdb54a012f843f3cb0e7f20e29eda4f11e8ee6dce81a3d58c4ed9b7238857ee02609
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Smoke test for an INSTALLED expressir gem (run after `gem install`,
|
|
4
|
+
# never from the source directory): the Rust core binding must be
|
|
5
|
+
# active and the parser must handle the standard and #462 forms.
|
|
6
|
+
require "expressir"
|
|
7
|
+
|
|
8
|
+
abort "FAIL: NATIVE_AVAILABLE is false" unless Expressir::Express::Core::NATIVE_AVAILABLE
|
|
9
|
+
puts "native binding: active"
|
|
10
|
+
|
|
11
|
+
repo = Expressir::Express::Parser.from_exp(
|
|
12
|
+
"SCHEMA smoke; ENTITY e; a : STRING; END_ENTITY; END_SCHEMA;",
|
|
13
|
+
)
|
|
14
|
+
abort "FAIL: schema not parsed" unless repo.schemas.first.id == "smoke"
|
|
15
|
+
puts "standard parse: ok"
|
|
16
|
+
|
|
17
|
+
src = <<~EXP
|
|
18
|
+
SCHEMA m;
|
|
19
|
+
ENTITY e; END_ENTITY;
|
|
20
|
+
SUBTYPE_CONSTRAINT c FOR e;
|
|
21
|
+
ABSTRACT;
|
|
22
|
+
END_SUBTYPE_CONSTRAINT;
|
|
23
|
+
END_SCHEMA;
|
|
24
|
+
EXP
|
|
25
|
+
sc = Expressir::Express::Parser.from_exp(src).schemas.first.subtype_constraints.first
|
|
26
|
+
abort "FAIL: subtype constraint ABSTRACT body (#462)" unless sc&.abstract == true
|
|
27
|
+
puts "#462 form: ok"
|
|
28
|
+
|
|
29
|
+
puts "SMOKE OK"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Prebuilt platform gems (TODO.suma-improvements/01), following the
|
|
2
|
+
# parsanol-ruby gem-build pattern: oxidize-rb cross-gem builds
|
|
3
|
+
# per-platform gems in rake-compiler-dock containers, smoke jobs install
|
|
4
|
+
# them on real runners, and tag pushes publish to rubygems.
|
|
5
|
+
name: native-gems
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main, "cross-gem/*", "pkg/*"]
|
|
11
|
+
tags: ["v*"]
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
ci-data:
|
|
22
|
+
runs-on: ubuntu-22.04
|
|
23
|
+
outputs:
|
|
24
|
+
result: ${{ steps.fetch.outputs.result }}
|
|
25
|
+
steps:
|
|
26
|
+
- id: fetch
|
|
27
|
+
uses: oxidize-rb/actions/fetch-ci-data@v1
|
|
28
|
+
with:
|
|
29
|
+
supported-ruby-platforms: |
|
|
30
|
+
exclude: [x64-mingw32]
|
|
31
|
+
stable-ruby-versions: |
|
|
32
|
+
only: [3.2, 3.3, 3.4, 4.0]
|
|
33
|
+
|
|
34
|
+
native:
|
|
35
|
+
name: Build native gems
|
|
36
|
+
needs: ci-data
|
|
37
|
+
runs-on: ubuntu-22.04
|
|
38
|
+
strategy:
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix:
|
|
41
|
+
ruby-platform: ${{ fromJSON(needs.ci-data.outputs.result).supported-ruby-platforms }}
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- uses: ruby/setup-ruby@v1
|
|
46
|
+
with:
|
|
47
|
+
ruby-version: "3.4"
|
|
48
|
+
|
|
49
|
+
- uses: oxidize-rb/actions/cross-gem@v1
|
|
50
|
+
id: cross-gem
|
|
51
|
+
env:
|
|
52
|
+
EXPRESSIR_CORE_FORCE: "1"
|
|
53
|
+
with:
|
|
54
|
+
platform: ${{ matrix.ruby-platform }}
|
|
55
|
+
ruby-versions: "3.2,3.3,3.4,4.0"
|
|
56
|
+
|
|
57
|
+
- uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: cross-gem-${{ matrix.ruby-platform }}
|
|
60
|
+
path: ${{ steps.cross-gem.outputs.gem-path }}
|
|
61
|
+
if-no-files-found: error
|
|
62
|
+
|
|
63
|
+
smoke-linux:
|
|
64
|
+
name: Smoke test Linux x86_64
|
|
65
|
+
needs: native
|
|
66
|
+
runs-on: ubuntu-22.04
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
- uses: ruby/setup-ruby@v1
|
|
70
|
+
with:
|
|
71
|
+
ruby-version: "3.4"
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: cross-gem-x86_64-linux
|
|
75
|
+
path: pkg/
|
|
76
|
+
- run: |
|
|
77
|
+
gem install pkg/expressir-*.gem --no-document
|
|
78
|
+
ruby .github/scripts/test_installed_gem.rb
|
|
79
|
+
|
|
80
|
+
smoke-linux-arm:
|
|
81
|
+
name: Smoke test Linux ARM64
|
|
82
|
+
needs: native
|
|
83
|
+
runs-on: ubuntu-22.04-arm
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
- uses: ruby/setup-ruby@v1
|
|
87
|
+
with:
|
|
88
|
+
ruby-version: "3.4"
|
|
89
|
+
- uses: actions/download-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: cross-gem-aarch64-linux
|
|
92
|
+
path: pkg/
|
|
93
|
+
- run: |
|
|
94
|
+
gem install pkg/expressir-*.gem --no-document
|
|
95
|
+
ruby .github/scripts/test_installed_gem.rb
|
|
96
|
+
|
|
97
|
+
smoke-macos-arm:
|
|
98
|
+
name: Smoke test macOS ARM64
|
|
99
|
+
needs: native
|
|
100
|
+
runs-on: macos-15
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v4
|
|
103
|
+
- uses: ruby/setup-ruby@v1
|
|
104
|
+
with:
|
|
105
|
+
ruby-version: "3.4"
|
|
106
|
+
- uses: actions/download-artifact@v4
|
|
107
|
+
with:
|
|
108
|
+
name: cross-gem-arm64-darwin
|
|
109
|
+
path: pkg/
|
|
110
|
+
- run: |
|
|
111
|
+
gem install pkg/expressir-*.gem --no-document
|
|
112
|
+
ruby .github/scripts/test_installed_gem.rb
|
|
113
|
+
|
|
114
|
+
smoke-macos-intel:
|
|
115
|
+
name: Smoke test macOS x86_64
|
|
116
|
+
needs: native
|
|
117
|
+
runs-on: macos-15-intel
|
|
118
|
+
steps:
|
|
119
|
+
- uses: actions/checkout@v4
|
|
120
|
+
- uses: ruby/setup-ruby@v1
|
|
121
|
+
with:
|
|
122
|
+
ruby-version: "3.4"
|
|
123
|
+
- uses: actions/download-artifact@v4
|
|
124
|
+
with:
|
|
125
|
+
name: cross-gem-x86_64-darwin
|
|
126
|
+
path: pkg/
|
|
127
|
+
- run: |
|
|
128
|
+
gem install pkg/expressir-*.gem --no-document
|
|
129
|
+
ruby .github/scripts/test_installed_gem.rb
|
|
130
|
+
|
|
131
|
+
smoke-windows:
|
|
132
|
+
name: Smoke test Windows x64
|
|
133
|
+
needs: native
|
|
134
|
+
runs-on: windows-2025
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- uses: ruby/setup-ruby@v1
|
|
138
|
+
with:
|
|
139
|
+
ruby-version: "3.4"
|
|
140
|
+
- uses: actions/download-artifact@v4
|
|
141
|
+
with:
|
|
142
|
+
name: cross-gem-x64-mingw-ucrt
|
|
143
|
+
path: pkg/
|
|
144
|
+
- run: |
|
|
145
|
+
gem install pkg/expressir-*.gem --no-document
|
|
146
|
+
ruby .github/scripts/test_installed_gem.rb
|
|
147
|
+
|
|
148
|
+
source:
|
|
149
|
+
name: Build source gem
|
|
150
|
+
runs-on: ubuntu-22.04
|
|
151
|
+
steps:
|
|
152
|
+
- uses: actions/checkout@v4
|
|
153
|
+
- uses: oxidize-rb/actions/setup-ruby-and-rust@v1
|
|
154
|
+
with:
|
|
155
|
+
ruby-version: "3.4"
|
|
156
|
+
bundler-cache: true
|
|
157
|
+
cargo-cache: true
|
|
158
|
+
- run: bundle exec rake build
|
|
159
|
+
- uses: actions/upload-artifact@v4
|
|
160
|
+
with:
|
|
161
|
+
name: source-gem
|
|
162
|
+
path: pkg/*.gem
|
|
163
|
+
if-no-files-found: error
|
|
164
|
+
|
|
165
|
+
release:
|
|
166
|
+
name: Release gems to rubygems
|
|
167
|
+
needs: [native, smoke-linux, smoke-linux-arm, smoke-macos-arm, smoke-macos-intel, smoke-windows, source]
|
|
168
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
169
|
+
runs-on: ubuntu-22.04
|
|
170
|
+
environment: release
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/download-artifact@v4
|
|
173
|
+
with:
|
|
174
|
+
path: pkg/
|
|
175
|
+
pattern: cross-gem-*
|
|
176
|
+
merge-multiple: true
|
|
177
|
+
- name: Push to rubygems
|
|
178
|
+
env:
|
|
179
|
+
GEM_HOST_API_KEY: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
|
|
180
|
+
run: |
|
|
181
|
+
for gem in pkg/expressir-*.gem; do
|
|
182
|
+
gem push "$gem"
|
|
183
|
+
done
|
data/.gitignore
CHANGED
|
@@ -48,3 +48,8 @@ CLAUDE.md
|
|
|
48
48
|
Cargo.lock
|
|
49
49
|
/ext/expressir_core/target/
|
|
50
50
|
/ext/expressir_core/*.bundle
|
|
51
|
+
|
|
52
|
+
# prebuilt binding staged by rake-compiler for platform gems
|
|
53
|
+
/lib/expressir/expressir_core.bundle
|
|
54
|
+
/lib/expressir/expressir_core.so
|
|
55
|
+
/lib/expressir/expressir_core.dll
|
data/Cargo.toml
CHANGED
data/Gemfile
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Suma improvements — integration + expressir roadmap
|
|
2
|
+
|
|
3
|
+
The metanorma ↔ expressir chain via suma: ISO 10303 SMRL schemas → expressir
|
|
4
|
+
parse (compiled set EXSCS1) → LazyRepository + Liquid drops
|
|
5
|
+
(metanorma-plugin-lutaml) → metanorma-iso schema documents. The chain is
|
|
6
|
+
proven piecewise (1307 schemas, 0 failures, warm start 7.8s, live xrefs in
|
|
7
|
+
rendered XML); the items below close the last mile and deepen expressir.
|
|
8
|
+
|
|
9
|
+
| # | item | status |
|
|
10
|
+
|---|------|--------|
|
|
11
|
+
| 01 | prebuilt platform gems (oxidize-rb cross-gem, parsanol pattern) | in flight |
|
|
12
|
+
| 02 | suma full-document E2E on current stack | pending |
|
|
13
|
+
| 03 | ItemGraph — cross-schema subtype/interface graph API | pending |
|
|
14
|
+
| 04 | compiled-set + lazy path as suma default build | pending |
|
|
15
|
+
| 05 | corpus validation surfaced in suma builds | pending |
|
|
16
|
+
| 06 | memory/perf re-measure with binding-by-default | pending |
|
|
17
|
+
| 07 | #276 Part 28 XML — select mapping v2 | pending |
|
|
18
|
+
| 08 | windows gnu toolchain for in-place ext builds | superseded by 01 for CI; kept for local dev |
|
|
19
|
+
| 09 | checker tranche 3 (INVERSE, SELF\ qualifiers) | pending |
|
|
20
|
+
| 10 | #91 EXPRESSdoc (upstream-gated) | parked |
|
|
21
|
+
|
|
22
|
+
Evidence trail: #88/#461 (refpath validation), #462/#467 (ABSTRACT body
|
|
23
|
+
form), v2.4.21–v2.4.24 (binding by default), plugin 0.7.53 + #305/#306.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 01 — Prebuilt platform gems
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
`gem install expressir` compiles nothing on machines with a prebuilt
|
|
5
|
+
platform gem: the binding binary ships inside per-platform gems
|
|
6
|
+
(`expressir-<ver>-arm64-darwin` etc.), following the parsanol-ruby
|
|
7
|
+
pattern (oxidize-rb cross-gem + RbSys::ExtensionTask).
|
|
8
|
+
|
|
9
|
+
## Pattern (from ~/src/parsanol/parsanol-ruby)
|
|
10
|
+
- `oxidize-rb/actions/fetch-ci-data@v1` computes supported platforms
|
|
11
|
+
- `oxidize-rb/actions/cross-gem@v1` matrix builds per-platform gems in
|
|
12
|
+
rake-compiler-dock containers (coherent gnu toolchain per target —
|
|
13
|
+
this is what fixes windows)
|
|
14
|
+
- per-platform smoke jobs on REAL runners installing the gem and running
|
|
15
|
+
a smoke script (NATIVE true + parse)
|
|
16
|
+
- source gem still builds via the gemspec (extensions stay declared);
|
|
17
|
+
rubygems prefers a matching platform gem automatically
|
|
18
|
+
|
|
19
|
+
## expressir implementation
|
|
20
|
+
- `rakelib/native.rake`: `RbSys::ExtensionTask.new("expressir_core")`,
|
|
21
|
+
`ext.lib_dir = "lib/expressir"`, `cross_compiling` callback stages
|
|
22
|
+
`expressir_core.{so,dylib,dll}` into `lib/expressir/` (loads via the
|
|
23
|
+
existing first `require "expressir/expressir_core"` in core.rb)
|
|
24
|
+
- extconf: mingw skip becomes bypassable (`EXPRESSIR_CORE_FORCE=1` set in
|
|
25
|
+
the cross-gem container, where cargo gnu matches the mingw-ucrt ruby)
|
|
26
|
+
- `.github/workflows/native-gems.yml`: ci-data + cross-gem matrix +
|
|
27
|
+
smoke jobs + source gem + `gem push` on tag (RUBYGEMS_API_KEY secret)
|
|
28
|
+
- `.github/scripts/test_installed_gem.rb`: smoke script
|
|
29
|
+
|
|
30
|
+
## Acceptance
|
|
31
|
+
- workflow green across platforms with smoke jobs passing
|
|
32
|
+
- a platform gem installs cargo-less and reports NATIVE true
|
|
33
|
+
- source-gem install path unchanged (2.4.21 behavior)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 02 — Suma full-document E2E on the current stack
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
One clean E2E run of an actual suma document build with expressir 2.4.24 +
|
|
5
|
+
metanorma-plugin-lutaml 0.7.53 + current metanorma gems, rendering a real
|
|
6
|
+
ISO 10303 module document to XML.
|
|
7
|
+
|
|
8
|
+
## Why now
|
|
9
|
+
The earlier full-render was blocked by the metanorma-document/relaton
|
|
10
|
+
0.2.x skew; that ecosystem has moved (metanorma-document 0.5.1, relaton
|
|
11
|
+
3.0 pre-alpha). The expressir side is proven current.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
- locate the suma build entry (~/src/mn/iso-10303 suma config or the
|
|
15
|
+
metanorma-iso document that renders schema pages)
|
|
16
|
+
- bundle with current metanorma; render one module doc end to end
|
|
17
|
+
- capture XML diffs vs the last good render; if the relaton skew
|
|
18
|
+
persists, name the exact unresolved constraint
|
|
19
|
+
|
|
20
|
+
## Acceptance
|
|
21
|
+
- a rendered module document, or a one-line statement of the precise
|
|
22
|
+
upstream blocker with the failing constraint
|
|
23
|
+
|
|
24
|
+
## Result: DONE (2026-09-27)
|
|
25
|
+
|
|
26
|
+
The upstream blocker has cleared. On the iso-10303 ladder (user's
|
|
27
|
+
Gemfile WIP + suma from path + relaton prereleases with the rubyzip-3
|
|
28
|
+
fix now on rubygems — the fix/rubyzip-3 git pins were dropped as
|
|
29
|
+
obsolete):
|
|
30
|
+
|
|
31
|
+
bundle exec suma build srl-smol.yml # one-part SMOL collection
|
|
32
|
+
# => Compiling schema collection... Compiling complete collection...
|
|
33
|
+
# => SMOL-EXIT=0
|
|
34
|
+
|
|
35
|
+
Artifacts: 286 files in _site/ — real ISO document XML (flavor iso,
|
|
36
|
+
schema content present), schema-doc pages, presentation XML. The stale
|
|
37
|
+
sums pin was the rubygems suma 0.1.11 (expressir ~> 2.1, metanorma-cli
|
|
38
|
+
0.0.3 → broken 2019 stack); suma from source (metanorma ~> 2.3 line)
|
|
39
|
+
resolves cleanly.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 03 — ItemGraph
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Cross-schema graph API over a parsed repository: subtype edges, interface
|
|
5
|
+
(USE FROM/REFERENCE FROM) edges, transitive queries — feeding suma doc
|
|
6
|
+
generation (hyperlink coverage, "used by" sections, dependency diagrams).
|
|
7
|
+
|
|
8
|
+
## Evidence
|
|
9
|
+
SMRL full-set load proved the data: 37.7k subtype edges, 5.5k interface
|
|
10
|
+
edges over 1307 schemas (TODO.memory SMRL notes). The class is the
|
|
11
|
+
documented remaining gap.
|
|
12
|
+
|
|
13
|
+
## Shape
|
|
14
|
+
- `Expressir::Model::ItemGraph` built from a Repository (or a
|
|
15
|
+
LazyRepository hydrated once)
|
|
16
|
+
- `subtypes(id)`, `supertypes(id)`, `used_by(id)`, `uses(id)`,
|
|
17
|
+
`transitive_subtypes(id)` — downcased-id keyed, cycle-safe
|
|
18
|
+
- built once per repository; O(V+E)
|
|
19
|
+
|
|
20
|
+
## Acceptance
|
|
21
|
+
- specs over a multi-schema fixture (diamond subtyping, mutual USE FROM)
|
|
22
|
+
- SMRL smoke: edge counts match the recorded 37.7k/5.5k within tolerance
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 04 — Compiled-set + lazy path as the suma default build
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
suma builds parse the SMRL once into an EXSCS1 artifact and all document
|
|
5
|
+
renders hydrate per schema from it (warm start 7.8s vs 12s cold), instead
|
|
6
|
+
of eager-parsing on every build.
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
- locate the suma/rake build entry that calls Parser.from_files
|
|
10
|
+
- switch to `Parser.from_files(files, compiled_set:)` then
|
|
11
|
+
`LazyRepository.new(set)`
|
|
12
|
+
- keep an escape hatch (env) for debugging with eager parse
|
|
13
|
+
|
|
14
|
+
## Acceptance
|
|
15
|
+
- a suma build log showing one artifact build + per-page hydration
|
|
16
|
+
- rendered output byte-identical to the eager path
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 05 — Corpus validation surfaced in suma builds
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
The expressir validation layers (mapping_validate incl. refpath
|
|
5
|
+
resolution — which found 2 genuine corpus drifts in a 60-module sweep)
|
|
6
|
+
run as part of suma builds, reporting per-module drift in the build log
|
|
7
|
+
(and optionally in published output).
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
- wire `expressir mapping_validate` over each module's mapping.yaml in
|
|
11
|
+
the suma build loop
|
|
12
|
+
- aggregate a drift report artifact; non-zero exit only on NEW drift vs
|
|
13
|
+
a committed baseline file
|
|
14
|
+
|
|
15
|
+
## Acceptance
|
|
16
|
+
- baseline for the 2 known drifts; a build with a synthetic new drift
|
|
17
|
+
fails with the module/location in the message
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# 06 — Memory/perf re-measure with binding-by-default
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
The recorded profile ("50x memory ratio not achieved", pure-Ruby parse
|
|
5
|
+
dominating) predates binding-by-default. Consumers now get the Rust
|
|
6
|
+
parse path automatically; re-measure and refresh the story.
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
- rerun the memory harness (TODO.memory methodology) with
|
|
10
|
+
NATIVE_AVAILABLE true on the same fixtures
|
|
11
|
+
- record ratio vs the 50x target; note remaining hot spots
|
|
12
|
+
- update TODO.memory notes + the perf memory file
|
|
13
|
+
|
|
14
|
+
## Acceptance
|
|
15
|
+
- fresh numbers committed alongside the old ones, with the parse
|
|
16
|
+
backend identified per measurement
|
|
17
|
+
|
|
18
|
+
## Results (2026-09-27, ruby 3.4.8, 227KB synthetic / 4000 entities)
|
|
19
|
+
|
|
20
|
+
| path | parse time | RSS delta | after GC |
|
|
21
|
+
|------|-----------|-----------|----------|
|
|
22
|
+
| Rust binding (NATIVE_AVAILABLE) | 7.7s | 348 MB | 387 MB |
|
|
23
|
+
| pure-Ruby (use_native: false) | 7.5s | 307 MB | 353 MB |
|
|
24
|
+
|
|
25
|
+
Both paths hydrate the full lutaml-model object tree, which dominates
|
|
26
|
+
the delta — the native parse itself is not the differentiator at this
|
|
27
|
+
size (arena overhead is offset by wire-hash materialization). The
|
|
28
|
+
binding's memory advantage in the field comes from the compiled-set
|
|
29
|
+
path (hydrate per file / warm start 7.8s), not from a lower per-parse
|
|
30
|
+
floor. Ratio ~1.5 MB/KB of source on both paths — consistent with the
|
|
31
|
+
older "~600x for large files" observation: the object MODEL is the
|
|
32
|
+
cost driver, which is exactly what M2 laziness addresses.
|
|
33
|
+
|
|
34
|
+
Old "50x memory ratio target" concern: RETIRED as the wrong metric for
|
|
35
|
+
the binding; per-page hydration (LazyRepository) is the delivered
|
|
36
|
+
mechanism for bounded memory at render time.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 07 — Part 28 XML: select mapping v2 (#276)
|
|
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.
|
|
7
|
+
|
|
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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 08 — Windows gnu toolchain for in-place ext builds
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Local windows source installs and windows CI in-place builds compile the
|
|
5
|
+
binding: rustup gnu target (`x86_64-pc-windows-gnu`) matching the mingw
|
|
6
|
+
ruby, so extconf's guard can be lifted there.
|
|
7
|
+
|
|
8
|
+
## Status
|
|
9
|
+
Superseded for CI by 01 (cross-gem containers carry coherent toolchains;
|
|
10
|
+
platform gems ship prebuilt). Kept for developers hacking on windows
|
|
11
|
+
checkouts directly.
|
|
12
|
+
|
|
13
|
+
## Acceptance
|
|
14
|
+
- a windows machine with the gnu target builds the ext via extconf
|
|
15
|
+
without EXPRESSIR_CORE_FORCE
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 09 — Checker tranche 3 + SHTOLO completion
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Remaining eengine-parity checks: INVERSE-family rules and SELF\ qualifier
|
|
5
|
+
validation (tranche 3), then the leftover SHTOLO longform items
|
|
6
|
+
(TODO.parity-ee/14).
|
|
7
|
+
|
|
8
|
+
## Gate
|
|
9
|
+
Grammar support for INVERSE-scoped references and SELF\ qualified
|
|
10
|
+
refs — verify against the current grammar after recent fixes.
|
|
11
|
+
|
|
12
|
+
## Acceptance
|
|
13
|
+
- each new check has a fixture that fires it + a clean-corpus negative
|
|
@@ -16,7 +16,4 @@ magnus = { version = "0.9" }
|
|
|
16
16
|
expressir-rs = { git = "https://github.com/lutaml/expressir-rs", tag = "v0.1.6" }
|
|
17
17
|
serde_json = "1.0"
|
|
18
18
|
|
|
19
|
-
# Standalone workspace: the repo root workspace owns crates/, the ext
|
|
20
|
-
# is built by rb_sys with its own cargo invocation.
|
|
21
|
-
[workspace]
|
|
22
19
|
|
|
@@ -24,7 +24,10 @@ end
|
|
|
24
24
|
# Windows rubies are mingw-built while rustup defaults to the msvc
|
|
25
25
|
# toolchain; rb-sys cannot bridge the two, so skip there too (windows
|
|
26
26
|
# installs fall back to the pure-Ruby parser).
|
|
27
|
-
|
|
27
|
+
# Local mingw rubies pair with an msvc-default cargo and cannot build;
|
|
28
|
+
# cross-gem containers carry the coherent gnu toolchain, so the skip is
|
|
29
|
+
# bypassable there (EXPRESSIR_CORE_FORCE=1).
|
|
30
|
+
if RUBY_PLATFORM.include?("mingw") && ENV["EXPRESSIR_CORE_FORCE"] != "1"
|
|
28
31
|
write_noop_makefile
|
|
29
32
|
warn "expressir: skipping the Rust core extension on mingw (msvc " \
|
|
30
33
|
"toolchain mismatch)"
|
|
@@ -17,6 +17,9 @@ module Expressir
|
|
|
17
17
|
# check-unresolved-ref (aggregate of eeng type/entity unparsed notes)
|
|
18
18
|
# check-select-extended-type
|
|
19
19
|
# check-enumeration-extended-type
|
|
20
|
+
# check-inverse-entity/-attrib-ref/-inverse/-derive (tranche 3)
|
|
21
|
+
# qualified-attrib-group-not-found/-not-ancestor,
|
|
22
|
+
# qualified-attrib-attr-not-found (tranche 3)
|
|
20
23
|
#
|
|
21
24
|
# Severity: :error stops a clean SHTOLO run; :warning is advisory.
|
|
22
25
|
class Checker
|
|
@@ -226,6 +229,8 @@ module Expressir
|
|
|
226
229
|
check_subtype_cycles(schema, entity)
|
|
227
230
|
|
|
228
231
|
check_where_rules(schema, entity, entity.where_rules)
|
|
232
|
+
check_inverse_attributes(schema, entity)
|
|
233
|
+
check_group_references(schema, entity)
|
|
229
234
|
Array(entity.unique_rules).each do |ur|
|
|
230
235
|
next if ur.id.nil? || ur.id.match?(UNIQUE_LABEL)
|
|
231
236
|
|
|
@@ -290,6 +295,193 @@ module Expressir
|
|
|
290
295
|
end
|
|
291
296
|
end
|
|
292
297
|
|
|
298
|
+
# eeng check-inverse-* (ISO 10303-11 9.2.1.4): every INVERSE
|
|
299
|
+
# attribute must name a real entity and pair with a real
|
|
300
|
+
# attribute on it — one that is neither DERIVE nor itself an
|
|
301
|
+
# INVERSE — whose domain includes the owning entity (through
|
|
302
|
+
# subtype relations and nested aggregates). check-inverse-agg
|
|
303
|
+
# (only SET/BAG may aggregate an inverse) is structurally
|
|
304
|
+
# impossible here: the grammar rejects other aggregate kinds.
|
|
305
|
+
def check_inverse_attributes(schema, entity)
|
|
306
|
+
entity.attributes.to_a
|
|
307
|
+
.grep(Model::Declarations::InverseAttribute)
|
|
308
|
+
.each { |inv| check_inverse_attribute(schema, entity, inv) }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def check_inverse_attribute(schema, entity, inv)
|
|
312
|
+
target = inverse_target(schema, inv)
|
|
313
|
+
if target.nil?
|
|
314
|
+
note!(:check_inverse_entity, :error, schema,
|
|
315
|
+
"ENTITY #{entity.id}: INVERSE '#{inv.id}' target " \
|
|
316
|
+
"'#{inverse_target_id(inv)}' is not an entity", inv)
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
attr_id = inverse_paired_attr_id(inv)
|
|
321
|
+
if attr_id.nil?
|
|
322
|
+
note!(:check_inverse_attrib_ref, :error, schema,
|
|
323
|
+
"ENTITY #{entity.id}: INVERSE '#{inv.id}' names no " \
|
|
324
|
+
"attribute after FOR", inv)
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
owner = inverse_attr_owner(schema, inv, target)
|
|
329
|
+
paired = owner && attribute_on_closure(schema, owner, attr_id)
|
|
330
|
+
if paired.nil?
|
|
331
|
+
note!(:check_inverse_attrib_ref, :error, schema,
|
|
332
|
+
"ENTITY #{entity.id}: INVERSE '#{inv.id}': entity " \
|
|
333
|
+
"'#{owner&.id || target.id}' has no attribute '#{attr_id}'", inv)
|
|
334
|
+
return
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if paired.is_a?(Model::Declarations::InverseAttribute)
|
|
338
|
+
note!(:check_inverse_inverse, :error, schema,
|
|
339
|
+
"ENTITY #{entity.id}: INVERSE '#{inv.id}' pairs with " \
|
|
340
|
+
"'#{owner.id}.#{attr_id}', which is itself an INVERSE", inv)
|
|
341
|
+
elsif paired.kind == Expressir::Model::Declarations::Attribute::DERIVED
|
|
342
|
+
note!(:check_inverse_derive, :error, schema,
|
|
343
|
+
"ENTITY #{entity.id}: INVERSE '#{inv.id}' pairs with " \
|
|
344
|
+
"'#{owner.id}.#{attr_id}', which is a DERIVE", inv)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def inverse_target(schema, inv)
|
|
349
|
+
type = inv.type
|
|
350
|
+
type = type.base_type if type.respond_to?(:base_type)
|
|
351
|
+
return nil unless type.respond_to?(:id)
|
|
352
|
+
|
|
353
|
+
find_entity(schema, ref_id(type))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def inverse_target_id(inv)
|
|
357
|
+
type = inv.type
|
|
358
|
+
type = type.base_type if type.respond_to?(:base_type)
|
|
359
|
+
ref_id(type)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# The entity the FOR-attribute lives on: the qualified
|
|
363
|
+
# `e.a FOR` form overrides the type's entity.
|
|
364
|
+
def inverse_attr_owner(schema, inv, target)
|
|
365
|
+
expr = inv.expression
|
|
366
|
+
if expr.is_a?(Model::References::AttributeReference) &&
|
|
367
|
+
expr.ref.respond_to?(:id)
|
|
368
|
+
find_entity(schema, ref_id(expr.ref)) || target
|
|
369
|
+
else
|
|
370
|
+
target
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def inverse_paired_attr_id(inv)
|
|
375
|
+
expr = inv.expression
|
|
376
|
+
return ref_id(expr) if expr.respond_to?(:id)
|
|
377
|
+
|
|
378
|
+
expr.respond_to?(:attribute) ? ref_id(expr.attribute) : nil
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Attribute on +entity+ or any of its supertype ancestors.
|
|
382
|
+
def attribute_on_closure(schema, entity, attr_id)
|
|
383
|
+
key = attr_id.safe_downcase
|
|
384
|
+
seen = {}.compare_by_identity
|
|
385
|
+
queue = [entity]
|
|
386
|
+
until queue.empty?
|
|
387
|
+
current = queue.shift
|
|
388
|
+
next if seen[current]
|
|
389
|
+
|
|
390
|
+
seen[current] = true
|
|
391
|
+
hit = Array(current.attributes).find do |a|
|
|
392
|
+
a.id&.safe_downcase == key
|
|
393
|
+
end
|
|
394
|
+
return hit if hit
|
|
395
|
+
|
|
396
|
+
Array(current.subtype_of).each do |ref|
|
|
397
|
+
id = ref_id(ref)
|
|
398
|
+
queue << find_entity(schema, id) if id
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# eeng qualified-attrib-* (ISO 10303-11 12.7.4). The group must
|
|
405
|
+
# resolve to an entity and the qualified attribute must exist on
|
|
406
|
+
# it. The ANCESTRY requirement applies only to redeclared
|
|
407
|
+
# attributes (SELF\T.attr as an attribute declaration) — eeng
|
|
408
|
+
# checks exactly those; TYPEOF-guarded sibling views in
|
|
409
|
+
# expressions (geometry_schema's SELF\subface.parent_face) are
|
|
410
|
+
# standard corpus usage and are not flagged.
|
|
411
|
+
def check_group_references(schema, entity)
|
|
412
|
+
closure = supertype_closure_ids(schema, entity)
|
|
413
|
+
closure << entity.id if entity.id
|
|
414
|
+
redeclared = {}.compare_by_identity
|
|
415
|
+
entity.attributes.to_a.each do |attr|
|
|
416
|
+
sa = attr.supertype_attribute
|
|
417
|
+
next unless sa.is_a?(Model::References::AttributeReference) &&
|
|
418
|
+
sa.ref.is_a?(Model::References::GroupReference)
|
|
419
|
+
|
|
420
|
+
redeclared[sa.ref] = true
|
|
421
|
+
group = sa.ref
|
|
422
|
+
target = resolve_group(schema, entity, group)
|
|
423
|
+
next if target.nil?
|
|
424
|
+
next if closure.include?(ref_id(group.entity)&.safe_downcase)
|
|
425
|
+
|
|
426
|
+
note!(:qualified_attrib_group_not_ancestor, :error, schema,
|
|
427
|
+
"ENTITY #{entity.id}: SELF\\'#{ref_id(group.entity)}' " \
|
|
428
|
+
"is not a supertype of #{entity.id}", group)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
each_node(entity) do |node|
|
|
432
|
+
next unless node.is_a?(Model::References::GroupReference)
|
|
433
|
+
next if redeclared[node]
|
|
434
|
+
|
|
435
|
+
target = resolve_group(schema, entity, node)
|
|
436
|
+
next if target.nil?
|
|
437
|
+
|
|
438
|
+
parent = node.parent
|
|
439
|
+
attr_id = parent.respond_to?(:attribute) ? ref_id(parent.attribute) : nil
|
|
440
|
+
next if attr_id.nil?
|
|
441
|
+
next if Array(target.attributes).any? do |a|
|
|
442
|
+
a.id&.safe_downcase == attr_id.safe_downcase
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
note!(:qualified_attrib_attr_not_found, :error, schema,
|
|
446
|
+
"ENTITY #{entity.id}: '#{ref_id(node.entity)}.#{attr_id}': " \
|
|
447
|
+
"'#{ref_id(node.entity)}' has no attribute '#{attr_id}'", node)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Resolve a group qualifier's entity, noting
|
|
452
|
+
# qualified-attrib-group-not-found when it does not resolve.
|
|
453
|
+
def resolve_group(schema, entity, node)
|
|
454
|
+
group_id = ref_id(node.entity)
|
|
455
|
+
target = group_id && find_entity(schema, group_id)
|
|
456
|
+
return target if target
|
|
457
|
+
|
|
458
|
+
note!(:qualified_attrib_group_not_found, :error, schema,
|
|
459
|
+
"ENTITY #{entity.id}: group '#{group_id}' in a qualified " \
|
|
460
|
+
"attribute does not resolve to an entity", node)
|
|
461
|
+
nil
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def supertype_closure_ids(schema, entity)
|
|
465
|
+
seen = {}.compare_by_identity
|
|
466
|
+
queue = [entity]
|
|
467
|
+
ids = []
|
|
468
|
+
until queue.empty?
|
|
469
|
+
current = queue.shift
|
|
470
|
+
next if seen[current]
|
|
471
|
+
|
|
472
|
+
seen[current] = true
|
|
473
|
+
Array(current.subtype_of).each do |ref|
|
|
474
|
+
id = ref_id(ref)
|
|
475
|
+
next if id.nil?
|
|
476
|
+
|
|
477
|
+
ids << id
|
|
478
|
+
parent = find_entity(schema, id)
|
|
479
|
+
queue << parent if parent
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
ids
|
|
483
|
+
end
|
|
484
|
+
|
|
293
485
|
def each_reference(node, &block)
|
|
294
486
|
case node
|
|
295
487
|
when Model::References::SimpleReference then yield node
|
|
@@ -160,6 +160,10 @@ module Expressir
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
repository = build_repository(all_exp_files, skip_references: skip_references)
|
|
163
|
+
if set_path && !skip_references && !File.exist?(set_path) &&
|
|
164
|
+
core_set_available? && !all_exp_files.include?(nil)
|
|
165
|
+
write_compiled_set(set_path, files, all_exp_files, root_path)
|
|
166
|
+
end
|
|
163
167
|
if set_path && !skip_references && File.exist?(set_path)
|
|
164
168
|
files = all_exp_files.compact
|
|
165
169
|
RefsOverlay.write(set_path, files)
|
|
@@ -383,6 +387,47 @@ include_source: nil, root_path: nil, use_native: nil, &block)
|
|
|
383
387
|
all_exp_files
|
|
384
388
|
end
|
|
385
389
|
|
|
390
|
+
# Artifact write for the non-batch paths (single file,
|
|
391
|
+
# EXPRESSIR_BATCH=0, small sequential sets): serializes the
|
|
392
|
+
# parsed models through Core.write_set, producing the same
|
|
393
|
+
# artifact shape the batch path writes natively. The wire is
|
|
394
|
+
# pre-remark — RemarkOverlay.write carries the remark-derived
|
|
395
|
+
# attributes, exactly as it does for the batch path.
|
|
396
|
+
def self.write_compiled_set(set_path, files, exp_files, root_path)
|
|
397
|
+
require "digest"
|
|
398
|
+
require "json"
|
|
399
|
+
schema_file_for = lambda do |file|
|
|
400
|
+
root_path ? Pathname.new(file.to_s).relative_path_from(root_path).to_s : file.to_s
|
|
401
|
+
end
|
|
402
|
+
entries = files.zip(exp_files).map do |file, exp_file|
|
|
403
|
+
[schema_file_for.call(file),
|
|
404
|
+
Digest::SHA256.file(file.to_s).hexdigest,
|
|
405
|
+
JSON.generate(strip_remark_wire(exp_file.to_hash))]
|
|
406
|
+
end
|
|
407
|
+
::Expressir::Core.write_set(set_path, entries, Expressir::Version::VERSION)
|
|
408
|
+
RemarkOverlay.write(set_path, exp_files)
|
|
409
|
+
end
|
|
410
|
+
private_class_method :write_compiled_set
|
|
411
|
+
|
|
412
|
+
# Deep copy of a serialized model without the RemarkOverlay
|
|
413
|
+
# attributes: the artifact wire carries only the parsed model;
|
|
414
|
+
# untagged remarks are plain hashes that the Rust hydrator
|
|
415
|
+
# cannot instantiate, and every remark-derived value belongs to
|
|
416
|
+
# the overlay anyway.
|
|
417
|
+
def self.strip_remark_wire(node)
|
|
418
|
+
case node
|
|
419
|
+
when Hash
|
|
420
|
+
node.each_with_object({}) do |(key, value), out|
|
|
421
|
+
out[key] = strip_remark_wire(value) unless RemarkOverlay::CAPTURED.include?(key)
|
|
422
|
+
end
|
|
423
|
+
when Array
|
|
424
|
+
node.map { |value| strip_remark_wire(value) }
|
|
425
|
+
else
|
|
426
|
+
node
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
private_class_method :strip_remark_wire
|
|
430
|
+
|
|
386
431
|
def self.build_repository(all_exp_files, skip_references: nil)
|
|
387
432
|
repository = Model::Repository.new(files: all_exp_files)
|
|
388
433
|
|
|
@@ -6,13 +6,18 @@ module Expressir
|
|
|
6
6
|
# TODO.parity-ee 15) — the express2xsd counterpart, built on
|
|
7
7
|
# moxml.
|
|
8
8
|
#
|
|
9
|
-
# Mapping (
|
|
9
|
+
# Mapping (v2):
|
|
10
10
|
# ENTITY → xs:element + xs:complexType (explicit attributes
|
|
11
11
|
# in declaration order); SUBTYPE OF a schema-local
|
|
12
12
|
# entity becomes a substitutionGroup reference.
|
|
13
13
|
# ENUMERATION → xs:simpleType with enumeration facets.
|
|
14
|
-
# SELECT → xs:
|
|
15
|
-
#
|
|
14
|
+
# SELECT → xs:complexType with an xs:choice over the member
|
|
15
|
+
# types (ISO 10303-28 select mapping): entity
|
|
16
|
+
# members reference their global element
|
|
17
|
+
# declarations, defined-type members carry their
|
|
18
|
+
# simpleType. Declared members only — extensible
|
|
19
|
+
# selects and cross-schema members keep the
|
|
20
|
+
# documented v1 fallbacks.
|
|
16
21
|
# defined TYPE over a simple type → xs:simpleType restriction.
|
|
17
22
|
# attributes → xs:element inside the complexType sequence;
|
|
18
23
|
# LIST/SET/BAG/ARRAY → maxOccurs="unbounded".
|
|
@@ -54,6 +59,10 @@ module Expressir
|
|
|
54
59
|
|
|
55
60
|
def type_node(doc, schema, type)
|
|
56
61
|
underlying = type.underlying_type
|
|
62
|
+
if underlying.is_a?(Expressir::Model::DataTypes::Select)
|
|
63
|
+
return select_type_node(doc, schema, type, underlying)
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
node = doc.create_element("xs:simpleType")
|
|
58
67
|
node.set_attributes("name" => type.id)
|
|
59
68
|
restriction = doc.create_element("xs:restriction")
|
|
@@ -71,6 +80,52 @@ module Expressir
|
|
|
71
80
|
node
|
|
72
81
|
end
|
|
73
82
|
|
|
83
|
+
# ISO 10303-28: a select type maps to a complexType choosing
|
|
84
|
+
# among its member types. An extensible select with no declared
|
|
85
|
+
# items gets an empty sequence — an empty xs:choice is not
|
|
86
|
+
# schema-valid.
|
|
87
|
+
def select_type_node(doc, schema, type, select)
|
|
88
|
+
node = doc.create_element("xs:complexType")
|
|
89
|
+
node.set_attributes("name" => type.id)
|
|
90
|
+
container = select.items.to_a.empty? ? "xs:sequence" : "xs:choice"
|
|
91
|
+
choice = doc.create_element(container)
|
|
92
|
+
node.add_child(choice)
|
|
93
|
+
select.items.to_a.each do |item|
|
|
94
|
+
choice.add_child(select_member_element(doc, schema, item))
|
|
95
|
+
end
|
|
96
|
+
node
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def select_member_element(doc, schema, item)
|
|
100
|
+
node = doc.create_element("xs:element")
|
|
101
|
+
if item.is_a?(Expressir::Model::References::SimpleReference)
|
|
102
|
+
id = item.respond_to?(:base_path) && item.base_path ? base_id(item.base_path) : item.id
|
|
103
|
+
member = select_member_target(schema, id)
|
|
104
|
+
if member == :entity
|
|
105
|
+
node.set_attributes("ref" => id)
|
|
106
|
+
elsif member == :type
|
|
107
|
+
node.set_attributes("name" => id, "type" => id)
|
|
108
|
+
else
|
|
109
|
+
node.set_attributes("name" => id, "type" => "xs:anyType")
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
name = item.class.name.split("::").last.downcase
|
|
113
|
+
node.set_attributes("name" => name,
|
|
114
|
+
"type" => BUILTIN_TYPES.fetch(name, "xs:anyType"))
|
|
115
|
+
end
|
|
116
|
+
node
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def select_member_target(schema, id)
|
|
120
|
+
if schema.entities.to_a.any? { |e| e.id.safe_downcase == id.safe_downcase }
|
|
121
|
+
:entity
|
|
122
|
+
elsif schema.types.to_a.any? { |t| t.id.safe_downcase == id.safe_downcase }
|
|
123
|
+
:type
|
|
124
|
+
else
|
|
125
|
+
:unknown
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
74
129
|
def entity_element(doc, schema, entity)
|
|
75
130
|
node = doc.create_element("xs:element")
|
|
76
131
|
node.set_attributes("name" => entity.id)
|
|
@@ -83,18 +138,84 @@ module Expressir
|
|
|
83
138
|
def entity_type(doc, schema, entity)
|
|
84
139
|
node = doc.create_element("xs:complexType")
|
|
85
140
|
node.set_attributes("name" => entity.id)
|
|
141
|
+
container = node
|
|
142
|
+
parent = supertype_name(schema, entity)
|
|
143
|
+
if parent
|
|
144
|
+
content = doc.create_element("xs:complexContent")
|
|
145
|
+
extension = doc.create_element("xs:extension")
|
|
146
|
+
extension.set_attributes("base" => parent)
|
|
147
|
+
content.add_child(extension)
|
|
148
|
+
node.add_child(content)
|
|
149
|
+
container = extension
|
|
150
|
+
end
|
|
86
151
|
sequence = doc.create_element("xs:sequence")
|
|
87
|
-
|
|
88
|
-
entity.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
152
|
+
container.add_child(sequence)
|
|
153
|
+
explicit_attributes(entity).each do |attr|
|
|
154
|
+
sequence.add_child(attribute_element(doc, schema, attr))
|
|
155
|
+
end
|
|
156
|
+
# EXPRESS multiple inheritance: the extension base carries the
|
|
157
|
+
# first parent's chain; later parents' attributes (transitively)
|
|
158
|
+
# become explicit members of the subtype's sequence.
|
|
159
|
+
additional_parent_attributes(schema, entity).each do |attr|
|
|
93
160
|
sequence.add_child(attribute_element(doc, schema, attr))
|
|
94
161
|
end
|
|
95
162
|
node
|
|
96
163
|
end
|
|
97
164
|
|
|
165
|
+
def explicit_attributes(entity)
|
|
166
|
+
entity.attributes.to_a.reject do |attr|
|
|
167
|
+
attr.kind == Expressir::Model::Declarations::Attribute::INVERSE ||
|
|
168
|
+
(attr.respond_to?(:derive?) && attr.derive?)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Attributes contributed by multiple inheritance: every ancestor
|
|
173
|
+
# reachable from the additional local supertypes (all but the
|
|
174
|
+
# first), minus the first supertype's own chain — the extension
|
|
175
|
+
# base already carries that. Cycle-safe.
|
|
176
|
+
def additional_parent_attributes(schema, entity)
|
|
177
|
+
parents = local_supertypes(schema, entity)
|
|
178
|
+
return [] if parents.size <= 1
|
|
179
|
+
|
|
180
|
+
base_chain = ancestor_set(schema, parents.first)
|
|
181
|
+
seen = {}.compare_by_identity
|
|
182
|
+
queue = parents.drop(1)
|
|
183
|
+
attrs = []
|
|
184
|
+
until queue.empty?
|
|
185
|
+
current = queue.shift
|
|
186
|
+
next if seen[current] || base_chain[current]
|
|
187
|
+
|
|
188
|
+
seen[current] = true
|
|
189
|
+
attrs.concat(explicit_attributes(current))
|
|
190
|
+
queue.concat(local_supertypes(schema, current))
|
|
191
|
+
end
|
|
192
|
+
attrs
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Identity set of +entity+ and every supertype ancestor.
|
|
196
|
+
def ancestor_set(schema, entity)
|
|
197
|
+
seen = {}.compare_by_identity
|
|
198
|
+
queue = [entity]
|
|
199
|
+
until queue.empty?
|
|
200
|
+
current = queue.shift
|
|
201
|
+
next if seen[current]
|
|
202
|
+
|
|
203
|
+
seen[current] = true
|
|
204
|
+
queue.concat(local_supertypes(schema, current))
|
|
205
|
+
end
|
|
206
|
+
seen
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def local_supertypes(schema, entity)
|
|
210
|
+
Array(entity.subtype_of).filter_map do |ref|
|
|
211
|
+
id = ref.is_a?(String) ? ref : ref.id
|
|
212
|
+
next nil unless id
|
|
213
|
+
|
|
214
|
+
schema.entities.to_a
|
|
215
|
+
.find { |e| e.id.safe_downcase == id.safe_downcase }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
98
219
|
def attribute_element(doc, schema, attr)
|
|
99
220
|
node = doc.create_element("xs:element")
|
|
100
221
|
node.set_attributes("name" => attr.id)
|
|
@@ -126,14 +247,7 @@ module Expressir
|
|
|
126
247
|
end
|
|
127
248
|
|
|
128
249
|
def supertype_name(schema, entity)
|
|
129
|
-
|
|
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
|
|
250
|
+
local_supertypes(schema, entity).first&.id
|
|
137
251
|
end
|
|
138
252
|
|
|
139
253
|
def base_id(base_path)
|
|
@@ -80,6 +80,56 @@ module Expressir
|
|
|
80
80
|
@subtype_of.flat_map { |child, parents| parents.map { |p| [child, p] } }
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Direct subtypes of an entity (the inverse of subtype_of).
|
|
84
|
+
def subtypes(path)
|
|
85
|
+
key = canonical(path)
|
|
86
|
+
return [] unless @nodes.key?(key)
|
|
87
|
+
|
|
88
|
+
@subtype_of.select { |_child, parents| parents.include?(key) }.keys
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Transitive subtype closure of an entity (everything below it
|
|
92
|
+
# in the hierarchy), breadth-first and cycle-safe.
|
|
93
|
+
def transitive_subtypes(path)
|
|
94
|
+
key = canonical(path)
|
|
95
|
+
return [] unless @nodes.key?(key)
|
|
96
|
+
|
|
97
|
+
seen = {}
|
|
98
|
+
queue = subtypes(key)
|
|
99
|
+
until queue.empty?
|
|
100
|
+
current = queue.shift
|
|
101
|
+
next if seen[current]
|
|
102
|
+
|
|
103
|
+
seen[current] = true
|
|
104
|
+
queue.concat(subtypes(current))
|
|
105
|
+
end
|
|
106
|
+
seen.keys
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Transitive interface closure of a schema — every schema it
|
|
110
|
+
# depends on, directly or through intermediates. Cycle-safe.
|
|
111
|
+
def dependency_closure(schema)
|
|
112
|
+
schema_id = schema.respond_to?(:id) ? schema.id : schema.to_s
|
|
113
|
+
seen = {}
|
|
114
|
+
queue = dependencies_of(schema_id).dup
|
|
115
|
+
until queue.empty?
|
|
116
|
+
current = queue.shift
|
|
117
|
+
next if seen[current]
|
|
118
|
+
|
|
119
|
+
seen[current] = true
|
|
120
|
+
queue.concat(@dependencies[current] || [])
|
|
121
|
+
end
|
|
122
|
+
seen.keys
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Schemas that import `schema` (USE FROM / REFERENCE FROM it),
|
|
126
|
+
# direct only — the "used by" direction for doc generation.
|
|
127
|
+
def dependents_of(schema)
|
|
128
|
+
schema_id = schema.respond_to?(:id) ? schema.id : schema.to_s
|
|
129
|
+
key = schema_id.safe_downcase
|
|
130
|
+
@dependencies.select { |_from, deps| deps.include?(key) }.keys
|
|
131
|
+
end
|
|
132
|
+
|
|
83
133
|
def cross_schema_subtype_edges
|
|
84
134
|
subtype_edges.count { |child, parent| child.split(".")[0] != parent.split(".")[0] }
|
|
85
135
|
end
|
data/lib/expressir/version.rb
CHANGED
data/rakelib/native.rake
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Prebuilt platform gems (TODO.suma-improvements/01): the compiled Rust
|
|
4
|
+
# core ships inside per-platform gems, mirroring the parsanol-ruby
|
|
5
|
+
# pattern (RbSys::ExtensionTask + cross_compiling staging). The source
|
|
6
|
+
# gem keeps its gemspec-declared extconf build; platform gems carry the
|
|
7
|
+
# artifact in lib/expressir/, which core.rb's first require finds.
|
|
8
|
+
begin
|
|
9
|
+
require "rb_sys/extensiontask"
|
|
10
|
+
require "rb_sys/toolchain_info"
|
|
11
|
+
|
|
12
|
+
gemspec = Gem::Specification.load("expressir.gemspec")
|
|
13
|
+
|
|
14
|
+
RbSys::ExtensionTask.new("expressir_core", gemspec) do |ext|
|
|
15
|
+
ext.lib_dir = "lib/expressir"
|
|
16
|
+
|
|
17
|
+
ext.cross_compiling do |spec|
|
|
18
|
+
next if spec.platform == Gem::Platform::RUBY
|
|
19
|
+
|
|
20
|
+
plat = spec.platform.to_s
|
|
21
|
+
triple = begin
|
|
22
|
+
RbSys::ToolchainInfo::DATA.fetch(plat).fetch("rust-target")
|
|
23
|
+
rescue KeyError
|
|
24
|
+
{ "arm-linux-musl" => "arm-unknown-linux-musleabihf" }.fetch(plat)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rb-sys's cargo build already ran for the cross target; stage the
|
|
28
|
+
# artifact from the tree (rake-compiler stages callback-added files).
|
|
29
|
+
# rb_sys uses a per-package target dir unless the crate is a
|
|
30
|
+
# workspace member (then the root target/ holds artifacts) — glob
|
|
31
|
+
# both layouts.
|
|
32
|
+
art = Dir["target/#{triple}/release/expressir_core.{so,dylib,dll}",
|
|
33
|
+
"target/#{triple}/release/libexpressir_core.{so,dylib}",
|
|
34
|
+
"ext/expressir_core/target/#{triple}/release/expressir_core.{so,dylib,dll}",
|
|
35
|
+
"ext/expressir_core/target/#{triple}/release/libexpressir_core.{so,dylib}"].first
|
|
36
|
+
raise "binding artifact not found for #{plat} (#{triple})" unless art
|
|
37
|
+
|
|
38
|
+
cp art, "lib/expressir/"
|
|
39
|
+
spec.files << "lib/expressir/#{File.basename(art)}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
rescue LoadError
|
|
43
|
+
# rake-compiler/rb_sys not installed (consumer checkouts); the native
|
|
44
|
+
# gem tasks are a maintainer/CI concern only.
|
|
45
|
+
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.
|
|
4
|
+
version: 2.4.25
|
|
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-
|
|
11
|
+
date: 2026-09-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -251,9 +251,11 @@ extensions:
|
|
|
251
251
|
extra_rdoc_files: []
|
|
252
252
|
files:
|
|
253
253
|
- ".cargo/config.toml"
|
|
254
|
+
- ".github/scripts/test_installed_gem.rb"
|
|
254
255
|
- ".github/workflows/codeql.yml"
|
|
255
256
|
- ".github/workflows/docs.yml"
|
|
256
257
|
- ".github/workflows/links.yml"
|
|
258
|
+
- ".github/workflows/native-gems.yml"
|
|
257
259
|
- ".github/workflows/rake.yml"
|
|
258
260
|
- ".github/workflows/release.yml"
|
|
259
261
|
- ".github/workflows/rust-ext.yml"
|
|
@@ -336,6 +338,17 @@ files:
|
|
|
336
338
|
- TODO.parity-ee/18-mim-mapping.md
|
|
337
339
|
- TODO.parity-ee/19-architecture.md
|
|
338
340
|
- TODO.parity-ee/parity-matrix.md
|
|
341
|
+
- TODO.suma-improvements/00-overview.md
|
|
342
|
+
- TODO.suma-improvements/01-prebuilt-platform-gems.md
|
|
343
|
+
- TODO.suma-improvements/02-suma-e2e.md
|
|
344
|
+
- TODO.suma-improvements/03-item-graph.md
|
|
345
|
+
- TODO.suma-improvements/04-compiled-set-default.md
|
|
346
|
+
- TODO.suma-improvements/05-validation-in-docs.md
|
|
347
|
+
- TODO.suma-improvements/06-memory-remeasure.md
|
|
348
|
+
- TODO.suma-improvements/07-part28-xml.md
|
|
349
|
+
- TODO.suma-improvements/08-windows-gnu-toolchain.md
|
|
350
|
+
- TODO.suma-improvements/09-checker-tranche3.md
|
|
351
|
+
- TODO.suma-improvements/10-expressdoc.md
|
|
339
352
|
- benchmark/srl_benchmark.rb
|
|
340
353
|
- benchmark/srl_native_benchmark.rb
|
|
341
354
|
- benchmark/srl_ruby_benchmark.rb
|
|
@@ -657,6 +670,7 @@ files:
|
|
|
657
670
|
- lib/expressir/schema_manifest_entry.rb
|
|
658
671
|
- lib/expressir/version.rb
|
|
659
672
|
- lib/tasks/verify_remarks.rake
|
|
673
|
+
- rakelib/native.rake
|
|
660
674
|
- rakelib/oracle.rake
|
|
661
675
|
- rakelib/verify_remarks.rake
|
|
662
676
|
homepage: https://github.com/lutaml/expressir
|