Diagram Index Files¶
Purpose¶
A diagram index is a YAML file, passed as --diagrams-index, that serves three functions:
- File selection — controls which files are processed (unreleased entries skipped)
- Identity —
diagrams[].iddetermines IRI path segments, and names therenderoutput file. Read the next section before relying on that: the field is misnamed - Provenance metadata — title, author, version, dates and lifecycle state flow into the provenance graph
Models hold views¶
A diagram is an arch:View inside an arch:Model, and the index says so directly: name the
model, list the diagrams it holds.
model:
id: order-domain # the arch:Model — IRI namespace and named graphs
title: "Order Domain"
views:
- id: order-flow # the arch:View — and the render SVG filename
file: processes/order-flow.puml
status: publish
title: "Order Flow"
- id: fulfilment-flow
file: processes/fulfilment-flow.puml
status: publish
Both diagrams convert into …/plantuml/order-domain, so an element drawn in both is one
element with a node in each view:
<https://example.org/la/plantuml/order-domain/graph/semantic> {
<https://example.org/la/plantuml/order-domain/element/OrderService>
a arch:Element , arch:ModelConcept ;
arch:inModel <https://example.org/la/plantuml/order-domain> .
<https://example.org/la/plantuml/order-domain/view/order-flow>
a arch:View , arch:Diagram , arch:ModelConcept ;
arch:inModel <https://example.org/la/plantuml/order-domain> .
<https://example.org/la/plantuml/order-domain/view/fulfilment-flow>
a arch:View , arch:Diagram , arch:ModelConcept ;
arch:inModel <https://example.org/la/plantuml/order-domain> .
}
<https://example.org/la/plantuml/order-domain/graph/model> {
<https://example.org/la/plantuml/order-domain> a arch:Model .
}
<https://example.org/la/plantuml/order-domain/graph/views> {
<…/view/order-flow/node/OrderService> archvis:archElement <…/element/OrderService> .
<…/view/fulfilment-flow/node/OrderService> archvis:archElement <…/element/OrderService> .
}
Several models can live in one index, each listing its own views:
models:
- id: order-domain
title: "Order Domain"
views:
- id: order-flow
file: processes/order-flow.puml
- id: payments
views:
- id: payment-authorisation
file: payments/payment-auth.puml
Metadata follows the level it describes: title, created, modified and description on a
view describe that diagram; author and version on the model describe the model, and a view
inherits them unless it says otherwise.
Declaring identity in the source file¶
A PlantUML source can declare its own identity in header comments, which every renderer ignores:
Only the lines before @startuml are read. convert and render both read them, and a
declaration in the source must agree with one in the index — a disagreement fails the run rather
than one side silently winning:
[ERROR] Conversion failed: Conflicting view id for 'order-flow.puml': the source file declares
'order-flow', diagram-index.yaml declares 'renamed-flow'. Remove one of the two declarations.
An index entry may therefore omit id for such a file, and needs only file: and status:. For
notations whose files cannot carry an identity — BPMN sources are tool-generated — the index
remains the only declaration site. See
ADR 0001.
Requiring a declared identity¶
Without a declaration the model ID falls back to the input filename, which is unvalidated and
changes when the file is renamed. --require-id removes that fallback:
[ERROR] Conversion failed: --require-id is set and no model id was declared for 'Order Flow.puml'.
Without one the filename would become the identity, which is neither validated nor stable.
Declare one in the diagram index, or pass --model-id.
Publishing pipelines should set it. Ad-hoc local runs can leave it off and keep converting a file without an index.
Changing a published identity¶
An indexed run records the IDs it published in identity-lock.yaml, written beside the index and
committed. A later run whose IDs differ fails, because changing them relocates every IRI in the
model:
[ERROR] Conversion failed: Identity change for 'flow.puml': view id was published as
'order-flow', now 'fulfilment'. Declare the previous id under formerIds:, or pass
--allow-identity-change.
Declare the change to authorise it and keep the previous IRI resolvable:
<https://example.org/la/plantuml/order-domain/view/fulfilment>
dct:replaces <https://example.org/la/plantuml/order-domain/view/order-flow> .
<https://example.org/la/plantuml/order-domain/view/order-flow>
owl:sameAs <https://example.org/la/plantuml/order-domain/view/fulfilment> ;
dct:isReplacedBy <https://example.org/la/plantuml/order-domain/view/fulfilment> .
formerIds: is read at the level it sits on. On a view entry it supersedes view IDs, on the
model: entry it supersedes the model ID, and in the flat schema the entry's formerIds:
supersede the model ID because that is what the entry id is:
model:
id: order-domain
formerIds: [orders] # aliases the model IRI
views:
- id: fulfilment
formerIds: [order-flow] # aliases the view IRI and the SVG name
file: flow.puml
A former ID is held to the same slug shape as a current one, since it is published as an IRI and as a filename.
The alias is emitted on every run for as long as formerIds: names it, not only on the run that
performed the rename: the lock records the new ID immediately, so a later run sees no change and
would otherwise publish the model without the alias.
Because a view ID also names the rendered SVG, render writes the picture under the superseded
name as well, so a documentation link to the previous image URL keeps resolving:
A superseded model ID does not change the filename, so it produces alias triples only.
--allow-identity-change accepts an undeclared change and updates the record, for a model whose
IRIs were never published outside the repository. It authorises the change without describing it,
so no aliases are emitted. Full rules in ADR 0003.
Legacy flat schema¶
Still supported, and still means one model per file: diagrams[].id is the model id, and
the notation supplies the view id — the BPMNDiagram id for BPMN, the model id itself for
PlantUML, since a .puml has none. Reinterpreting it would move every IRI in every index that
uses it, so it keeps its meaning. Use model: / models: for anything new, and for any set of
diagrams that belong to one model.
Model, view and element IDs are three different things with different rules — see Identifiers.
It is not universal, and it is never mandatory. Which converters read one follows from what a single input file means to them:
| Converters | One input file is… | Identity comes from | |
|---|---|---|---|
| Diagram-oriented | BPMN, PlantUML | one diagram, authored in isolation | the index (id), or the filename without one |
| Catalog-oriented | Backstage, LeanIX | one catalog file or directory / one fact sheet export | the index (id), --model-id, or the filename |
| Model-oriented | ArchiMate, Structurizr | one entire model, views included | --model-id, or the filename. No index support |
BPMN and PlantUML behave identically here — same selection rules, same ID resolution, same
file: matching — because both are collections of independent diagrams. ArchiMate and
Structurizr have nothing to select between: the file is the model, so they take
--model-id instead and have no render command either.
Backstage and LeanIX read an index for a reason the diagram-oriented converters do not have. Neither
input is a diagram and neither needs selecting between, but both are somebody else's schema — a
catalog file owned by Backstage, an export written from a LeanIX workspace — with no extension point to
write into and nothing that would survive the next regeneration. For them the index is the only route for
a statement the source format cannot carry. Both emit no arch:View, so index metadata that would land
on a view lands on the model instead.
Fields¶
model:
id: order-domain # Model id — the arch:Model, its IRI namespace and graphs
title: "Order Domain" # Model title
author: "Commerce Team" # Author / owner — inherited by every view
version: "1.0" # Version string — inherited by every view
architectureState: baseline # Which reality the model describes — see below
links: { … } # Statements about the model — see below
data: { … } # ditto, with literal values
views:
- id: order-fulfillment # View id — the arch:View, and the render SVG filename
file: order-fulfillment.bpmn # Filename (relative to --diagrams-root)
status: publish # Lifecycle state, decided per view — see below
architectureState: target # Which reality this diagram describes — see below
supersededBy: order-flow-v2 # On a retired entry: what to read instead
title: "Order Fulfillment" # Diagram title
created: "2024-03-15" # Creation date
modified: "2025-11-20" # Last modification date
description: "End-to-end flow" # Description
links: { … } # Statements about the view — see below
data: { … } # ditto, with literal values
elements: { … } # Statements about elements in it — see below
What an entry produces¶
Converted with --base-iri https://example.org/la/ (real output, abbreviated):
# ── the model and its named graphs: from model.id ──
<https://example.org/la/bpmn/order-domain> a arch:Model .
<https://example.org/la/bpmn/order-domain/graph/semantic> { … }
<https://example.org/la/bpmn/order-domain/graph/views> { … }
<https://example.org/la/bpmn/order-domain/graph/provenance> { … }
# ── the view: from views[].id ──
<https://example.org/la/bpmn/order-domain/view/order-fulfillment>
a arch:View , arch:Diagram ;
dct:isPartOf <https://example.org/la/bpmn/order-domain> .
# ── elements and relationships: from IDs in the source file, shared by every view ──
<https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder> a arch:Element .
<https://example.org/la/bpmn/order-domain/relationship/Flow_1> a arch:QualifiedRelationship .
render writes order-fulfillment.svg — named after the view, because an SVG depicts one
diagram. The full output, including how the metadata fields land in the provenance graph, is in
Identifiers.
Element entries¶
An index can also make statements about individual elements, which is the index route to extension data — for a team that owns the pipeline rather than the diagrams.
prefixes: # index-level, for every model in the file
am: https://meta.linked.archi/archimate3/onto#
arch: https://meta.linked.archi/core#
kg: https://example.org/graph/
lx: https://leanix.example.com/factsheet/
x: https://example.org/vocab#
model:
id: order-domain
prefixes: # model-level, overriding index-level
x: https://example.org/orders-vocab#
views:
- id: order-flow
file: order-flow.puml
status: publish
elements:
OrderService: # the element, as the notation names it
links:
am:realizes: kg:CAP-OrderManagement
arch:partOf: # a list, since YAML cannot repeat a key
- kg:CAP-Commerce
- kg:CAP-Fulfilment
am:serves: # a map, to declare a direction
target: lx:APP-order-api
direction: Backward
data:
x:costCentre: CC-4711
| Key | Meaning |
|---|---|
prefixes: |
prefix to namespace, at index or model level. YAML resolves nothing, so CURIEs need a declared table |
elements: |
keyed by the element as the notation names it — see referring to an element |
links: |
predicate to target. The object is a resource, so it is resolved |
data: |
predicate to value. The object is a literal, used as written |
links: and data: are separate because the difference is not inferable: CC-4711 is a literal and
CAP-1 is an element id, and both are bare strings.
A link value may be a scalar, a list of scalars, or a map with target and direction. Requires
--emit-extension-data; without it the entries are read and ignored.
Prefixes are declared at index or model level but never per view: a prefix is a vocabulary, and one that changed per diagram would make the same CURIE mean different things in one model's graph.
Statements about a view or a model¶
links: and data: also work one level up, written directly on an entry rather than inside
elements:. There they are about that entry — the view, or the model — rather than about
anything drawn in it:
prefixes:
arch: https://meta.linked.archi/core#
archvp: https://meta.linked.archi/core-viewpoints#
kg: https://example.org/graph/
x: https://example.org/vocab#
model:
id: order-domain
links:
arch:architectureState: arch:Baseline # about the model
views:
- id: target-payments
file: target-payments.puml
links:
arch:architectureState: arch:Target # about this view
arch:viewConformsToViewpoint: archvp:Roadmap
data:
x:reviewedBy: Jane Doe
- id: option-b
file: option-b.puml
links:
# arch:inView runs concept → view, so the statement is reversed
arch:inView: { target: kg:ReadReplicas, direction: Backward }
This is for anything true of the diagram as a whole and of no element in it — which architecture
state it depicts, which viewpoint it conforms to, which decision option it articulates. The blocks
behave exactly as they do inside elements:: same prefixes, same scalar-or-list-or-map values, same
direction tokens, same --emit-extension-data gate.
Model-level statements stay at model level. They are not copied onto the views, for the reason
formerIds: is not: the two levels are different resources, and attaching a model-wide claim to
every view would say something about each diagram that the author said about the whole model. A
model-level statement is written once, in the one place the model is named — which is also why
there is no in-file route for it, since a model spans several files.
In the legacy flat schema an entry is a model, so links: and data: there are model-level. There
is no view for the index to describe, because the notation names the view. elements: works in both
schemas.
Colons and quoting¶
This file is full of colons — CURIEs as keys and as values, and entity refs like
component:default/order-service — so it is worth knowing exactly when YAML minds.
A colon inside a word is fine. YAML only ends a key at a colon followed by whitespace, so none of these need quoting:
elements:
component:default/order-service: # a ref as a key — fine
links:
am:realizes: kg:CAP-Orders # CURIE key, CURIE value — fine
A colon followed by a space is not. It reads as a nested mapping and the file fails to parse, which at least fails loudly:
data:
x:note: see also: the runbook # ERROR: mapping values are not allowed here
x:note: "see also: the runbook" # quoted — fine
Two values do need quoting, and these are the ones worth remembering, because YAML reads them as something other than text without complaining:
data:
x:window: "12:30" # unquoted 12:30 is the number 750
x:note: "{n/a}" # unquoted {…} is a mapping, not a literal
12:30 is sexagesimal in YAML 1.1 — 12×60+30 — so an unquoted time-like value silently becomes an
integer and reaches the graph as "750". A value starting with { is read as a flow mapping; that
one is refused rather than published, naming the quoting fix, since a mapping with no target: is
never a statement anyone meant. Values starting with *, & or @ are reserved and fail to parse.
The rule that covers all of it: quote any literal that is not plainly a word. Keys and CURIEs need nothing.
PlantUML has an in-file equivalent for the view — '!la-view-link and '!la-view-data — and the two
routes are additive like the element ones. BPMN and the other notations have the index route only:
bpmn:extensionElements nests inside the element it annotates, so there is nowhere in the file to
put a statement about the diagram. See extension data.
Where a model ID comes from¶
Every command resolves a model ID for each input file, index or no index. The chain is
first match wins. The view ID resolves alongside it: the index views[].id when there is one,
otherwise whatever the notation supplies.
| Command | Resolution |
|---|---|
bpmn2linkedarchi convert |
index model id → --model-id → filename |
plantuml2linkedarchi convert |
index model id → --model-id → filename |
backstage2linkedarchi convert |
index model id → --model-id → filename |
archimate2linkedarchi convert |
--model-id → filename (always a single input, -i) |
structurizr2linkedarchi convert |
--model-id → filename |
bpmn2linkedarchi render |
index model id → filename |
plantuml2linkedarchi render |
index id → filename |
"filename" means the input file's name without its extension.
One model per --model-id¶
A model ID identifies one model, so one explicit value cannot serve a batch. Passing
--model-id with more than one input file is refused:
[ERROR] --model-id 'both' cannot be used with 2 input files: a model ID names one model.
Convert the files one at a time, or use --diagrams-index to give each one its own id.
Nothing is written when that happens. Passing it alongside an index is allowed but has no
effect, and says so, because the index supplies an id for every file it selects:
[WARN] --model-id 'ignored-value' is ignored: the diagram index supplies the id for every
file it selects.
What is validated¶
Only the index path. A model ID taken from --model-id or from a filename is used as
given, so --model-id "My Model" or a source file called Order Flow.bpmn still produces
IRIs containing spaces. That is why anything published should come from an index (BPMN,
PlantUML, Backstage) or from an explicitly chosen --model-id (ArchiMate, Structurizr) —
as in --model-id payment-platform, never the filename by accident.
View and element IDs are not validated at all, and model IDs are not checked across repositories. Identifiers → What is guaranteed lists what holds and what does not; whether identity should be declared here or in the source file is evaluated in ADR 0001.
id rules¶
Model and view ids are used unescaped as IRI path segments
({base}{notation}/{model}/view/{view}), and a view id also names the render output file, so
both must match:
Lowercase letters and digits, then any of -, _, .. A view id must also be unique within
its model, and a file may appear once in the index. Anything else fails the command, convert
and render alike, naming the offending entries:
[ERROR] Render failed: Invalid id(s) in models/diagram-index.yaml:
view id 'Order Fulfillment Process' (file: order-fulfillment.bpmn). Model and view ids
become IRI path segments, and a view id also names the rendered SVG, so both must match
^[a-z0-9][a-z0-9._-]*$ …
Why each restriction:
| Rejected | Reason |
|---|---|
space, % |
not legal in an IRI; needs percent-encoding in every published link |
#, ? |
truncates the IRI into a fragment or query, so it identifies something else |
/, .. |
adds IRI path segments, and writes the SVG outside -o |
| uppercase | distinct IRIs that collide on case-insensitive filesystems |
| duplicate view id in one model | two views share one set of IRIs, and one SVG overwrites the other |
| duplicate model id in the flat schema | each flat entry is its own model, so the two would merge unintentionally |
Ids are not normalised for you. Slugifying silently would leave the graph and the
filename disagreeing, which is the mismatch a shared id exists to prevent.
Excluded entries are validated too: a broken id fails the run even when --exclude-states leaves
the entry out, so the problem surfaces before the entry is published rather than on the day it goes
live.
One view, one file¶
A file appears once, and each view id is used once in its model. Both directions are rejected:
| Message | |
|---|---|
| One view id twice in a model | Duplicate view id(s) … — the two views would share one set of IRIs, and one SVG would overwrite the other |
Two views, the same file: |
Duplicate file(s) … — only one entry can apply to a file, so the others would produce no SVG and no IRIs |
Equivalent spellings of one path count as the same file, so nested/x.bpmn and
./nested/./x.bpmn collide rather than slipping through.
An index cannot publish one diagram under two ids. Copy or link the rendered SVG instead.
Element and relationship ids must agree across views¶
Views of one model share one namespace — that is the point, and it is what makes one component drawn in three diagrams one resource. It also means the same local id in two files is the same concept, so a conflict is a defect:
[ERROR] Conversion failed: Identity collision in model 'order-domain':
<…/bpmn/order-domain/element/Task_ShipOrder> is claimed by two views:
'proc-a.bpmn' says Ship Order (Element/ModelConcept/ServiceTask),
'proc-c.bpmn' says Dispatch Parcel (Element/ModelConcept/ServiceTask).
Nothing is written when that happens. Agreement is not an error: two views showing the same
element with the same name and type merge silently, which is the intended case. The check
compares name and type, so it cannot catch two genuinely different things that happen to share
both — BPMN ids like Task_1 from separate templated processes are worth reviewing when you
group them.
Relationships are held to the same rule. A PlantUML relationship id is built from its label
(ADR 0005), so two views drawing the same arrow
agree on the id and on the label and merge quietly — one arch:QualifiedRelationship with a link
in each view — while a call and a reply between the same pair are two relationships. What still
reaches this check is what identity cannot settle:
- BPMN ids are tool-assigned and only unique per file, so
Flow_1in two grouped processes is two different flows on one IRI. - Two views disagreeing about the type — an
Associationin one and aDependencyin the other. - Two views whose labels are different words, not a different spelling of the same words.
A difference of spelling is reconciled, not refused¶
create order against Create Order, or Command Cancel Request against Command (Cancel) Request.
These are not collisions. A relationship id folds case and punctuation on purpose, so both views
always meant one message, and an identifier that ignores those differences cannot then insist on them.
They still need settling, because both views emitted a skos:prefLabel for that one IRI and SKOS allows
one per language. So the run keeps the first spelling seen, drops the other, and warns:
[WARN] <…/element/Task_1> is labelled 'Ship order' in 'a.bpmn' and 'ship order' in 'b.bpmn'. Both mean
the same thing — the id folds case and punctuation — so 'Ship order' was kept and the other dropped,
because a resource can carry only one skos:prefLabel per language. Spell it the same way in both sources
to choose for yourself.
Output is still written. First-seen wins, so the choice depends on input order — spell it the same way in both sources if you want to make the choice yourself.
The error, when there is one, says which kind it is, because the remedies differ: a type disagreement needs distinct ids or separate models, whereas genuinely different labels need either agreement or separation.
How metadata flows into RDF¶
In provenance graph (BPMN converter)¶
<.../bpmn/order-fulfillment/graph/provenance> {
<.../bpmn/order-fulfillment>
dct:source "order-fulfillment.bpmn" ; # source filename
dct:title "Order Fulfillment Process" ; # ← from index
dct:creator "Commerce Team" ; # ← from index (author)
owl:versionInfo "1.0" ; # ← from index
dct:created "2024-03-15" ; # ← from index
dct:modified "2025-11-20" ; # ← from index
dct:description "End-to-end flow" ; # ← from index
adms:status
<http://purl.org/adms/status/Completed> . # ← from index (status)
}
Every predicate above carries exactly one value, and that is the point. The conversion timestamp used
to be emitted as a second dct:created beside the author's date, and the converter's own name as a
second dct:creator beside the author — one predicate, one subject, two answers. Both facts describe
the run rather than the diagram, so they moved to the PROV activity and its agent. See
ADR 0008.
Those two facts live in the same graph, as PROV, describing the run instead of the diagram:
<.../bpmn/order-fulfillment/graph/provenance> {
<{base}provenance/run/9c2f1e04> a prov:Activity ;
prov:startedAtTime "2026-06-28T12:07:57Z"^^xsd:dateTime ;
prov:endedAtTime "2026-06-28T12:07:59Z"^^xsd:dateTime ;
prov:wasAssociatedWith <{base}provenance/agent/7d1c40ba> ;
prov:used <{base}provenance/source/group-models/8c44d1a4e9f0/models-bpmn-order-fulfillment-bpmn> .
<{base}provenance/agent/7d1c40ba> a prov:SoftwareAgent, schema:SoftwareApplication ;
schema:name "bpmn2linkedarchi" ; # which converter of the image ran
schema:softwareVersion "1.3.0" ;
dct:identifier "registry.gitlab.com/…/converters@sha256:89ab…" .
<.../bpmn/order-fulfillment>
prov:wasGeneratedBy <{base}provenance/run/9c2f1e04> ;
prov:wasDerivedFrom <{base}provenance/source/group-models/8c44d1a4e9f0/models-bpmn-order-fulfillment-bpmn> .
}
The run, agent and source nodes sit under {base}provenance/, not under this model — they are shared
across every model and run, so one file at one commit is one node and one run is one activity. Only the
statements about them are per-model. See
ADR 0008 §2.
Current metadata support per converter¶
| Converter | Index metadata → provenance | Notes |
|---|---|---|
| BPMN | ✓ Full | All fields emitted (title, author, version, created, modified, description, status) |
| PlantUML | ✓ Full | All fields emitted (title, author, version, created, modified, description, status) |
| Structurizr | ✗ | Does not use --diagrams-index |
| Backstage | ✓ Full | All fields emitted (title, author, version, created, modified, description, status) |
| ArchiMate | ✗ | Does not use --diagrams-index (single-file converter) |
Metadata is emitted by BaseLinkedArchiEmitter.emitProvenance(), which every
index-aware converter calls with the resolved ModelMetadata. BPMN builds the same triples in its
own emitter, because it overlays the dc:/dcterms: fields embedded in the BPMN file first.
Shared index between convert and render¶
Both the convert and render commands read the same index file:
# Convert: uses id for IRI minting, status for filtering and provenance, metadata for provenance
plantuml2linkedarchi convert models/*.puml \
--diagrams-index models/diagram-index.yaml -o out/model.trig
# Render: uses id for SVG filename, status for filtering
plantuml2linkedarchi render models/*.puml \
--diagrams-index models/diagram-index.yaml -o out/svg/
Usage with --diagrams-root¶
When files are in subdirectories, use --diagrams-root to resolve relative paths:
models/
├── diagram-index.yaml
├── processes/
│ └── checkout.bpmn ← file: processes/checkout.bpmn
└── events/
└── payment-event.bpmn ← file: events/payment-event.bpmn
bpmn2linkedarchi convert models/**/*.bpmn \
--diagrams-root models/ \
--diagrams-index models/diagram-index.yaml \
-o out/bpmn.trig
When --diagrams-root is omitted, paths resolve against the directory holding the index
file.
How file: is matched¶
An input file is matched against the index by its path relative to the root, falling back
to its bare file name. Both spellings work, so an index for a flat directory can say
file: checkout.bpmn while one for a tree says file: processes/checkout.bpmn, and
equivalent spellings of one path (./processes/checkout.bpmn) match too.
The bare-name fallback only applies when that name identifies exactly one entry. Given
processes/x.bpmn and events/x.bpmn, a lookup for x.bpmn matches neither rather than
guessing.
Every command applies these rules identically — they live in one DiagramSelection in
core — and warns when an index selected nothing at all:
That warning is the thing to look for when a run reports Converted OK — 0 triples.
Lifecycle states¶
status: says where a diagram is in its life. It describes the diagram; it does not decide whether
the diagram exists. Every state is converted and rendered, and each one reports itself in the
graph as adms:status, so a consumer that wants only current diagrams filters on that.
status: |
Converted and rendered | adms:status |
|---|---|---|
draft |
yes | adms/status/UnderDevelopment |
in-review |
yes | adms/status/UnderDevelopment |
publish (default) |
yes | adms/status/Completed |
deprecated |
yes | adms/status/Deprecated |
archived |
yes | adms/status/Withdrawn |
Converting everything is the safer default in both directions. A diagram that has been released
cannot simply vanish when it falls out of favour, because documentation, other models and
downstream graphs already link to its IRIs — so deprecated and archived stay and say what they
are. And a diagram nobody has released yet is still worth having in a graph that reviewers and
tooling can read, marked plainly as unfinished rather than absent.
Withholding a diagram is therefore an explicit request:
--exclude-states. To take one out permanently, remove its
entry.
obsolete is accepted as a synonym for deprecated, and published for publish. Case and the
choice of -, _ or a space are ignored, so In Review and in-review are the same state — for
status: and for --exclude-states alike.
What each state means¶
draft and in-review are both unreleased, and both carry the same adms:status, so the
difference between them is neither what gets published nor what the graph says — it is whose turn
it is.
draft— work in progress. The author is still deciding what the diagram says, and nobody is waiting on it.in-review— submitted, and waiting for someone else to validate it.
The distinction still matters even though both are converted: it drives the transitions below, and it is what an author excludes by name when a build must not publish unreleased work.
That makes in-review the gate into publication, and the only state a published diagram can
return to without being retired. A diagram that needs re-validation goes back to in-review, not
to draft: it is already public, and "unfinished" is not something a public resource can be.
The legal moves, in full:
| From | May become |
|---|---|
draft |
in-review |
in-review |
draft, publish |
publish |
in-review, deprecated, archived |
deprecated |
publish, archived |
archived |
publish, deprecated |
Two moves are refused, for one reason each:
- Anything published back to
draft. Its IRIs are public. Send it toin-reviewinstead. draftstraight topublish. That skips the validationin-reviewexists to require.
Nothing is a dead end: a deprecated or archived diagram can be reinstated without editing anything by hand.
Transitions are checked against identity-lock.yaml, which records the state each entry was last
seen in alongside the ids it was published under. A state the record has never seen is not a move,
so an index adopting states for the first time passes freely and the run after it starts enforcing
— the same bootstrap the identity check has, and no migration step:
[ERROR] Conversion failed: Illegal state change for 'order-flow.puml': 'publish' cannot become
'draft'. 'publish' has been published, and a published diagram cannot become work in progress
again — its IRIs are already public. Send it to 'in-review' instead. Allowed from 'publish':
in-review, deprecated, archived. Move through one of those, or pass --allow-state-change.
--allow-state-change accepts the move and updates the record, for the case where the lifecycle is
in the way rather than helping.
Withholding something that was already published¶
Moving a live diagram to a state the build excludes takes it out of that build. That is a link breaking, so it is said out loud — once, on the run where it happens:
[WARN] 'order-flow.puml' was published as 'publish' and is now 'in-review', which this run
excludes, so it is left out and its IRIs stop resolving. Drop 'in-review' from --exclude-states to
keep publishing it while it is out of use.
It is a warning rather than an error because the author asked for it twice over: once by moving the diagram, once by excluding the state. Stop excluding the state to keep publishing the diagram while it is being validated, or accept the gap.
This only arises in a build that passes --exclude-states. Without it, sending a diagram back for
validation changes what the graph says about it and nothing else — the IRIs keep resolving, so
there is no link to break and nothing to warn about.
An unrecognised value fails the run, in status: and in --exclude-states:
[ERROR] Conversion failed: Unknown status 'publsih' for 'order-flow.puml' in
/repo/models/diagram-index.yaml. Accepted: draft, in-review, publish, deprecated, archived.
A status decides whether the entry is published and what the graph says about it, so an
unrecognised one is refused rather than treated as publishable.
The check is worth having because the failure it replaces was silent. status: used to be a free
string in which only the exact token draft had any effect, so status: Draft published a draft
and status: archived published an archived diagram as though it were current.
Validation runs over the whole index before anything is filtered out, so a typo on an excluded entry fails on the run that introduced it rather than months later on the run that first tried to publish it.
Withholding a state from a build¶
--exclude-states names the states to leave out. Everything else is converted:
# Every state, each carrying its own adms:status
plantuml2linkedarchi convert models/*.puml --diagrams-index index.yaml -o out/model.trig
# A release build that must not publish unreleased work
plantuml2linkedarchi convert models/*.puml --diagrams-index index.yaml \
--exclude-states draft,in-review -o out/release.trig
Exclusion rather than inclusion because the two fail in opposite directions. An inclusion list omits by default, so forgetting to name a state silently drops diagrams and the missing IRIs surface later as dead links. An exclusion list publishes by default, so forgetting to name one publishes something early — visible, and undone by a rerun.
Naming every state is refused. It would convert nothing and write an empty graph, which looks like a successful build:
[ERROR] Conversion failed: --exclude-states names every state (draft, in-review, publish,
deprecated, archived), so the run would convert nothing and write an empty graph. Leave at least
one state in.
Both convert and render resolve the state set identically, so a diagram in the graph has a
picture and a picture has a graph.
--include-states and --include-drafts are accepted and ignored, and the run says so. They
only ever added to the processed set, and that set is now everything, so anything they could have
named is already in. Reinterpreting --include-states as "only these" would quietly narrow the
output of every pipeline that passes it, so it does nothing instead:
[WARN] --include-states no longer changes anything: every state is converted by default, so draft
would have been included regardless. The flag is deprecated and ignored. To leave states out, use
--exclude-states.
What reaches the graph¶
A declared state is published as adms:status, whose
values are concepts from the ADMS status
vocabulary. A consumer that already reads DCAT-AP understands the
lifecycle without knowing anything about Linked.Archi:
<https://example.org/la/plantuml/order-domain/graph/provenance> {
<…/view/current> dct:title "Current Flow" ;
adms:status <http://purl.org/adms/status/Completed> .
<…/view/leaving> adms:status <http://purl.org/adms/status/Deprecated> .
}
The status sits on the view, beside that entry's title and dates, because it describes the
diagram the entry selected. A status: on the model: entry is a default for its views, so one
line can retire a whole model; it is emitted on each view rather than on the model. In the flat
schema, where there is no view, it lands on the model like every other entry-level field.
An entry that declares no status: is published and emits no adms:status. Silence is not an
author calling a diagram finished, so nothing is asserted on their behalf.
Architecture state¶
This section covers the index route
For every route across every notation and level — including element scope and ArchiMate, neither of which the index reaches — see Architecture state.
architectureState: says which reality a diagram describes: how the architecture is today, how it is
meant to end up, or a plateau in between. Without it a graph merges them — a target-state component
and its baseline counterpart become one resource making contradictory claims, and a query for "what
runs today" returns things nobody has built yet.
architectureState: |
arch:architectureState |
Meaning |
|---|---|---|
baseline |
arch:Baseline |
how the architecture exists today |
target |
arch:Target |
how it should exist once planned changes are realised |
transitional |
arch:Transitional |
a stable plateau on the way from one to the other |
The spelling variants the core ontology records as skos:altLabel are accepted too: as-is,
current and current-state for baseline; to-be, future and future-state for target;
transition, intermediate and intermediate-state for transitional. Case and the choice of -,
_ or a space are ignored, so As Is and as-is are one value.
Not the same thing as status:
The two are easy to confuse and answer different questions.
| Field | Question | Subject |
|---|---|---|
status: |
has this diagram been released? | the diagram, as a document |
architectureState: |
which reality does it describe? | the architecture |
A target-state diagram can be perfectly published — finished, reviewed, and describing a future
that does not exist yet. Neither field implies anything about the other, which is why they are
separate fields rather than one vocabulary. Writing draft under architectureState: fails the
run and says where it belongs.
Each level describes itself, and nothing is inherited. This is where it differs from status:,
where a model-level value is only a default and the model is never the subject. arch:architectureState
is defined on a Model as readily as on a view — the core ontology's own example uses a Model — so a
model saying baseline is a claim about the model, not a default for its views:
model:
id: order-domain
architectureState: baseline # the model is the baseline architecture
views:
- id: as-built
file: as-built.puml # says nothing of its own, and inherits nothing
- id: planned
file: planned.puml
architectureState: target # this diagram describes the target
A consumer asking about a view that says nothing of its own follows dct:isPartOf to its model. In
the flat schema an entry is a model, so the value there is the model's; there is no view for the
index to describe.
An unrecognised value fails the run, naming the entry:
[ERROR] Conversion failed: Unknown architectureState 'Targt' for 'planned.puml' in
/repo/models/diagram-index.yaml. Accepted: baseline, target, transitional. A state says which
reality the diagram describes, so an unrecognised one is refused rather than published as a link to
a term nothing defines. Note this is not the publication status: 'draft' and 'publish' belong under
'status:'.
Failing closed is the point of the field existing at all. The same statement can be written through
the generic annotation route —
links: { arch:architectureState: arch:Targt } — and there the typo is emitted as a link to an IRI
nothing defines, with no complaint, because a generic predicate has no set of values to check
against. This one does.
Saying nothing asserts nothing. Unlike status:, there is no default: a repository that has
never distinguished baseline from target is not thereby claiming everything is baseline.
PlantUML sources can declare it in the file as well, with '!la-architecture-state. If both the file
and the index declare one they must agree — baseline and target are opposite claims about one
diagram, so a disagreement is refused rather than settled by precedence, which is where this differs
from a prefix declared twice.
Naming a successor¶
A status says a diagram is on its way out. supersededBy: says where to go instead, which is the
half of a deprecation a reader actually needs:
model:
id: order-domain
views:
- id: order-flow-v1
file: order-flow-v1.puml
status: deprecated
supersededBy: order-flow-v2
- id: order-flow-v2
file: order-flow-v2.puml
status: publish
<https://example.org/la/plantuml/order-domain/graph/provenance> {
<…/view/order-flow-v1>
adms:status <http://purl.org/adms/status/Deprecated> ;
dct:isReplacedBy <…/view/order-flow-v2> .
<…/view/order-flow-v2> dct:replaces <…/view/order-flow-v1> .
}
Not the same thing as formerIds:¶
The two look alike and mean opposite things, so it is worth being explicit:
| What it says | Triples | |
|---|---|---|
formerIds: |
one resource, previously published under another name | owl:sameAs, dct:replaces, dct:isReplacedBy |
supersededBy: |
two resources, one replacing the other | dct:isReplacedBy, dct:replaces |
supersededBy: deliberately emits no owl:sameAs. A successor is a different diagram, so claiming
the two IRIs denote one thing would merge them — and any reasoner would then merge their elements
too. Use formerIds: to rename a diagram, supersededBy: to replace one.
Rules¶
supersededBy: is read at the level the entry sits on, like formerIds:: on a view entry it names
another view of the same model, and in the flat schema another model. Succession across models
is not expressible, since the reference is resolved within the index.
Four things are refused, all so that following dct:isReplacedBy arrives somewhere real:
| Refused | Because |
|---|---|
| a successor not in the index | the reference would point at an IRI nothing mints |
| a successor in a withheld state | a default build publishes nothing for it |
| an entry naming itself | to rename a diagram, use formerIds: |
a cycle (v1 → v2 → v1) |
following the chain would never reach a current diagram |
supersededBy: also requires a retired status: — deprecated or archived. "Current, and also
replaced" is not a state a consumer can act on, and it usually means the two entries were swapped:
[ERROR] Conversion failed: supersededBy on a current entry in /repo/models/diagram-index.yaml:
'order-flow-v1.puml' is 'publish'. Only a retired diagram has a successor, so the entry needs
'deprecated' or 'archived'. Did the two entries get swapped?
Chains are allowed, so a diagram replaced twice keeps a trail: v1 archived → v2 deprecated →
v3 published.
Full rules in ADR 0004.