diff --git a/src/docs/arc42/04-solution-strategy/doc-04001-metamodel.adoc b/src/docs/arc42/04-solution-strategy/doc-04001-metamodel.adoc index f094550..df784c4 100644 --- a/src/docs/arc42/04-solution-strategy/doc-04001-metamodel.adoc +++ b/src/docs/arc42/04-solution-strategy/doc-04001-metamodel.adoc @@ -5,6 +5,7 @@ title: Architecture Metamodel status: draft owner: architecture-knowledge-toolkit maintainers created: '2026-06-25' +updated: '2026-09-21' reviewed: false generated: false summary: Defines the small metadata and relationship model that enables validated and generated architecture knowledge. @@ -76,7 +77,7 @@ suggestion until a reviewer accepts it. | `status` | yes -| Lifecycle state such as `draft`, `proposed`, `accepted`, or `superseded`. +| Review state of the artifact. See xref:artifact-lifecycle-status[] for the defined values and for the separate validity axis. | `owner` | yes @@ -155,6 +156,118 @@ traceability between artifacts. | Supporting documentation that participates in traceability. |=== +[[artifact-lifecycle-status]] +=== Artifact Lifecycle Status + +An artifact carries two lifecycle axes. `status` answers how confirmed the +artifact is, and `validity` answers whether it still holds. Both can be true at +once: an accepted risk that has been mitigated away is `status: accepted` and +`validity: retired`. Keeping the axes apart is decided in +xref:adr-009-artifact-retirement-and-relation-validity[]. + +The separation is not complete yet. `superseded` and `deprecated` are temporal +statements that still sit on the review axis, and ADR-009 deliberately left +them there instead of migrating them with the same decision, because that +migration overlaps with deriving `superseded_by` from an outgoing `supersedes` +relation. Read them as transitional values: every other `status` value is a +review verdict, and new temporal information belongs on `validity`. + +`status` is the review axis, apart from those two transitional values. + +[cols="1,3", options="header"] +|=== +| Status | Meaning + +| `draft` +| Work in progress. Incomplete notes or an artifact that is not yet a coherent +proposal. Not architecture truth. + +| `proposed` +| Complete enough to review, but not reviewed. AI-created artifacts start here. + +| `reviewed` +| A human has checked the content without deciding to adopt it. + +| `accepted` +| Adopted as the repository's architecture knowledge. Only reachable through +human review. For a `Risk` artifact this means the risk description is accepted +knowledge, not that the risk itself is deliberately carried; that belongs in the +risk's own acceptance section. + +| `rejected` +| Considered and intentionally not adopted. The artifact stays in the repository +as the record of an option that was turned down. + +| `superseded` +| Transitional, temporal. Replaced by a newer artifact, which is named in +`superseded_by`. The newer artifact carries the `supersedes` relation. + +| `deprecated` +| Transitional, temporal. Still in force, but discouraged for new work. Use it +where something is being phased out and can still be encountered, such as a +component, an interface, or a concept. Do not use it for an artifact whose +subject no longer exists at all; that is retirement on the validity axis. +|=== + +`validity` is the temporal axis. It is optional, and an absent value means +`active`. Validator and generator implement that reading explicitly; a schema +`default` is an annotation and materializes nothing. + +[cols="1,3", options="header"] +|=== +| Validity | Meaning + +| `active` +| The artifact still holds. This includes a risk that the team knowingly +carries: a deliberately accepted residual risk is still present, so it stays +active and records its acceptance in the risk artifact. + +| `retired` +| The artifact's subject no longer exists or no longer applies. The artifact +stays in the repository with its stable ID and its review status. +|=== + +A retired artifact records when and why: + +[cols="1,3", options="header"] +|=== +| Field | Meaning + +| `retired_on` +| Date of retirement, on or after `created`. + +| `retired_reason` +| Machine-readable reason, for filtering and generated views. +`no-longer-applicable` and `removed` apply to every artifact type. `Risk` +artifacts add `mitigated` and `materialized`. + +| `retired_note` +| Free text that explains the retirement to a human reader. The reason is for +machines, the note is for people; neither replaces the other. +|=== + +The three fields belong to retirement and are absent while an artifact is +active. + +Retirement never rewrites the relations that touch the artifact. A relation +keeps the relationship status it was reviewed with, because it stated something +that was true when it was made. `rejected` is a verdict about the claim itself, +for example that a decision never mitigated the risk it named, and not a way to +express that time has passed. + +A relation is inactive for the current architecture when either endpoint is +retired, not only its target. Generated impact and traceability matrices keep +rendering an inactive relation and mark it as such. Authoring a new relation to +a retired artifact stays allowed, because a historical link such as `documents` +or `supersedes` is legitimate; the validator warns about it so an accidental +reference to obsolete knowledge becomes visible. + +NOTE: The validity axis is decided in +xref:adr-009-artifact-retirement-and-relation-validity[] but not yet +implemented. `metamodel/artifact.schema.yaml` rejects unknown metadata fields, +so do not add `validity`, `retired_on`, `retired_reason`, or `retired_note` to +artifact metadata before the schema and the validator accept them. + === Relationship Types and Semantics Relationships are directed: `source --type--> target`. Relationship semantics diff --git a/src/docs/arc42/09-architecture-decisions/adr-009-artifact-retirement-and-relation-validity.adoc b/src/docs/arc42/09-architecture-decisions/adr-009-artifact-retirement-and-relation-validity.adoc new file mode 100644 index 0000000..a8fc2bd --- /dev/null +++ b/src/docs/arc42/09-architecture-decisions/adr-009-artifact-retirement-and-relation-validity.adoc @@ -0,0 +1,263 @@ +--- +id: ADR-009-artifact-retirement-and-relation-validity +type: ADR +title: Artifact Retirement and Relation Validity Over Time +status: accepted +owner: architecture +created: '2026-09-21' +updated: '2026-09-21' +reviewed: true +summary: Accepted decision to keep relationship status a review state and to express artifact retirement on a separate validity axis that generated matrices render as inactive. +derived_from: +- type: conversation + description: Maintainer question whether a mitigates relation must become rejected once the target risk no longer exists, and whether deprecated is the right status for a risk that is closed rather than discouraged. +tags: +- decision +- metamodel +- traceability +relations: +- type: refines + target: DOC-04001-metamodel + status: accepted + rationale: This ADR adds a validity axis to artifact metadata and fixes the meaning of relationship status when a relation target stops being current. + evidence: src/docs/arc42/04-solution-strategy/doc-04001-metamodel.adoc + reviewed: true +- type: addresses + target: QS-004-ai-suggestion-reviewability + status: accepted + rationale: Keeping review maturity separate from temporal validity preserves status as the marker that tells a reviewer whether content is suggested or accepted. + evidence: src/docs/arc42/10-quality-requirements/qs-004-ai-suggestion-reviewability.adoc + reviewed: true +- type: introduces_risk + target: R-004-heavy-metadata-authoring + status: accepted + rationale: A second lifecycle axis adds another optional metadata field that authors must understand and maintain. + evidence: src/docs/arc42/11-risks-and-technical-debt/r-004-heavy-metadata-authoring.adoc + reviewed: true +metadata_version: '1.0' +--- +[[adr-009-artifact-retirement-and-relation-validity]] +== ADR-009: Artifact Retirement and Relation Validity Over Time + +=== Status + +Accepted (derived). + +The rationale was AI-drafted from a maintainer conversation and then reviewed +and accepted by the accountable owner. The derived marker records that the +drafting was AI-assisted; the acceptance is human. + +=== Decision + +`relationshipStatus` stays a review state only. Whether an artifact is still +current is expressed on a separate validity axis in artifact metadata: +`validity` with the values `active` and `retired`, plus `retired_on`, the +machine-readable `retired_reason`, and the free-text `retired_note`. An absent +`validity` means `active`, and validator and generator implement that reading +explicitly rather than relying on a schema default, which is an annotation and +materializes nothing. + +A relation is inactive for the current architecture when **either** endpoint is +retired. An inactive relation keeps the relationship status it was reviewed +with and stays visible in generated impact and traceability matrices, marked +inactive, because it remains a true record of what was decided. + +Authoring a new relation that points at a retired artifact stays allowed. +Historical relations such as `documents` or `supersedes` are legitimate and a +reconstructed traceability link may well target retired knowledge. The +validator warns about such a relation so an accidental reference to obsolete +knowledge becomes visible instead of passing silently. + +Retirement means the subject is gone, not that it is deliberately carried. A +residual risk that the team knowingly accepts stays `validity: active`; its +acceptance belongs in the risk artifact, not on the validity axis. + +=== Context + +A relation such as `ADR-00X mitigates R-00Y` can outlive its target: the risk +is mitigated away, stops applying after an architecture change, or is merged +into another risk. The question is what happens to the relation, and what +happens to the risk artifact. + +`relationshipStatus` is defined in +xref:architecture-metamodel[the architecture metamodel] as a review state: +`proposed` is unreviewed, `reviewed` is checked by a human, `accepted` is taken +into the repository's architecture knowledge, and `rejected` is considered and +intentionally rejected. `rejected` is therefore a verdict about the claim, not +about time. Setting an accepted `mitigates` relation to `rejected` because the +risk is gone would retroactively deny a statement that was true and reviewed +when it was made. + +The artifact `status` enum in `metamodel/artifact.schema.yaml` offers `draft`, +`proposed`, `reviewed`, `accepted`, `rejected`, `superseded`, and `deprecated`. +The metamodel document lists these only as examples and never defines the +individual values, so `deprecated` has no repository-specific meaning; it +carries its ordinary document and API lifecycle sense of "still present, do not +use it for new work". A risk that no longer exists is not discouraged for +future use, it is closed. The enum also mixes two axes: review maturity +(`draft` through `rejected`) and temporal validity (`superseded`, +`deprecated`). Replacement is already modelled by `superseded` together with +the `superseded_by` field; retirement without a replacement has no +representation at all. + +For `Risk` artifacts the mixed axis additionally collides with domain +vocabulary: `status: accepted` means "accepted as architecture knowledge" in +this metamodel, while an accepted risk in risk management means "known and +deliberately carried". Both readings are plausible on the same artifact today. + +The generator in `scripts/validate-metamodel.rb` renders an artifact status +column and relation cells, but has no concept of a relation that was valid and +is not any more. + +=== Decision Drivers + +* Keep the history of an accepted relation intact instead of rewriting a + statement that was true when it was reviewed. +* Make retirement machine-readable so generated matrices can mark a relation + inactive without hiding it. +* Keep review maturity and temporal validity separately readable on the same + artifact. +* Avoid status vocabulary that means different things per artifact type. +* Keep the authoring burden small, because metadata weight is already tracked + by xref:r-004-heavy-metadata-authoring[]. + +=== Considered Options + +==== Option 1: Add a closure value to the shared status enum + +Add `closed` to the artifact `status` enum and use it for risks that no longer +exist. One new value, no new field, and the generator keys its inactive marker +on the target's status. + +==== Option 2: Separate validity axis + +Add an optional `validity` field with `active` and `retired`, together with +`retired_on`, a machine-readable `retired_reason`, and a free-text +`retired_note`. `status` keeps answering how confirmed an artifact is, +`validity` answers whether it still holds. The generator keys its inactive +marker on the validity of the relation's endpoints. + +==== Option 3: Prose-only closure note + +Record closure in the risk body, for example in `=== Decision or Acceptance`, +and leave metadata untouched. + +=== Pugh Matrix + +[.pugh-matrix] +[cols="2,1,1,1", options="header"] +|=== +| Criterion | Status value | Validity axis | Prose only +| Generated matrices can mark relations inactive | +1 | +1 | -1 +| Review maturity stays readable after retirement | -1 | +1 | +1 +| No vocabulary collision per artifact type | -1 | +1 | 0 +| Works uniformly for every artifact type | -1 | +1 | 0 +| Small schema and low authoring burden | +1 | -1 | +1 +| Retirement date and reason are validatable | 0 | +1 | -1 +s| Sum s| -1 s| 4 s| 0 +|=== + +=== Consequences + +==== Positive + +* An accepted relation stays accepted, so the traceability history remains a + truthful record of what was decided and why. +* Retirement is machine-readable, so generated impact and traceability + fragments can render the relation as inactive rather than dropping it. +* A retired risk keeps its review state, so reviewers can still see whether the + risk was ever accepted as architecture knowledge. +* The axis generalizes beyond risks to retired components, interfaces, + constraints, and quality scenarios. +* A generated risk register can filter open risks without interpreting review + vocabulary. + +==== Negative + +* The metamodel gains a second axis with four optional fields, which increases + the authoring surface tracked by xref:r-004-heavy-metadata-authoring[]. +* Relations to retired artifacts produce validator warnings, and without a way + to record a deliberate historical link those warnings can accumulate as + permanent noise. +* An artifact's state must be read from two fields instead of one. +* Validator, generator, templates, and the `example/metamodel/` schema copy all + have to learn the second axis, and consuming projects that vendored the + schema need a migration note. + +==== Neutral or Follow-Up + +* `superseded` and `deprecated` stay in the `status` enum for now. A follow-up + decision has to say whether they move to the validity axis or keep their + document and API lifecycle meaning. +* How the generator marks an inactive row, for example a marker column, a + footnote, or struck-through text, is a rendering decision made when the + generator changes. +* The artifact status table that the metamodel document was missing is written + with this decision, so every status value and both lifecycle axes have a + defined meaning in xref:architecture-metamodel[]. + +=== Impact + +include::generated/adr-009-artifact-retirement-and-relation-validity-impact.adoc[leveloffset=+2] + +=== Traceability + +include::generated/adr-009-artifact-retirement-and-relation-validity-attributes.adoc[] + +ifdef::derived_from_description[] +==== Derived from +{derived_from_description} +endif::[] + +include::generated/adr-009-artifact-retirement-and-relation-validity-traceability.adoc[leveloffset=+2] + +=== Assumptions + +* Retirement is a human decision recorded in the repository, like acceptance; + an agent may only propose it. +* Reviewers want a retired relation to stay visible as history rather than + disappear from the matrix. +* A small reason vocabulary is enough: `no-longer-applicable` and `removed` for + every artifact type, plus `mitigated` and `materialized` for risks. + +=== Open Questions + +These belong to the implementation slice. The decision holds regardless of how +they are answered; they shape the schema and the rendering, not the axis +separation. + +* Do `superseded` and `deprecated` remain status values, or migrate to the + validity axis? This overlaps with the derivation of `superseded_by` from an + outgoing `supersedes` relation. +* How does an author record that a relation to a retired artifact is + deliberately historical, so its warning does not become permanent build + noise? +* How does a generated matrix mark an inactive relation, for example a marker + column, a footnote, or struck-through text? + +=== Review Notes + +Accepted after review of the decision itself: that review maturity and temporal +validity are separate axes, and that a relation to a retired artifact keeps its +reviewed status instead of being rejected. + +The impact claims recorded as relations were accepted with the decision. Their +targets remain proposed artifacts in their own right; an accepted relation +states that the claim holds, not that the artifact it points at has been +accepted. + +Acceptance does not cover the implementation slice. +`metamodel/artifact.schema.yaml` carries none of the new fields yet, and the +generator has no inactive marker. Do not put the new fields into artifact +metadata before the schema and the validator accept them, and decide the +migration note for projects that vendored the schema before any schema change +is released. + +Amended after a convergence check against the implementation issue. The +amendment closed four semantic gaps that the first version had left to the +implementation: a relation is inactive when either endpoint is retired, not +only its target; a new relation to a retired artifact is allowed and warned +about rather than rejected; a deliberately carried residual risk stays active, +so residual acceptance is not a retirement reason; and the machine-readable +reason is separated from its explanatory note. The axis separation itself was +not reopened.