Skip to content

ADR 0010 — What names a PlantUML element

Status: Accepted, implemented

Scope: how the local ID of an arch:Element is derived for the PlantUML converter, which decides when two shapes are one resource and what happens to an element's IRI when its label is edited.

Depends on: ADR 0002 for why views of one model share a namespace; ADR 0005 for relationship IDs, which are composed from element IDs and therefore inherit whatever this decides; ADR 0003 for the principle that a published identity does not change silently — a principle this ADR invokes without being able to use its mechanism.

Context

Most converters read an element's identifier out of the source: BPMN takes the id attribute, ArchiMate Exchange the identifier, Structurizr the workspace JSON id, LeanIX a UUID. PlantUML looks like it has nothing to read, because a .puml is a text file with no id attributes. It does have something to read.

A PlantUML entity has two names, and they do different jobs:

class "Payment Gateway" as PayGw

Payment Gateway is the display name — what the picture shows a reader. PayGw is the code — what the source refers to the entity by, in every arrow and in every annotation. PlantUML sets the code to the display text when the author writes no as clause, so it is always populated: class OrderService has display name and code both OrderService.

One of those two is an identifier and the other is a label. The question is which the IRI is built from.

Options

A. The display name

Slugify what the picture shows: class "Payment Gateway" as PayGw mints …/element/Payment_Gateway.

Attractive because it needs nothing but the text a reader already sees, and because the same slug feeds skos:prefLabel, so there is one string to reason about.

It fails on both properties an identifier needs.

It is not unique. Nothing in PlantUML constrains the display text, so two entities may show the same words:

class "Order Service" as Primary
class "Order Service" as Replica
Primary --> Replica : replicates

Keyed on the display name both mint …/element/Order_Service. Two distinct entities collapse into one resource carrying both their types and both their labels, and the arrow between them becomes a self-loop whose arch:source and arch:target are the same IRI. The graph says a component replicates itself. Nothing reports it, because from the converter's side one id was minted twice and that is indistinguishable from one element drawn twice — which is a merge it is supposed to perform (ADR 0002).

It is not stable. A display name is prose, edited for the reader's benefit. Retitling the box to "Payment Gateway (EU)" is a presentation change that relocates …/element/Payment_Gateway to …/element/Payment_Gateway_EU, and relocates every relationship composed from it, and every view node, and every SVG href. The old addresses stop existing. Every inbound link from documentation or from a downstream graph breaks, on an edit no author would think of as structural.

B. The code

Slugify what the source refers to the entity by: the same declaration mints …/element/PayGw.

It is unique, and PlantUML enforces it. An arrow endpoint is resolved by code, so two entities cannot share one — declaring the same code twice declares one entity, which PlantUML merges itself. The uniqueness an identifier needs is a guarantee the notation already provides, rather than a property to hope for. The example above becomes …/element/Primary and …/element/Replica, and the arrow runs between two resources.

It is stable against editing a label. as PayGw does not move when the box is retitled, so the IRI does not either. The display name remains the skos:prefLabel, which is where a reader's name belongs.

The cost: an IRI reads less well. …/element/PayGw is more cryptic than …/element/Payment_Gateway, and where an author picked a terse alias the address inherits it. That is a legibility cost on a value that is an address, paid to remove a correctness failure and a stability failure.

C. The code, with the display name as a fallback

B, and where the code is absent use the display name.

This is B in practice, because PlantUML populates the code from the display text on its own. Stating the fallback anyway costs one expression and covers the one entity kind that has no display text at all: [*] in a state machine, whose code is region-scoped (*start*, *start*Running) so the two [*]s of a machine with a nested region stay distinct while both read "initial".

Decision

C.

element id = slug(code)                 when the entity has a code
           = slug(display name)         otherwise

slug is LabelIdentity.slug: anything outside [A-Za-z0-9_\-.] becomes _, runs collapse, leading and trailing _ are trimmed, and an empty result becomes element. Case is preserved, because OrderService is a name someone capitalised deliberately.

The slug is applied to the code as well as to the display name. A code is only usually free of characters an IRI segment cannot hold — an author who writes participant "Book API" with no as clause has a code containing a space.

Both names resolve in an annotation. An author writing '!la-link may reach for either, and the element is registered under its code, its display name and its minted id. An ambiguous name — one entity's code equal to another's display name — is refused rather than guessed at.

