Skip to content

ADR 0003 — Changing a published identity

Status: Accepted, implemented

Scope: detecting and authorising a change to a model or view ID that has already been published, and recording the change in the graph.

Depends on: ADR 0001 for where an identity is declared, ADR 0002 for which resource it names.

Context

A model ID is the IRI namespace of every element, relationship and view in the model. A view ID names an arch:View and the SVG rendered from it. Changing either relocates published addresses: every inbound link from documentation, from another model, or from a downstream graph resolves to a resource that no longer exists.

Nothing prevented such a change or recorded that it happened. Renaming an entry in a diagram index produced a new namespace and left the previous one unreferenced, with no error and nothing in the output relating the two.

The converter cannot detect the change from its inputs alone. An index states the identity a model has now; it carries no record of the identity it had when it was last published. Detection therefore requires a record of what was published.

Options

A. Declaration only: formerIds

The author lists superseded IDs on the entry. The converter emits the alias triples and needs no prior state.

Detection is not possible: an author who renames an ID and does not add formerIds gets the same silent relocation as before. The mechanism authorises a change but does not require the authorisation.

B. Recorded state: an identity lock file

The converter writes the IDs it published to a file committed alongside the index, and compares the current run against it. A file whose ID has changed is an error unless the run is explicitly allowed to change it.

This detects the case A cannot, at the cost of a generated file in the repository and a flag to update it. The precedent is a dependency lock file: state that is generated, committed, reviewed, and updated deliberately.

C. Compare against the previously published artifact

Read the last published .trig and compare its model IRIs. No new file, but it requires the previous artifact to be available at conversion time. In this pipeline the artifact is a build output, regenerated from scratch and not committed in the source repository, so it is not reliably present.

Decision

Adopt B, with A as the way to authorise a change.

  1. The identity lock is identity-lock.yaml, written next to the diagram index and committed. It records, per source file, the model and view IDs the converter last published. It also records the entry's lifecycle state, added by ADR 0004 for the same reason: a transition, like a rename, can only be detected against a record of what came before.
  2. A run whose resolved identity differs from the lock fails, naming the file, the recorded ID and the current one.
  3. --allow-identity-change authorises the run: the lock is rewritten and the change proceeds.
  4. formerIds: on an entry declares that the previous ID is superseded rather than replaced by accident. When it covers the recorded ID, the run proceeds without the flag. It is read at the level it sits on: on the model: entry it supersedes model IDs, on a view entry view IDs, and in the flat schema the entry id is the model ID so its formerIds: are model IDs.
  5. Without a lock file, the first run writes one and reports nothing. Adopting the mechanism therefore does not require a migration step.
  6. The declaration, not the record, is what the output publishes about a superseded ID:
    • convert emits the alias triples on every run for as long as formerIds: names them. The lock cannot serve as the source, because the run that performs a rename also rewrites the lock, so every later run would publish the model without the alias.
    • render writes the SVG under each superseded view ID as well as the current one, because a view ID names the file and a rename therefore relocates a published image URL. A superseded model ID does not name a file and produces alias triples only.
    • --allow-identity-change authorises a change without describing it, so it produces no aliases. A rename that must stay resolvable is declared under formerIds:.

An alias is not a second identity: it resolves to the current resource and is never minted for new content. It points backwards only. 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.

Consequences

  • Renaming a published model or view is a deliberate act with a reviewable diff in identity-lock.yaml, rather than an invisible consequence of editing an index.
  • A consumer holding a previous IRI can follow owl:sameAs or dct:isReplacedBy to the current one, provided the change was declared with formerIds.
  • A consumer holding a previous image URL keeps resolving it, at the cost of one duplicated SVG per superseded view ID in the output directory.
  • The lock file must be committed. A pipeline that discards it loses detection and reverts to the behaviour described in Context.
  • --allow-identity-change exists for the case where the previous IRI genuinely does not matter, such as a model that has never been published outside the repository. It suppresses detection for that run only.
  • Detection is per index. Two repositories renaming into each other's IDs remains the aggregation repository's concern (ADR 0001, decision 2).

Example

An index that renames a view, without declaring the change:

model:
  id: order-domain
views:
  - id: fulfilment            # previously published as order-fulfillment
    file: order-fulfillment.bpmn
    status: publish
[ERROR] Conversion failed: identity change for 'order-fulfillment.bpmn': view id was published
as 'order-fulfillment', the index now declares 'fulfilment'. Declare the previous id under
formerIds:, or pass --allow-identity-change.

Declaring the change instead:

model:
  id: order-domain
views:
  - id: fulfilment
    formerIds: [order-fulfillment]
    file: order-fulfillment.bpmn
    status: publish

The run proceeds, the lock is updated, and the superseded IRI is emitted as an alias:

<https://example.org/la/bpmn/order-domain/graph/provenance> {
    <https://example.org/la/bpmn/order-domain/view/fulfilment>
        dct:replaces <https://example.org/la/bpmn/order-domain/view/order-fulfillment> .

    <https://example.org/la/bpmn/order-domain/view/order-fulfillment>
        owl:sameAs       <https://example.org/la/bpmn/order-domain/view/fulfilment> ;
        dct:isReplacedBy <https://example.org/la/bpmn/order-domain/view/fulfilment> .
}

render reads the same declaration and writes both names, so the previously published image URL resolves to the same picture:

Rendered: order-fulfillment.bpmn → out/svg/fulfilment.svg
Also wrote superseded name: order-fulfillment.svg

Open questions

  • Should a formerIds entry expire, so aliases do not accumulate indefinitely in the graph? Removing the declaration already retires both the alias triples and the superseded SVG; what is undecided is whether the converter should require that retirement rather than leave it to the author.