issue_101: Implement the artifact validity axis - #105
Conversation
aff0dc4 to
556548b
Compare
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.
556548b to
5d87be0
Compare
dieterbaier
left a comment
There was a problem hiding this comment.
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: "", orretired_note: ""are rejected by theelsebranch 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:
- make the Ruby validator property-presence based where the schema is property-presence based (recommended), and add cases for empty string/null; or
- 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.
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
left a comment
There was a problem hiding this comment.
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.
Closes #101.
Builds on #103, which is merged. This PR was stacked on
issue_102until then;it now targets
mainand 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.yamlsetsadditionalProperties: false, so an author who followed the metamodel documentbroke validation. This makes the axis real.
issue_101: Implement the artifact validity axisThe schema owns the vocabulary.
validity,retired_on,retired_reasonand
retired_noteare in both schema copies, and the validator reads the enumsfrom the schema exactly as it already reads relation types from
relations.schema.yaml. The two representations cannot drift, because there isonly 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
mitigatedandmaterializedbelong to risks whileno-longer-applicableandremovedareuniversal. 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_oncannot precede
created; a risk-specific reason cannot appear on aDocument.An absent
validityis read as active in code, because a JSON Schemadefaultis 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: acceptedstaysaccepted, and the marker is(inactive)on the relation type cell, carryingthe
relation-inactiverole 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 axisThe 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 answeredADR-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 schemaFrom the review on #103. The finding there — that the axis separation was
described as complete while
supersededanddeprecatedstayed onstatus—applies to every place that states the split. The schema's
statusdescriptionand
skills/references/metadata-rules.mdmade the same clean claim and now namethe 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 doesFrom the review. The validator used
blank?where the schema decides byproperty presence, so
validity: ""read as active and empty retirement fieldsslipped 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_notegainsminLength: 1so the schema states the samerule 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
11176abRun immediately before integration, against the commit being integrated. It
replaces the provisional results; those recorded against
aff0dc4,556548band
5d87be0were void. No commit landed after11176ab, local and remoteheads are identical, CI
validateis green, no pull request depends on thisbranch, and the review re-checked
11176abwith no further findings.After #103 was rebase-merged, the branch was rebased onto
main; the fourcommits are unchanged in content, and
mainatecf8729is tree-identical tothe old
issue_102tip, so nothing from #103 was lost or duplicated.Result: Converged.
(inactive)with a role. One scope item was deferred, see below.features/metamodel-validation.featureandfeatures/documentation-generation.feature, eleven bridged tests named after them. No waiver taken. The review fix adds its own proof: restoring the oldblank?logic fails the new scenario and the parity table. Mutation check: neuteringmark_inactiveand the retirement branch fails seven of them, so the guards have a real failure path rather than reading like coverage.Findings
11176ab, with a verdict-parity table and a new scenarioajv6.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.R-004-heavy-metadata-authoring.Verification
./build.sh test./build.sh check-adapters./build.sh buildretired_onfails the run with a message naming the file and the field