LeanIX Converter¶
Converts a LeanIX fact sheet export to RDF, typed in the published
SAP LeanIX Meta Model v4 ontology (lmm: prefix) — or in the
v3 ontology (lmm3:) under
--meta-model v3, for a workspace that has not migrated.
The input is a file, not a workspace. Fetching is a separate tool — leanix-pull —
and the separation is deliberate:
| Different failure modes | A pull fails on credentials, rate limits and a workspace someone reshaped. A conversion fails on the data. Bundled together, a token that expired at 03:00 looks like a broken graph. |
| Different cadence | The workspace is authoritative and changes continuously; the graph is published on a schedule. A file between them is where that difference is allowed to exist. |
| Reproducibility | The same export gives the same graph today and in six months. A converter that called the API could never say that. |
| Reviewability | The export is committed, so a change to the inventory arrives as a diff somebody can read, before it becomes triples. |
graph LR
W[LeanIX workspace] -->|GraphQL: fact sheets| P[leanix-pull pull]
W -->|REST: diagrams| D[leanix-pull pull-diagrams]
P -->|factsheets.json| G[(git)]
D -->|diagrams.json| G
G --> C[leanix2linkedarchi]
C -->|TriG| R[(graph)]
Two pulls, because a workspace holds two kinds of thing. The inventory becomes elements and relationships; the diagrams become views. An inventory export is not a view and never becomes one — see Views.
Ontology¶
| Document | Namespace | Prefix |
|---|---|---|
| Ontology v4 | https://meta.linked.archi/leanix/onto# |
lmm |
| Ontology v3 | https://meta.linked.archi/leanix/v3/onto# |
lmm3 |
| SHACL shapes | https://meta.linked.archi/leanix/shapes# |
lmmsh |
| Metamodel manifest (v4) | https://meta.linked.archi/leanix/metamodel# |
lmmmm |
| Metamodel manifest (v3) | https://meta.linked.archi/leanix/v3/metamodel# |
lmm3mm |
| Viewpoints | https://meta.linked.archi/leanix/viewpoints# |
lmmvp |
| Architecture processes | https://meta.linked.archi/arch-processes# |
ap |
ap: is not a LeanIX document. The LeanIX ontology reuses its lifecycle stage vocabulary rather than
minting one, so ap: terms appear in this converter's output and its document has to be loaded to validate
against them.
Every fact sheet class is a subclass of lmm:FactSheet, which is a subclass of arch:Element, so
notation-agnostic queries work as they do for every other converter. A converted model declares
arch:modelConformsToMetamodel lmmmm:LeanIXv4 — or lmm3mm:LeanIXv3 under --meta-model v3.
| LeanIX fact sheet type | Ontology class |
|---|---|
| Objective, Initiative | lmm:Objective, lmm:Initiative |
| Business Capability | lmm:BusinessCapability |
| Organization (→ Organization Unit) | lmm:Organization, lmm:OrganizationUnit |
| Business Context (→ Process, Product, Value Stream) | lmm:BusinessContext, lmm:Process, lmm:Product, lmm:ValueStream |
| Application (→ Business Application, Deployment, Microservice) | lmm:Application, lmm:BusinessApplication, lmm:Deployment, lmm:Microservice |
| Interface, Data Object | lmm:Interface, lmm:DataObject, |
| IT Component, Tech Category, Platform, Provider | lmm:ITComponent, lmm:TechCategory, lmm:Platform, lmm:Provider |
| System (optional type) | lmm:System |
The metamodel is configured per workspace¶
This is the one thing LeanIX does that no other source here does: fact sheet types can be renamed, added and removed per tenant, their fields are defined per tenant, and a relation is a field whose name is derived from the type pair. The published ontology describes the out-of-the-box metamodel, which is what most workspaces still resemble — but not all of them.
So the converter keys on the name your workspace uses, and when that name is not one the ontology declares it does not invent a class:
[WARN] factsheets.json: 'ProductFamily' is not a fact sheet type the published LeanIX v4 ontology
declares, so its fact sheets are typed lmm:FactSheet only, with the name kept on dct:type. If it is a
renamed or custom type, map it with --type-mapping under 'elements:' keyed 'ProductFamily'.
Minting lmm:ProductFamily would put an undeclared term inside a published namespace this converter reads
rather than owns — a term a reader cannot dereference, and one a future upstream release could give another
meaning. lmm:FactSheet is kept because it is true, published, and enough for the naming shape to apply,
and --type-mapping is how the type gets a real class. See
Mapping a renamed or custom type.
Subtypes¶
A subtype is not a fact sheet type, and this catches everyone once. LeanIX models a Business Application
as an Application fact sheet whose category is businessApplication — not as a BusinessApplication
fact sheet type. The API's own subtype filter says as much: it is a category facet beside the
FactSheetTypes facet, and --type BusinessApplication matches nothing because no such type exists.
Three consequences, in the order they bite:
- The pull has to ask.
typealways comes back as the parent, so the subtype has to be requested explicitly withsubType:on the type in the pull configuration. Without it there is nothing in the export to convert, and nothing warns — no field was requested, so no request failed. - It is per type, not a base field.
categoryis not onBaseFactSheet, so asking for it on a type that does not declare it is a GraphQL error that fails the whole query for that type.leanix-pull introspectreads the real schema and writessubType:wherever the field is actually there, which is the reliable way to fill these in. - The built-in defaults claim it for
ITComponentonly. That is the one type whose subtypes —software,hardware,service— are in the standard metamodel. Application's three are optional upstream and off by default, so a workspace that has not added them may have nocategoryonApplicationat all.
types:
- name: ITComponent
subType: category
- name: Application
subType: category # only once print-query or introspect has confirmed the field
Subtypes become classes, alongside the parent¶
A subtype is a statement of kind, so it reaches rdf:type and not a string. The published ontology
already declares nine of them as subclasses — lmm:BusinessApplication rdfs:subClassOf lmm:Application —
and the converter emits both classes:
<…/element/a100…> a arch:Element, arch:ModelConcept,
lmm:FactSheet, lmm:Application, lmm:BusinessApplication ;
skos:prefLabel "Ledger"@en ;
dct:type "Application" ;
vocab:subType "businessApplication" .
Both, not just the narrower one, so a query for every lmm:Application still finds this fact sheet with no
reasoner in the loop. The subclass axiom entails the parent anyway; asserting it means the distinction is
free to adopt rather than a breaking change.
The raw token stays on vocab:subType under --ns-vocab, for the same reason the type token stays on
dct:type: the class is this converter's reading of the token, the literal is what the workspace actually
said. It is not a second dct:type — two values under one predicate with nothing to tell them apart is a
collision this project has unpicked once already — and not schema:additionalType, which schema.org declares
rdfs:subPropertyOf rdf:type, so a literal there would entail a literal in rdf:type position.
Custom subtypes: is_a or property¶
A subtype the ontology does not declare — one your workspace added, or software and its two siblings, which
LeanIX ships and lmm: has no class for — resolves in this order:
| Route | How | Result |
|---|---|---|
| Qualified mapping | elements: keyed ITComponent/software |
your class |
| Bare mapping | elements: keyed software or Software |
your class, under any type |
| Published class | nothing to do | lmm:BusinessApplication and the other eight |
| Minted class | --ns-vocab |
vocab:Software |
| None | — | parent type only, reported once |
Prefer the qualified key. A bare token is not unique across types: service is an ITComponent subtype in
the standard metamodel, and nothing stops a workspace using the same word elsewhere meaning something else.
A minted name is capitalised — token externalPartner becomes vocab:ExternalPartner — because a class name
is not a field value. The token itself stays exactly as the workspace spells it on vocab:subType.
When the type itself is renamed¶
The name on the left of the slash is the one your workspace uses, never the class you mapped it to. The
two resolutions are independent: mapping a type does not change the key its subtypes are looked up under. So
a workspace with UserGroup where the standard metamodel has Organization keys its subtypes
UserGroup/…:
elements:
UserGroup: https://meta.linked.archi/leanix/onto#Organization # the type
UserGroup/team: https://meta.linked.archi/leanix/onto#OrganizationUnit # its subtype ✓
# Organization/team: … # ✗ matches nothing
businessUnit: https://example.org/house#BusinessUnit # bare — any type
ug-1 a lmm:FactSheet, lmm:Organization, lmm:OrganizationUnit ; vocab:subType "team" .
ug-2 a lmm:FactSheet, lmm:Organization, house:BusinessUnit ; vocab:subType "businessUnit" .
ug-3 a lmm:FactSheet, lmm:Organization, vocab:ExternalPartner ; vocab:subType "externalPartner" .
The parent class comes from the type's own entry and the subtype's is added beside it, exactly as it is for a
type the ontology declares. Organization/team is easy to reach for and is not silent — it falls to the last
row of the table above, and the warning names the key that would have worked.
Every subtype of a renamed type needs one of the routes, because the published ontology declares classes
under the standard names only. --ns-vocab alone is a reasonable answer where there are many and no strong
opinion about each: every one gets a class in a namespace you own, with no mapping file to maintain, and the
ones that matter can be mapped individually later.
The last row is the only lossy one, and it needs both a missing mapping and no --ns-vocab:
[WARN] factsheets.json: 'ITComponent' fact sheets with subtype 'software' reach no class: the published
LeanIX v4 ontology declares no lmm:Software, and nothing mapped it. The token is kept, but it is a string
rather than a type. Map it with --type-mapping under 'elements:' keyed 'ITComponent/software' — or keyed
'software' to map it wherever it appears — or pass --ns-vocab <your namespace> to have a class minted for
it there.
mapping-report lists every subtype in an export and which route it takes, before
anything is converted, so this is answerable in CI rather than after the fact.
Meta Model v3 and v4¶
Both LeanIX meta models are live and both are published, so the version is an input rather than an assumption:
--meta-model v4 (default) |
--meta-model v3 |
|
|---|---|---|
| Fact sheet classes | lmm: — 20 classes |
lmm3: — 12 classes |
| Root class on every fact sheet | lmm:FactSheet |
lmm3:FactSheet |
arch:modelConformsToMetamodel |
lmmmm:LeanIXv4 |
lmm3mm:LeanIXv3 |
| Relationship classes | the sixteen lmm: relationships |
none — v3 publishes none |
| Attribute terms | lmm: |
lmm:, unvalidated — see below |
v3 has UserGroup, Project, Process and TechPlatform, the four types v4 replaced. They are renames
with changed semantics rather than aliases — UserGroup became Organization, Project became
Initiative, Process became a BusinessContext subtype, TechPlatform became Platform — so nothing is
mapped across. Process and Product exist in both documents with different definitions, which is why
the namespace is chosen once for the run rather than inferred per type.
Pointing the converter at the wrong version says so by name, rather than producing a pile of "unknown type" warnings that read as a defect in the export:
[WARN] factsheets.json: 'UserGroup' is not a fact sheet type the published LeanIX v4 ontology declares, so
its fact sheets are typed lmm:FactSheet only, with the name kept on dct:type. It is a v3 fact sheet type, so
this export probably belongs to Meta Model v3 — re-run with --meta-model v3.
No relationship class under v3. The v3 manifest is deliberately thin: fact sheet classes only, no
relationships, no attributes, no viewpoints. The v4 relationship classes are not borrowed, because every
endpoint shape names v4 classes at both ends — an lmm:Requiring between two lmm3:Applications would be a
validation violation, not a richer graph. So a v3 relationship carries arch:QualifiedRelationship, the
workspace's own field names on dct:type, and core's generic arch:hasQualifiedRelationship as the route in.
The run says this once:
[WARN] Meta Model v3 publishes no relationships, so every relationship in this run carries
arch:QualifiedRelationship, the workspace's own field name on dct:type, and no LeanIX class. That is the
whole of what v3 declares. Use --type-mapping under 'relationships:' to point them at a vocabulary you have
chosen.
Two things still work as they do under v4, and both matter more than they look:
- The two records LeanIX writes per edge still collapse to one relationship. The v4 relationship table is used as a de-duplication and direction oracle whatever the meta model — it asserts nothing, and ignoring it would double every edge. The relationship IRI keeps the published name for the same reason, so an edge holds the same IRI across a v3 → v4 migration: the one part of the graph that need not move when a workspace upgrades.
skos:broaderstill comes out of the hierarchy, since it is a SKOS statement rather than a LeanIX term.
Attributes still use the lmm: terms. Status, completion, tags, subscriptions and lifecycle are platform
features rather than meta model versions, and lmm: is the only place they are declared. They attach through
arch:domainIncludes, which entails no type, so an lmm:completion on an lmm3:Application does not quietly
make it a v4 fact sheet. They go unvalidated under v3, though: LeanIXFactSheetAttributeShape targets
lmm:FactSheet only, and says so. Labels are validated, because LeanIXFactSheetLabelShape targets
lmm3:FactSheet as well — which is what makes a v3 export a supported input rather than an archived one.
--type-mapping remains the escape hatch, and is the natural tool for a workspace mid-migration: name the v4
class for each type that has already been reworked, and the v3 root stays underneath it.
Identity¶
A fact sheet's IRI is minted from its LeanIX id, which is a UUID:
This is the one place LeanIX is easier than the other sources. The id is stable across renames, so an element IRI survives a fact sheet being retitled — where a Backstage catalog has only names, and a rename moves a published IRI. Names are labels here and nothing else.
One export is one model, so the model id is the IRI namespace and the named graphs of every fact sheet in it. Splitting an export per fact sheet type would put the two ends of a relation in different namespaces and turn every internal reference into a dangling one.
Which way a relation points¶
A LeanIX relation is a field on both fact sheets it joins, under two different names. An application's
link to a component is relApplicationToITComponent; the same link read from the component is
relITComponentToApplication. An export scoped to both types therefore contains the edge twice, and
emitting what was read would produce two relationships for one edge, pointing opposite ways.
The ontology decides. Each of the sixteen published relationships declares arch:domainIncludes and
arch:rangeIncludes, so the direction is a modelling decision made upstream rather than a tie-break:
| Unqualified property | Qualified class | Qualified property | Direction |
|---|---|---|---|
lmm:supports |
lmm:Supporting |
lmm:qualifiedSupports |
Application → BusinessCapability |
lmm:requires |
lmm:Requiring |
lmm:qualifiedRequires |
Application → ITComponent |
lmm:hasInterface |
lmm:InterfaceOwnership |
lmm:qualifiedHasInterface |
Application → Interface |
lmm:usesData |
lmm:DataUsage |
lmm:qualifiedUsesData |
Application or Interface → DataObject |
lmm:impacts |
lmm:Impact |
lmm:qualifiedImpacts |
Initiative → Application or Platform |
lmm:drives |
lmm:Driving |
lmm:qualifiedDrives |
Objective → Initiative |
lmm:usedByOrg |
lmm:OrganizationalUsage |
lmm:qualifiedUsedByOrg |
Organization → Application |
lmm:providedBy |
lmm:Provision |
lmm:qualifiedProvidedBy |
ITComponent → Provider |
lmm:consumesInterface |
lmm:InterfaceConsumption |
lmm:qualifiedConsumesInterface |
Application → Interface |
lmm:categorizedBy |
lmm:Categorization |
lmm:qualifiedCategorizedBy |
ITComponent → TechCategory |
lmm:parentOf |
lmm:FactSheetHierarchy |
lmm:qualifiedParentOf |
FactSheet → FactSheet |
lmm:partOfPlatform |
lmm:PlatformMembership |
lmm:qualifiedPartOfPlatform |
Application or BusinessCapability or ITComponent → Platform |
lmm:supportsContext |
lmm:ContextSupport |
lmm:qualifiedSupportsContext |
Application → BusinessContext |
lmm:contextRealizes |
lmm:ContextRealization |
lmm:qualifiedContextRealizes |
BusinessContext → BusinessCapability |
lmm:targetsCapability |
lmm:CapabilityTargeting |
lmm:qualifiedTargetsCapability |
Objective → BusinessCapability |
lmm:composedOf |
lmm:SystemComposition |
lmm:qualifiedComposedOf |
System → Application or ITComponent |
Because the mapping is semantic rather than name-derived, it also collapses pairs whose names look
unrelated. relApplicationToInterface and relInterfaceToProviderApplication are one ownership edge, and
both resolve to lmm:InterfaceOwnership pointing Application → Interface.
Each relationship gets the three representations
core documents: the resource, the qualified
predicate from the source to it, and — under --emit-direct-rel-triples — the direct triple, bridged back
with rdf:reifies.
<…/relationship/requires--a1000000-…-000000000001--c1000000-…-000000000001>
a arch:QualifiedRelationship, arch:ModelConcept, lmm:Requiring ;
arch:source <…/element/a1000000-…-000000000001> ;
arch:target <…/element/c1000000-…-000000000001> ;
dct:type "relApplicationToITComponent", "relITComponentToApplication" ;
skos:notation "r-app1-itc1", "r-itc1-app1" ;
schema:validFrom "2021-04-01"^^xsd:date ;
schema:validThrough "2027-12-31"^^xsd:date .
<…/element/a1000000-…-000000000001>
lmm:qualifiedRequires <…/relationship/requires--a1000000-…-000000000001--c1000000-…-000000000001> .
dct:type carries the field names the workspace actually used, one per side the edge was read from — not the
canonical name, which is a published class name the workspace may never use. skos:notation carries each
LeanIX relation id, so either workspace record can be found again.
The relationship IRI is content-addressed from the canonical relation name and the two fact sheet ids, not from a LeanIX relation id: LeanIX gives each side its own record with its own id, so an id-derived IRI would mint two resources for one edge. Same reasoning as ADR 0005.
Relations the ontology does not declare¶
Every relationship the documented out-of-the-box metamodel has is now published, so this path is reached only
by a relation a workspace invented. The two sides still merge, by a fallback rule: a field name of the
form rel<A>To<B> has a derivable mirror rel<B>To<A>, and the lexicographically smaller of the two is
canonical. Arbitrary, and deliberately so — with no ontology to consult there is no right direction, and
what a graph needs is the same direction every time. The rule reads one field name and never what else the
export contains, because choosing among the names actually observed would mean that widening a pull's scope
silently moved every published relationship IRI.
The relationship resource then carries arch:QualifiedRelationship with no LeanIX subclass, reached by
core's generic arch:hasQualifiedRelationship, and the run names the key to map it under:
[WARN] relation 'relApplicationToRiskAssessment' is not one the published LeanIX v4 ontology declares, so
the relationship carries arch:QualifiedRelationship and no LeanIX class. Map it with --type-mapping under
'relationships:' keyed 'relApplicationToRiskAssessment'.
Retyping a relation into another vocabulary¶
Case 3 of the type mapping — LeanIX output typed in ArchiMate, so it merges with ArchiMate models — needs
predicates: and qualifiedPredicates: entries alongside each relationships: entry, not just the class.
A relationships: entry replaces the published class. Once relApplicationToITComponent is an
am:Serving rather than an lmm:Requiring, lmm:qualifiedRequires no longer describes it: that predicate
declares rdfs:range lmm:Requiring. Because rdfs:range is an entailment rather than a constraint, reusing
it would have a reasoner infer lmm:Requiring back onto the resource and silently undo the retyping. So the
converter withholds the published predicate and says so:
[WARN] relation 'relApplicationToITComponent' is retyped by 'relationships:' to <…archimate3/onto#Serving>,
but 'qualifiedPredicates:' names no predicate for it — so it carries arch:hasQualifiedRelationship. The
published LeanIX predicate is not used here: its rdfs:range is the LeanIX class the retyping replaced, and a
reasoner would infer that class back onto the relationship. Add the qualified form of
<…archimate3/onto#Serving> under 'qualifiedPredicates:' keyed 'Requiring'.
The relationship stays reachable on arch:hasQualifiedRelationship, so nothing is orphaned — it just no
longer says of what kind on the way in. The direct triple is dropped rather than substituted, since there is
no generic core predicate to stand in for one; the resource still carries the statement.
ArchiMate publishes a qualified form for every relationship class, so completing the mapping is naming existing terms:
relationships:
relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#Serving
predicates:
relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#serves
qualifiedPredicates:
relApplicationToITComponent: https://meta.linked.archi/archimate3/onto#qualifiedServes
playground/config/type-mapping-leanix.yml carries the full set for the sixteen relations it retypes.
The hierarchy¶
relToParent / relToChild are LeanIX's only structural relation and are two ends of one edge. They
resolve to lmm:FactSheetHierarchy, running parent → child — named for what LeanIX calls it rather than
"Composition", which in a merged graph would invite reading it as ArchiMate's composition, a different
relationship with whole-part semantics this one does not assert.
skos:broader goes out alongside it, because a capability breakdown is also a taxonomy:
<…/relationship/parentOf--bc-1--bc-2>
a arch:QualifiedRelationship, arch:ModelConcept, lmm:FactSheetHierarchy ;
arch:source <…/element/bc-1> ; # parent
arch:target <…/element/bc-2> ; # child
dct:type "relToChild", "relToParent" .
<…/element/bc-1> lmm:qualifiedParentOf <…/relationship/parentOf--bc-1--bc-2> .
<…/element/bc-2> skos:broader <…/element/bc-1> .
The ontology asks for skos:broader explicitly and is precise that it is additive, not a substitute: it
carries no relationship resource, so it cannot be typed, annotated or validated. Note it runs the opposite
way to the resource.
One rule the shapes hand back to the converter. A LeanIX hierarchy never crosses fact sheet types, but
FactSheetHierarchyEndpointsShape only requires a fact sheet at each end — and explains why it stops there:
the two ends need not share a class, since an Organization may legitimately parent an
OrganizationUnit, so a shape comparing rdf:type would reject a correct model while one walking the
subclass hierarchy would accept an Application under a Microservice. The rule is about the workspace's own
type token, which the graph carries as dct:type and only the export has in front of it. So the converter
checks it and warns:
[WARN] factsheets.json: a 'relToChild' edge runs from a BusinessCapability to an Application. A LeanIX
hierarchy does not cross fact sheet types, so this is either a relation that is not really a hierarchy —
check the --type-mapping entry for it — or an export that lost a type. Emitted as declared.
A warning rather than a refusal: the edge is what the workspace exported, so dropping it would hide data. Naming both types is what makes the usual cause — a mapping entry pointing at the wrong relationship — findable.
Relations leaving the pulled scope¶
Pulling a subset of fact sheet types is the normal case, so a relation naming a type that was not pulled is normal too. The converter mints the IRI that fact sheet would have, warns once, and leaves the node untyped. Because the id is stable, widening the pull later joins the two sides up: the IRI does not change.
Until then core-shapes#QualifiedRelationshipShape reports the dangling endpoint, which is the honest state
of the graph — either a deliberate scope boundary or a fact sheet that was deleted.
What each fact sheet emits¶
The ontology now declares the attribute set every workspace has. Where a term was already published elsewhere it deliberately does not shadow it, and neither does this:
<…/element/a1000000-…-000000000001>
a arch:Element, arch:ModelConcept, lmm:FactSheet, lmm:Application ;
arch:inModel <…/leanix/acme> ;
skos:prefLabel "Payments Gateway"@en ;
skos:altLabel "payments-gateway"@en ;
skos:notation "a1000000-0000-4000-8000-000000000001" ;
skos:definition "Authorises and captures card payments." ;
dct:type "Application" ;
dct:created "2020-06-02T09:30:00Z"^^xsd:dateTime ;
dct:modified "2026-08-01T11:05:00Z"^^xsd:dateTime ;
schema:url "https://acme.leanix.net/acme/factsheet/Application/a1000000-…"^^xsd:anyURI ;
lmm:factSheetStatus lmm:StatusActive ;
lmm:completion "0.83"^^xsd:decimal ;
ap:atLifecycleStage ap:Active ;
lmm:hasLifecyclePhase <…/element/a1000000-…-000000000001/phase/active> ;
schema:keywords "Cloud", "PCI" ;
lmm:hasTag [ a lmm:Tag ; lmm:tagName "Cloud" ; lmm:tagGroup "Hosting" ;
skos:notation "t-cloud" ] ;
lmm:hasSubscription [ a lmm:Subscription ;
lmm:subscriberEmail "accountable@example.org" ;
lmm:subscriptionType lmm:Responsible ;
lmm:subscriptionRole "Application Owner" ] .
<…/element/a1000000-…-000000000001/phase/active>
a lmm:LifecyclePhase ;
ap:atLifecycleStage ap:Active ;
lmm:phaseStart "2021-04-01"^^xsd:date .
| LeanIX | Emitted as |
|---|---|
| fact sheet id | skos:notation |
| display name | skos:prefLabel, language-tagged |
| name, when it differs | skos:altLabel |
| description | skos:definition |
| fact sheet type name | dct:type, verbatim |
| created / updated | dct:created / dct:modified |
| fact sheet URL | schema:url |
status |
lmm:factSheetStatus → lmm:StatusActive / StatusArchived / StatusBroken |
completion |
lmm:completion, an xsd:decimal in 0..1 |
lifecycle.asString |
ap:atLifecycleStage → ap:Plan / PhaseIn / Active / PhaseOut / Retired |
| dated phases | lmm:hasLifecyclePhase → lmm:LifecyclePhase |
| tags | lmm:hasTag → lmm:Tag, plus schema:keywords on the fact sheet |
| subscriptions | lmm:hasSubscription → lmm:Subscription |
relation activeFrom / activeUntil |
schema:validFrom / schema:validThrough |
| export timestamp | prov:generatedAtTime, in the provenance graph |
Four things here are less obvious than they look, and all four are the shapes' doing rather than taste:
States are individuals, not strings. lmm:factSheetStatus lmm:StatusActive, not "ACTIVE" — and
LeanIXFactSheetAttributeShape says so in its message: "A string literal such as ACTIVE does not satisfy
this." Same for subscription type. An unrecognised token is therefore reported and left out, because a
literal would fail validation while looking like data.
Lifecycle stages come from elsewhere. They are ap: individuals from the Architecture Processes
extension, which the LeanIX ontology reuses rather than minting its own. endOfLife maps to ap:Retired,
read off the skos:closeMatch each stage carries to the LeanIX lifecycle docs. This is why validate loads
arch-processes.
lmm:completion is xsd:decimal. A Kotlin Double gives xsd:double and fails sh:datatype, so it
is emitted from a BigDecimal. The shape also bounds it to 0..1, so a percentage would be caught.
A lifecycle phase needs both halves. LeanIXLifecyclePhaseShape requires exactly one stage and one
start date, so a phase missing either is skipped rather than emitted half-formed. A phase with no date is the
fact sheet's current stage, which belongs on the fact sheet as ap:atLifecycleStage.
Two smaller notes. Phase nodes are IRIs derived from the element's own IRI — matching the ontology's example
and keeping a re-conversion byte-stable — while tags and subscriptions are blank nodes, since neither has an
identity worth minting an IRI from. And schema:keywords stays alongside lmm:tagName: the ontology holds
the name in lmm: because this project publishes no cross-notation keyword term, and says that when one
arrives tagName becomes a sub-property of it; until then schema:keywords is what makes "everything tagged
Cloud" one query across PlantUML, Structurizr and LeanIX.
lmm:subscriber would be better than lmm:subscriberEmail — its range is arch:Stakeholder, so a
subscriber would be the same kind of thing as a stakeholder named by any other notation, and
lmm:Accountable is documented as the natural source for arch:conceptOwner. An export gives an email and
no stable identity to mint a stakeholder from, and the ontology names the email a legitimate fallback for
exactly this case.
Workspace-defined fields¶
The one attribute group with no published term, by design — they are defined per tenant, so there is no set to declare. The subtype field is not among them: it names a kind, so it is lifted out and resolved to a class instead. See Subtypes.
--ns-vocab puts the rest in a namespace you own:
Without it they are dropped, and the run says how many:
[WARN] factsheets.json: 7 workspace-defined field value(s) were not emitted. These are the one attribute
group the published ontology declares no term for — they are defined per tenant, so there is no set to
publish. Pass --ns-vocab <your namespace> to emit them under a namespace you own.
Subscriber emails are personal data
lmm:subscriberEmail is emitted as the export recorded it. The workspace already shows it to everyone
who can read the fact sheet, and it is the only stable handle on a subscriber an export carries — but a
graph published more widely than the workspace exposes contact details more widely too. Switch
subscriptions off at the pull (subscriptions: false) if that is not wanted, rather than converting and
filtering afterwards.
Validation¶
Loads leanix-shapes and core-shapes, with leanix, leanix-v3, arch-processes and core for
reasoning — 23 target classes in total. Three groups of rule:
| Group | Shapes | Checks |
|---|---|---|
| Naming | LeanIXFactSheetLabelShape |
a language-tagged skos:prefLabel on every fact sheet |
| Attributes | LeanIXFactSheetAttributeShape, LeanIXLifecyclePhaseShape, LeanIXSubscriptionShape, LeanIXTagShape |
status and lifecycle-stage value sets, completion datatype and 0..1 range, a phase's stage and date, a subscription's type and subscriber, a tag's name |
| Endpoints | one per relationship, e.g. RequiringEndpointsShape |
that each of the sixteen relationships runs between the types it declares |
Plus core-shapes#QualifiedRelationshipShape, which requires exactly one arch:source and one
arch:target. The endpoint shapes deliberately do not repeat that cardinality — they add only the LeanIX
types.
The endpoint shapes are what make the converter's own mapping checkable, which nothing did before. A
--type-mapping entry pointing a relation at the wrong class is now caught by name:
sh:resultMessage "A lmm:Provision must run from an ITComponent."
sh:resultMessage "A lmm:Provision must run to a Provider."
Subtypes pass, because sh:class resolves the subclass closure: a lmm:BusinessApplication satisfies
sh:class lmm:Application, and a lmm:Process satisfies sh:class lmm:BusinessContext.
Two things about the ontology set are easy to get wrong and both fail silently:
- v3 is loaded whichever version the run emitted, because the naming shape targets
lmm3:FactSheettoo — so a--meta-model v3graph needs no different asset set. A v3 graph matches fewer target classes, which--list-assetsand the coverage line make visible: the attribute shapes targetlmm:FactSheetand do not apply. arch-processesis loaded because the lifecycle stage values are its individuals, sosh:in ( ap:Plan … )has nothing to compare against without it.
A shape whose target class or value set the graph cannot resolve validates nothing and reports nothing —
indistinguishable from conformance. --list-assets shows the full set.
A fully-scoped export conforms. A scope-limited one reports two violations per dangling endpoint rather
than one, and that is a deliberate upstream decision worth knowing: core says an endpoint is missing, and the
endpoint shape names the type that failed to arrive, which is what tells you which type to add to the pull.
Both are kept at the same severity so the more useful message is not the easier one to ignore.
--without-shape attributes turns the four attribute shapes off together, for a graph converted before that
vocabulary was published.
Usage¶
# The whole pipeline: pull, then convert
leanix-pull pull --subdomain acme \
--type BusinessCapability,Application,Interface \
-o models/leanix/factsheets.json --write-index
leanix2linkedarchi convert models/leanix/factsheets.json \
--diagrams-index models/leanix/factsheet-index.yaml \
--diagrams-root models/leanix/ \
--base-iri https://example.org/la/ \
--ns-vocab https://example.org/vocab# \
--format TRIG -o out.trig
# Without an index: the model id comes from --model-id, or the filename
leanix2linkedarchi convert factsheets.json \
--model-id leanix-acme \
--base-iri https://example.org/la/ \
--format TRIG -o out.trig
Options¶
| Option | Default | Description |
|---|---|---|
--base-iri |
required | Base IRI for minting resource IRIs |
--format |
inferred | Default format for any --output that names none: TRIG / TURTLE / JSONLD / RDFXML / NTRIPLES / NQUADS. A file extension such as ttl is also understood |
-o, --output |
required | Output artifact, path[:FORMAT[:PROFILE]]. Repeatable — several artifacts are written from one conversion, which is faster than re-running the converter and is what makes them share one prov:generatedAtTime. PROFILE is full (default), no-geometry, no-views or no-diagrams; see output serialization |
--meta-model |
v4 |
Which published meta model the workspace is shaped by: v4 | v3 |
--model-id |
filename | Model identifier. One export is one model, so one id |
--require-id |
false |
Fail instead of deriving a model id from the filename |
--diagrams-index |
none | Fact sheet index: model identity, lifecycle state, extension data |
--diagrams-root |
index's directory | Root for resolving file: paths in the index |
--exclude-states |
none | Leave index entries in these lifecycle states out of the run |
--type-mapping |
none | Repoint fact sheet types and relations, for a renamed or custom metamodel |
--ns-vocab |
none | Namespace for attributes the published ontology has no term for |
--factsheet-url-base |
from the export | Base URL for each fact sheet's schema:url. Pass an empty value to emit none |
--diagrams-export |
none | LeanIX diagram export. The only route to an arch:View — see Views |
--diagram-index |
none | Per-diagram YAML declaring each diagram's viewpoint: and status: |
--ns-viewpoints |
lmmvp: |
Namespace for a viewpoint written as a bare name in the diagram index |
--emit-extension-data |
false |
Map the index's elements: / links: / data: into the graph |
--ns-global-id |
none | Base IRI for link targets written without a prefix |
--emit-direct-rel-triples |
false |
Also emit {src} lmm:requires {tgt}, bridged with rdf:reifies |
--emit-stakeholders |
false |
Mint arch:Stakeholder per subscriber and arch:conceptOwner from the accountable one |
--emit-skos-labels |
true |
Emit skos:prefLabel and skos:altLabel (negatable) |
--label-language |
en |
BCP-47 tag for the labels |
--emit-skos-notation |
true |
Emit skos:notation (negatable) |
Plus --git-provenance, --image-ref and --run-timestamp, shared with every other converter
(ADR 0008).
mapping-report¶
The other half of leanix-pull introspect. That says what a workspace
declares; this says what the converter will do with it — without writing a graph, so it can run before a
first conversion and in CI as a gate.
Fact sheet types (5):
ok Application 2 lmm:Application
ok BusinessCapability 2 lmm:BusinessCapability
ok Platform 1 lmm:Platform
Fact sheet subtypes (2):
ok Application/businessApplication 1 lmm:BusinessApplication
mapped ITComponent/software 1 onto#SystemSoftware
Relations (11):
ok relApplicationToITComponent 1 lmm:Requiring
ok relPlatformToApplication 1 lmm:PlatformMembership
ok relToChild 1 lmm:FactSheetHierarchy
16 published, 1 mapped by --type-mapping, 0 falling back.
Everything in this export reaches a class. Nothing to do.
Subtypes are keyed Type/subtype, which is both the unambiguous name and the mapping key to use. An export
with none says so, because a workspace with no subtypes and a pull that never asked for them look identical
from here and the second is more likely:
No fact sheet in this export carries a subtype. If the workspace has any, the pull did not ask: a subtype
rides on a field (conventionally 'category'), so it needs 'subType: category' on the type in the pull
configuration. 'leanix-pull introspect' writes that wherever the schema has the field.
A workspace with its own types, subtypes and relations gets told what it costs and how to fix it:
FALLBACK ProductFamily 1 lmm:FactSheet only
FALLBACK ITComponent/software 1 no lmm:Software — map it, or --ns-vocab mints one
FALLBACK relProductFamilyToApplication 1 no LeanIX class
What falls back, and what it costs:
ProductFamily: lmm:FactSheet only — map it under 'elements:' keyed 'ProductFamily'
ITComponent/software: no lmm:Software — map it under 'elements:' keyed 'ITComponent/software' — or
keyed 'software' to catch it under any type — or --ns-vocab
relProductFamilyToApplication: no LeanIX class — map it under 'relationships:' keyed
'relApplicationToProductFamily'
An unmapped subtype is the narrower loss: the fact sheet is still typed as its parent, so it is findable,
but nothing distinguishes it from the rest of that type.
--ns-vocab is deliberately not treated as resolved here, even though the converter would mint a class
from it: this command takes no such option, and a report that called it ok would hide the fact that a
published class was available and something pointed elsewhere.
It reads an export rather than a pull configuration, deliberately: the export is what gets converted, so
the report covers the data that will actually reach the graph. --type-mapping is taken into account, so an
entry that resolves a fall-back shows as mapped rather than FALLBACK.
--fail-on-unmapped exits 1 when anything falls back, for a pipeline that should not publish a graph with
untyped edges. Without it the command always exits 0, which is the right default: a fall-back is emitted,
not dropped — the resource is reachable, it just carries no LeanIX class.
--meta-model applies here too, and must match what the conversion will use or the report describes a run
that will not happen. Under v3 relations come back as n/a rather than FALLBACK, because v3 publishes no
relationships — there is no class to reach and no mapping is missing, so --fail-on-unmapped stays usable for
a v3 workspace instead of always failing:
Reporting against LeanIX Meta Model v3 (lmm3:)
Fact sheet types (12):
ok UserGroup 2 lmm3:UserGroup
Relations (12):
n/a relApplicationToITComponent 1 Meta Model v3 publishes no relationships
12 published, 0 mapped by --type-mapping, 0 falling back. 12 with no class to reach.
A type the other version declares is reported as a version mismatch rather than a missing mapping, and the
remedy names --meta-model instead of a mapping key.
Views, from diagrams¶
A LeanIX diagram is a view. An inventory export is not. That distinction decides what may be said in the graph, so it is worth stating plainly before the mechanics.
An export of fact sheets answers "what is in the workspace": a set of elements and the edges between them,
with no statement about what anyone chose to show, to whom, or in what arrangement. A view minted for it
would be a diagram nobody drew, its membership would be "everything that was pulled", and its
arch:viewConformsToViewpoint would have to be invented. So a conversion given only an inventory emits
no arch:View at all — not an empty one, and not an empty views graph either, so a consumer can tell
"this workspace has no diagrams" from "the diagrams were not pulled".
LeanIX does have diagrams — Free Draw, Data Flow — and those are views. They arrive through
leanix-pull pull-diagrams and reach the graph through
--diagrams-export:
leanix2linkedarchi convert models/leanix/factsheets.json \
--diagrams-export models/leanix/diagrams.json \
--diagram-index models/leanix/diagram-index.yaml \
--base-iri https://example.org/la/ --format TRIG -o out.trig
The two are converted together rather than separately because a view is only worth having if its nodes point at real elements, and the element IRIs are minted from the same fact sheet ids the diagrams reference.
# in the semantic graph
<…/view/d1000000-…-000000000002>
a arch:View, arch:Diagram, arch:ModelConcept ;
arch:inModel <…/leanix/acme> ;
skos:prefLabel "Capability map, board edition"@en ;
skos:notation "d1000000-0000-4000-8000-000000000002" ;
dct:type "FreeDraw" ;
dct:created "2025-11-11T08:30:00Z"^^xsd:dateTime ;
dct:modified "2026-07-20T16:45:00Z"^^xsd:dateTime ;
schema:url "https://acme.leanix.net/acme/diagrams/d1000000-…"^^xsd:anyURI ;
arch:viewConformsToViewpoint lmmvp:CapabilityMap .
# in the model graph, which holds the folder tree
<…/view/d1000000-…-000000000002> dct:isPartOf <…/folder/Views> .
# in the views graph
<…/view/d1000000-…-000000000002/node/b1000000-…-000000000001>
a archvis:ArchNode ;
archvis:view <…/view/d1000000-…-000000000002> ;
archvis:archElement <…/element/b1000000-…-000000000001> .
# in the provenance graph, from the diagram index
<…/view/d1000000-…-000000000002> adms:status <http://purl.org/adms/status/UnderDevelopment> .
The view IRI is minted from the LeanIX diagram id, so a diagram being retitled does not move a published
address — the same property a fact sheet's id gives an element. dct:type keeps the editor's own token
verbatim, as a fact sheet's type does, because no published term claims it.
The pay-off is that "which diagrams show this application" becomes one query, and a notation-agnostic
one: it is the same archvis:archElement traversal that answers it for a BPMN process or an ArchiMate view.
Members, and three things deliberately not invented¶
A node per fact sheet on the canvas, and no archvis:Link. A line between two boxes is only meaningful
as a relationship, and nothing recognisable in a LeanIX layout says which of the relationships joining
two fact sheets it stands for — two can be joined by lmm:Requiring and lmm:DataUsage at once. Choosing
would put an arrow in the graph that nobody drew.
No geometry. Nothing documented says where the coordinates are, and coordinates are the part of a layout that changes whenever somebody nudges a box. A Structurizr view carries none either, so a view without geometry is an established shape rather than a degraded one.
No viewpoint unless one is declared. See below.
A diagram that yielded no members is reported, because an empty canvas and a layout format the pull could not read produce identical output and only the person looking at the workspace can tell which:
[WARN] diagrams.json: 1 of 3 diagram(s) reference no fact sheet, so their views have no members. Either
those diagrams are empty, or the pull could not read their layout — 'leanix-pull pull-diagrams
--retain-state' says which.
A diagram can also show a fact sheet type the inventory pull did not include. The node is emitted against the IRI that fact sheet would have — the id is stable, so widening the pull joins the two sides without any published address moving — and it is reported, because until then the view has a member nothing describes.
The viewpoint is declared, never inferred¶
arch:viewConformsToViewpoint is what makes a view answerable, and the published catalogue
(lmmvp:) has five: ApplicationPortfolio,
TechnologyLandscape, InterfaceMap, CapabilityMap, TransformationRoadmap.
The tempting inference is from a diagram's contents, and the published documents rule it out:
lmmvp:ApplicationPortfolio and lmmvp:CapabilityMap declare identical arch:includesConcept sets —
lmm:Application, lmm:BusinessCapability, lmm:Organization — so no set of fact sheet types can
distinguish them. Nor does the bookmark say: a Free Draw canvas is a blank canvas whatever somebody drew on
it.
So it is a judgement, recorded per diagram in --diagram-index:
diagrams:
- id: d1000000-0000-4000-8000-000000000001
viewpoint: InterfaceMap
status: publish
- id: d1000000-0000-4000-8000-000000000002
viewpoint: https://example.org/viewpoints#PaymentsFlow
title: Capability map, board edition
status: draft
| Field | Effect |
|---|---|
viewpoint: |
a bare name resolved against lmmvp:, or an absolute IRI used as written |
status: |
adms:status on the view, in the provenance graph. Omit it and none is claimed |
title: |
wins over the workspace name for skos:prefLabel — renaming a diagram for publication should not mean renaming it upstream |
A bare name is checked against the published set, and an unrecognised one is refused rather than minted:
[WARN] 'PaymentsFlow' is not a viewpoint the published LeanIX catalogue declares, so diagram 'Payments
landscape' (d1000000-…) carries no arch:viewConformsToViewpoint. The published set is
ApplicationPortfolio, TechnologyLandscape, InterfaceMap, CapabilityMap, TransformationRoadmap. For a
viewpoint of your own, write its full IRI in the index, or point --ns-viewpoints at your namespace.
Same rule as every other term here: lmmvp: is a published namespace this converter reads and does not own,
so an undeclared term in it is one a reader cannot dereference. A team maintaining its own viewpoints writes
the full IRI, or points --ns-viewpoints at its namespace — in which case bare names resolve there and are
not checked, since the published set does not govern somebody else's catalogue.
A misspelled status: fails the run, as it does in every other index: reading an unknown value as the
default is how a typo publishes work in progress. And an index entry for a diagram the export does not
contain — normally one somebody deleted — is reported rather than conjured into a view.
Stakeholders and ownership¶
--emit-stakeholders turns LeanIX subscriptions into people:
<…/element/a1000000-…>
arch:conceptOwner <…/stakeholder/owner-example.org> ;
lmm:hasSubscription [ a lmm:Subscription ;
lmm:subscriber <…/stakeholder/owner-example.org> ;
lmm:subscriberEmail "owner@example.org" ;
lmm:subscriptionType lmm:Accountable ;
lmm:subscriptionRole "Business Owner" ] .
<…/stakeholder/owner-example.org>
a arch:Stakeholder ;
skos:prefLabel "owner@example.org"@en ;
skos:notation "owner@example.org" ;
schema:email "owner@example.org" .
The ontology invites exactly this: lmm:subscriber ranges over arch:Stakeholder, so a subscriber becomes
the same kind of thing as a stakeholder named by any other notation, and lmm:Accountable is documented as
"the natural source for arch:conceptOwner". The pay-off is that "who owns this application" stops being
a LeanIX question: the same query answers it for a Backstage spec.owner or an ArchiMate assignment.
Three decisions worth knowing:
- Only the accountable subscriber owns.
Responsiblekeeps a fact sheet's data accurate andObserveronly follows it, so neither answers for the thing. Both still getlmm:subscriber. - One resource per person, keyed on the lower-cased email, so someone subscribed to forty fact sheets is
one stakeholder and
Accountable@andaccountable@are the same person. - A subscription with no email gets no stakeholder, and the run says so once. An export offers no other identity for a person; the subscription still stands on its roles.
This creates resources for people, which is why it is opt-in
The only identity an export carries is an email address, so the address ends up in an IRI as well as in
the lmm:subscriberEmail literal it already appears in. Hashing the IRI would hide nothing — the literal
is right there — while making the resource unrecognisable, so it is left readable. What protects it is the
flag: nothing about people is emitted unless you ask.
An IRI also travels further than a literal. If a consumer materialises the graph as files, a person
becomes a filename. Decide that deliberately; subscriptions: false at the pull is the way to keep
contact details out of the export altogether.
The fact sheet index¶
Same format as every other converter's — model: with views:, one view per file — so nothing new has to be
learned. leanix-pull --write-index generates a starting point and never overwrites it afterwards, because
the index holds the editorial decisions a pull knows nothing about.
# models/leanix/factsheet-index.yaml
prefixes:
am: https://meta.linked.archi/archimate3/onto#
kg: https://example.org/graph/
model:
id: leanix-acme # → …/leanix/leanix-acme and its named graphs
title: "LeanIX — acme"
architectureState: baseline
views:
- id: factsheets
file: factsheets.json
status: publish
elements:
a1000000-0000-4000-8000-000000000001: # by fact sheet id
links: { am:realizes: kg:CAP-OrderManagement }
data: { x:costCentre: "CC-4711" }
An inventory is not a diagram, so no arch:View is emitted and there is no render command. The converter
is catalog-oriented in the sense of Diagram index files: index metadata
that would land on a view lands on the model instead.
Extension data can refer to a fact sheet by its id, its displayName or its name. LeanIX does not
require names to be unique, so a name matching two fact sheets is refused and reported rather than attached
to whichever was parsed last — use the id.
A directory of exports with an index gives one model per file, which is right: two exports are two snapshots, and merging them into one model would interleave them.
Mapping a renamed or custom type¶
--type-mapping keys on the names your workspace uses — including on the left of the slash in a
qualified subtype key, which is the one place that rule is easy to forget (When the type itself is
renamed). elements: takes fact sheet type names, and
subtype names in either the qualified Type/subtype or the bare form (Subtypes);
relationships:, predicates: and qualifiedPredicates: take relation field names, falling back to the
canonical name the converter resolved — Requiring — where no field name matches. All three sections resolve
the same way, so a mapping keyed consistently cannot retype a relation while leaving its predicates behind.
# A workspace that renamed things, mapped back onto the published vocabulary
elements:
BusinessApp: https://meta.linked.archi/leanix/onto#Application
Capability: https://meta.linked.archi/leanix/onto#BusinessCapability
relationships:
relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#Supporting
predicates:
relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#supports
qualifiedPredicates:
relBusinessAppToCapability: https://meta.linked.archi/leanix/onto#qualifiedSupports
The same mechanism retypes LeanIX output in another vocabulary entirely, so it merges with models converted
from other notations — see playground/config/type-mapping-leanix.yml for a full ArchiMate mapping. A mapped
class replaces the published one; lmm:FactSheet stays either way, so the naming shape still applies.
With ArchiMate classes in the output, validate --shapes relationships brings ArchiMate's own source/target
rules to bear on a LeanIX-derived graph.
Give every retyped relation its predicates as well as its class — Retyping a relation into another vocabulary explains why reusing the published ones would put the replaced class back by inference.
Known limitations¶
Nothing in the published vocabulary is missing any more: as of 2026-08-16 all sixteen relationships, the full attribute set and an endpoint shape per relationship are published, and the upstream request file is closed. What remains is either deliberate or ours:
- Workspace-defined fields need
--ns-vocab. Deliberate: they are defined per tenant, so there is no set to publish. Everything else lands on a published term. - A relation a workspace invented gets no LeanIX class. Also deliberate — the converter will not mint a
term into a namespace it reads rather than owns — and
--type-mappingis the route to giving it one. - A v3 run types no relationship and validates no attribute. Upstream's decision, not a gap here: the v3 manifest publishes fact sheet classes only. See Meta Model v3 and v4.
- The meta model version is an option, not a detection. Nothing in a GraphQL export states which version a
workspace runs, and the fact sheet types only suggest it — a v4 workspace that pulled no
Organizationlooks like neither. So--meta-modelis explicit, defaults to v4, and the converter names the other version when a type belongs to it rather than guessing. - Deletions are not detected by the converter. An export is a snapshot; a fact sheet deleted in the workspace simply stops appearing. What removes its triples from a published graph is republishing the model, so a pipeline that merges converter output into a long-lived store has to replace the model's named graphs rather than add to them.
- A view carries members, not connectors or geometry. Diagrams are read — see
Views — but only their membership is trustworthy. SAP publishes no schema for a
diagram's layout, so a connector cannot be resolved to the relationship it stands for and a coordinate
cannot be located at all. Both are omitted rather than approximated, and
pull-diagrams --retain-statekeeps the raw blob for a release that can read them. - A viewpoint has to be declared. Not a defect to fix: two published
lmmvp:viewpoints cover identical concept sets, so no inference from a diagram's contents could tell them apart. The diagram index is where the judgement is recorded. - A saved diagram is the last-saved view. Upstream's own caveat. A diagram reflects the layout as it was saved, so a converted view can lag the inventory it draws from — which is also why the two exports carry their own timestamps.
- The relation field-name table is a best effort. Field names are per workspace, so the table covers the
documented metamodel;
print-queryand--type-mappingare how a reshaped workspace closes the gap.