Consequences

  • An element IRI survives a relabel. Editing a display name changes one skos:prefLabel and nothing else. This is the property the whole decision is for.
  • Two entities that look alike stay two resources, and an arrow between them stays an edge between two IRIs.
  • Relationship IRIs inherit both properties, since ADR 0005 composes them from element IDs. A relabel no longer moves the arrows attached to the box.
  • An IRI is only as legible as the alias an author chose. …/element/PayGw is the address; the readable name is one skos:prefLabel away, and skos:notation carries the id itself.
  • A terse alias is now a published decision. Choosing as A over as AlphaApi picks the address, so an alias is worth naming with the same care as a model id.
  • An over-long address comes from a long code, not a long label. An author who names a participant after a URL and gives it no alias has that URL as the code, which is the case ADR 0005 bounds relationship segments for. Giving such a participant a short alias keeps both the element and its relationships small.
  • Two elements can still share an id in one file, in the one case PlantUML does not prevent: a nested namespace a.b is reported as one group per segment, so a.shared and c.shared both yield shared. The converter reports this against the file that contains it rather than leaving it to the cross-view collision check, whose message is about views disagreeing and which aborts the whole run.

On the identity change, and why there is no lock for it

Changing what names an element re-addresses published IRIs, and unlike a model or view rename it cannot be declared. IdentityLock records modelId, viewId and lifecycle state only, so nothing detects an element ID change and --allow-identity-change has nothing to authorise; formerIds: is read at the model and view level and has no per-element channel.

That is accepted rather than solved, for the reason ADR 0005 accepts the same gap: extending the lock to every element in every model would make it a full inventory of the graph, which is a different artefact with different costs. The migration is a republish — a consumer holding …/element/Payment_Gateway for an aliased shape re-resolves to …/element/PayGw.

An owl:sameAs bridge is derivable here, unlike for relationships: the old id is slug(displayName) and the parser holds both names, so an alias triple could be emitted for every element whose two names disagree. It is not emitted, because an alias asserted from a rule rather than from an author's declaration would claim that every such pair was a rename — and for a shape whose display name was never published, or whose old IRI denoted two entities at once, that claim is false. Recorded as an open question below.

Example

@startuml
participant "Book API" as BookApi
participant Ledger
database "Ledger Store" as LedgerDb

BookApi -> Ledger : post(entry)
Ledger -> LedgerDb : persist(entry)
@enduml
<…/plantuml/books/element/BookApi>   a uml:Lifeline ; skos:notation "BookApi"   ; skos:prefLabel "Book API"@en .
<…/plantuml/books/element/Ledger>    a uml:Lifeline ; skos:notation "Ledger"    ; skos:prefLabel "Ledger"@en .
<…/plantuml/books/element/LedgerDb>  a uml:Lifeline ; skos:notation "LedgerDb"  ; skos:prefLabel "Ledger Store"@en .

<…/plantuml/books/relationship/BookApi__Ledger__post_entry__…> a uml:Message ;
    arch:source <…/plantuml/books/element/BookApi> ;
    arch:target <…/plantuml/books/element/Ledger> .

Ledger has no as clause, so its code is its display text and the two names coincide. BookApi and LedgerDb are aliased, so the address is the alias and the label is the prose.

Retitling "Book API" to "Book API v2 (EU)" changes exactly one triple — the skos:prefLabel. Every IRI above holds.

Naming an element in an annotation

Both names resolve, so an annotation can use whichever the author has in hand:

'!la-prefix skos: http://www.w3.org/2004/02/skos/core#
'!la-prefix arch: https://meta.linked.archi/core#
'!la-prefix kg: https://example.org/graph/
@startuml
participant "Book API" as BookApi

'!la-link BookApi skos:exactMatch kg:APP-14
'!la-link "Book API" arch:conceptOwner kg:team-ledger
@enduml

Both statements land on …/element/BookApi — the first naming it by code, the second by display name, quoted because it contains a space:

<…/plantuml/books/element/BookApi>
    skos:exactMatch    <https://example.org/graph/APP-14> ;
    arch:conceptOwner  <https://example.org/graph/team-ledger> .

The code is the better one to write. It is what the rest of the source already uses, and it is the name that does not move when the label is edited — an annotation keyed on the display name has to be revisited when someone retitles the box.

The same holds for the diagram index, which names elements the same way:

views:
  - id: book-flow
    file: book-flow.puml
    elements:
      BookApi:
        links: { skos:exactMatch: kg:APP-14 }

Open questions

  • Whether to emit a derived owl:sameAs for relocated element IRIs. Mechanically possible, and declined above because a rule-derived alias overstates what is known. Revisit if a corpus turns out to have published aliased-element IRIs widely enough that a republish is not viable.
  • Whether a nested namespace a.b should compose its segments into the group id (a.b rather than b), which would remove the one remaining in-file collision. It is a separate identity change affecting group elements only, and is reported rather than silently merged in the meantime.