Skip to content

issue_101: Implement the artifact validity axis - #105

Merged
dieterbaier merged 5 commits into
mainfrom
issue_101
Sep 21, 2026
Merged

dieterbaier merged 5 commits into
mainfrom
issue_101

Conversation

@dieterbaier

@dieterbaier dieterbaier commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Closes #101.

Builds on #103, which is merged. This PR was stacked on issue_102 until then;
it now targets main and is rebased onto the integrated commits.

ADR-009 separated review maturity from temporal validity. Until now only the
documentation knew about it: metamodel/artifact.schema.yaml sets
additionalProperties: false, so an author who followed the metamodel document
broke validation. This makes the axis real.

issue_101: Implement the artifact validity axis

The schema owns the vocabulary. validity, retired_on, retired_reason
and retired_note are in both schema copies, and the validator reads the enums
from the schema exactly as it already reads relation types from
relations.schema.yaml. The two representations cannot drift, because there is
only one. The Ruby constants stay as a fallback for a checkout without the
schema, and a parity test holds them to the schema's values.

The one thing the schema does not express is that mitigated and
materialized belong to risks while no-longer-applicable and removed are
universal. A conditional per artifact type would be verbose and hard to extend,
so the value list lives in the schema and the type mapping lives in the
validator — one line, next to the rule that enforces it.

Invariants, where they can actually fail. A retired artifact must record
retired_on; an active one must not carry retirement fields; retired_on
cannot precede created; a risk-specific reason cannot appear on a Document.
An absent validity is read as active in code, because a JSON Schema default
is an annotation and materializes nothing.

Either endpoint. The matrices mark the ADR's outgoing row and the retired
risk's incoming row, since "active relation" has to mean the same thing from
both sides. The relation itself is never rewritten — status: accepted stays
accepted, and the marker is (inactive) on the relation type cell, carrying
the relation-inactive role so a renderer can style it.

Warn, don't reject. A new relation to a retired artifact validates and
warns, naming both endpoints.

issue_101: Teach authors the validity axis

The mechanism is useless if the guidance still describes one axis. Metadata
rules gain a validity section, relation rules state that a retired endpoint
never changes a relation's status, and the risk template shows retirement as
commented metadata rather than an example that would retire every new risk.

Both skills say retirement is a human decision an agent may only propose, and
that a residual risk the team knowingly carries stays active — the case where
the domain word and the metamodel word pull apart.

The README gains the migration note for projects that vendored the schema,
including the combination to avoid: a new schema beside an old validator
accepts the fields and enforces nothing.

issue_101: Resolve what the implementation answered

ADR-009 listed the matrix marker as open; this slice chose one, so the bullet
names it instead of still asking.

issue_101: Carry the transitional caveat into the schema

From the review on #103. The finding there — that the axis separation was
described as complete while superseded and deprecated stayed on status —
applies to every place that states the split. The schema's status description
and skills/references/metadata-rules.md made the same clean claim and now name
the two transitional values.

The schema matters most: a project that vendors it reads the schema, not the
arc42 chapter, so the exception has to be visible where it is most likely to be
met.

issue_101: Decide by presence, as the schema does

From the review. The validator used blank? where the schema decides by
property presence, so validity: "" read as active and empty retirement fields
slipped through — metadata could pass the build while violating the schema that
consumers vendor. The validator now rejects present-but-blank values as the
schema does; retired_note gains minLength: 1 so the schema states the same
rule for an empty explanation. A fourteen-case table asserts the schema's
verdict for present, empty, and null values, naming the deciding rule.

Convergence Check — authoritative, against 11176ab

Run immediately before integration, against the commit being integrated. It
replaces the provisional results; those recorded against aff0dc4, 556548b
and 5d87be0 were void. No commit landed after 11176ab, local and remote
heads are identical, CI validate is green, no pull request depends on this
branch, and the review re-checked 11176ab with no further findings.
After #103 was rebase-merged, the branch was rebased onto main; the four
commits are unchanged in content, and main at ecf8729 is tree-identical to
the old issue_102 tip, so nothing from #103 was lost or duplicated.

Result: Converged.

# Question Answer
1 Intent and scope Passed (assisted). All twelve acceptance criteria of #101 are met. Two decisions the issue asked for were made — the reason enum lives in the schema with the type mapping in Ruby, and the marker is (inactive) with a role. One scope item was deferred, see below.
2 Behaviour Passed (deterministic for the bridge). Eleven new scenarios across features/metamodel-validation.feature and features/documentation-generation.feature, eleven bridged tests named after them. No waiver taken. The review fix adds its own proof: restoring the old blank? logic fails the new scenario and the parity table. Mutation check: neutering mark_inactive and the retirement branch fails seven of them, so the guards have a real failure path rather than reading like coverage.
3 Architecture impact Passed (assisted). The metamodel document is the only architecture artifact touched, and only to drop the note this slice makes false and to name the marker. No new artifact, no relation change.
4 Decisions Passed (human tier, applied). No new ADR: this implements ADR-009 rather than deciding anything structural. The two implementation choices are recorded where they apply — the schema-versus-Ruby split as a comment next to the mapping, the marker in the metamodel document. Overrule welcome on either.
5 Traceability Passed (deterministic for the relation path). #102 → ADR-009 → #101 → ten scenarios → ten tests → three commits → this PR. Validation reports no dangling targets.
6 Implementation and verification Passed (deterministic). See below.
7 Documentation and delivery state Passed (assisted). The review finding from #103 is carried into the schema and the authoring rules, so the three places that describe the axes agree. The stale note is gone, the answered open question names its answer, and the deferred one points at #104.

Findings

Finding Tier Disposition
Ruby validation treated present-but-blank values as absent where the schema rejects them (review) deterministic fixed in 11176ab, with a verdict-parity table and a new scenario
Residual risk: parity expectations are hand-derived assisted The table's verdicts are written from the schema, not computed by a JSON Schema engine, because the pinned toolchain declares none. The image's ajv 6.12.6 is an incidental Debian package without draft 2020-12 support, so no guard was built on it. Reported rather than blocked: it would appear identically on any schema change.
The warning for a deliberately historical relation has no suppression mechanism, so intentional backward links would warn forever assisted deferred to #104. No artifact is retired yet, so the warning volume it must be weighed against is zero, and the obvious fix is a fifth field on a model whose weight is already tracked as R-004-heavy-metadata-authoring.

Verification

Run Result
./build.sh test Ruby 43 runs, 207 assertions, 0 failures; CLI 4 runs, 13 assertions, 0 failures; JS 41 pass, 0 fail
./build.sh check-adapters Generated agent adapters are current
./build.sh build Errors: 0. 38 warnings, all the pre-existing bidirectional-relation false positives tracked as #93
End-to-end CLI, retired risk plus an ADR that mitigates it Validation passes with one warning naming both endpoints; the impact fragment renders the relation marked inactive; deleting retired_on fails the run with a message naming the file and the field

Dieter Baier added 4 commits September 21, 2026 19:16
ADR-009 separated review maturity from temporal validity. The schema and
the validator only knew the review axis, so an author following the
metamodel document broke validation.

The artifact schema now carries validity, retired_on, retired_reason and
retired_note, and the validator reads the vocabulary from it exactly as
it already reads relation types from the relation schema. Drift between
the two representations is prevented by construction rather than by a
test; the constants remain only as a fallback for a checkout without the
schema, and a parity test holds them to the schema's values.

The invariants the axis needs are enforced where they can actually fail:
a retired artifact must record retired_on, an active one must not carry
retirement fields, retired_on cannot precede created, and a risk-specific
reason cannot appear on another artifact type. An absent validity is read
as active in code, because a JSON Schema default annotates without
materializing anything.

A relation is inactive when either endpoint is retired, not only its
target, so the matrices mark both the ADR's outgoing row and the retired
risk's incoming row. The relation itself is never rewritten. A new
relation to a retired artifact warns instead of failing: documents and
supersedes legitimately point backwards, while a silent accept would hide
stale references.

Ten scenarios, ten bridged tests. Breaking the marker and the retirement
branch fails seven of them, so the guards have a real failure path.
The mechanism is useless if the guidance still describes one axis. The
metadata rules gain the validity section, the relation rules state that a
retired endpoint never changes a relation's status, and the risk template
shows retirement as commented metadata rather than an example that would
retire every new risk.

Both skills now say that retirement is a human decision an agent may only
propose, and that a residual risk the team knowingly carries stays active
— the case where the domain word and the metamodel word pull apart.

The metamodel drops its not-yet-implemented note and names the marker the
generator writes. The README gains the migration note for projects that
vendored the schema, including the one combination to avoid: a new schema
beside an old validator accepts the fields and enforces nothing.
The ADR listed the matrix marker as open; this slice chose one, so the
bullet now names it instead of still asking. The suppression mechanism
for deliberately historical relations stays open and points at its
follow-up, because no artifact is retired yet and the warning volume it
would be weighed against does not exist.
The review on the metamodel applies to every place that states the split.
The schema's status description and the authoring rules made the same
clean claim, so both now name superseded and deprecated as the two
temporal values that stayed behind.

A vendoring project reads the schema, not the arc42 chapter, so the
caveat has to live there too or the exception is invisible where it is
most likely to be met.

@dieterbaier dieterbaier left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall implementation follows ADR-009 well: both endpoints participate in derived relation validity, accepted relation status is preserved, the schema copies match, the generator keeps historical relations visible, and the guidance/migration note are aligned. The current Validate run is green.

One finding to resolve before merge: schema and Ruby validation still diverge for present-but-blank values.

The schema treats these as present properties:

  • validity: "" is not in the enum and is invalid;
  • on an active artifact, retired_on: null, retired_reason: "", or retired_note: "" are rejected by the else branch because the properties exist;
  • on a retired artifact, retired_reason: "" is rejected by its enum.

The Ruby path instead uses blank? in all three places. It therefore treats blank validity as absent/active, ignores blank retirement fields on active artifacts, and accepts a blank reason on a retired artifact. Since ./build.sh validate exercises the Ruby path rather than a JSON Schema validator, metadata can pass the build while violating the schema distributed to consumers.

This is exactly the semantic drift the parity acceptance criterion is meant to prevent; the current parity tests compare vocabulary/structure but not validation outcomes.

Please either:

  1. make the Ruby validator property-presence based where the schema is property-presence based (recommended), and add cases for empty string/null; or
  2. relax the schema deliberately to match the Ruby semantics and document that blank values count as absent.

I recommend rejecting present-but-blank metadata: it keeps “omit while active” literal and avoids silently normalizing malformed YAML.

Comment thread scripts/validate-metamodel.rb Outdated
From the review. The validator used blank? in the three places where the
schema decides by property presence, so validity: "" read as active, an
active artifact with retired_note: "" passed, and a retired one with an
empty reason passed. The build runs the Ruby path, not a JSON Schema
engine, so metadata could pass the build while violating the schema that
consumers vendor — the drift the parity criterion exists to prevent.

The validator now rejects present-but-blank values the way the schema
does, and names null and empty string separately because they are
different YAML mistakes. retired_note gains minLength: 1 so the schema
states the same rule instead of accepting an empty explanation. The Ruby
check mirrors that exactly and does not strip whitespace, which would
have been drift in the other direction.

The existing parity tests compared vocabulary, not verdicts. A table now
runs fourteen present, empty, and null cases through the validator and
asserts the verdict the schema prescribes, naming the deciding schema
rule. Restoring the old blank? logic fails it and the new scenario.

@dieterbaier dieterbaier left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-check against 11176ab: the finding is resolved. Ruby now follows schema property-presence semantics for validity and all retirement fields, distinguishes null/empty values in actionable errors, and mirrors retired_note's new minLength: 1. The 14-case outcome table covers the reported boundary cases, both schema copies remain aligned, and the current Validate run is green. The documented limitation that expected schema outcomes are hand-derived rather than executed by a pinned Draft 2020-12 engine is real but not a blocker for this change. No further findings from this review.

@dieterbaier
dieterbaier merged commit c8b317d into main Sep 21, 2026
1 check passed
@dieterbaier
dieterbaier deleted the issue_101 branch September 21, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the artifact validity axis decided in ADR-009

1 participant