Skip to content

0.23.0 — a requirement records the DECISION about a gap

Choose a tag to compare

@dmealing dmealing released this 15 Aug 11:12
· 1059 commits to main since this release

A coordinated MINOR across all four registries, cut as MINOR because it adds registered
metamodel vocabulary: pre-1.0 ^0.22.x resolves <0.23.0, so a consumer adopts it
deliberately. Everything in it is additive — a ledger written against 0.22.x loads unchanged
and produces the same diagnostics — and the whole line is scoped to requirement.*, so a
project declaring no requirements sees no behaviour change at all.

Full lockstep across all 14 @metaobjectsdev/* publish candidates.

Added — a requirement records the DECISION about a gap, not just the gap (all five ports)

New registered vocabulary on both requirement.* subtypes, which is why this line is a
MINOR.
Purely additive: a ledger written against 0.22.x loads unchanged and produces the
same diagnostics.

@status: partial said there IS a gap. It never said what anyone DECIDED about it, so a
known-and-tolerated gap and a gap nobody has looked at were the same value. Dogfooding a real
235-entry adopter ledger is what surfaced it: 62 recorded gaps, and no way to ask which of
them had been ruled on
without writing a script. Three additions close that:

  • @status: planned — intended, not built. Its @implementedBy may dangle (write the
    requirement before the entity exists) and it is exempt from the architectural universality
    check, which would otherwise fire on precisely the entries meant to apply to nothing yet. A
    planned entry never counts toward object coverage — without that rule the cheapest way
    to clear an unclaimed-entity warning would be to declare an intention, and the gate would
    measure ambition rather than work.
  • @disposition — closed enum accepted (understood, deliberately not closing) or
    deferred (will close, not now). Absent means UNDECIDED, and keeping that distinct is
    the point: folding it into @status would make "there is a gap" and "we chose to live with
    it" the same fact, and lose the question a review exists to ask.
  • @trackedBy — issue/ticket references, a string array. Free-form and deliberately
    never resolved (verify has no network), and deliberately not a workflow vocabulary:
    which sprint, who owns it and whether it is in progress live in the tracker, because two
    systems holding that answer will drift and only one of them is refreshed daily.

Two new warnings: a @disposition on a status with no outstanding work (there, the
decision IS the status), and deferred naming no ticket — which is how a known problem
becomes an unknown one.

meta verify now prints a requirements summary on EVERY run, clean or not — entry counts
by status, entities claimed, and the number of gaps carrying no disposition. A gate that says
nothing when it passes cannot be told apart from a gate that checked nothing, and a ledger
that skipped a whole grain read exactly like a complete one.

Architectural requirements can now nest. requirement.functional declared a requirement.*
child rule and requirement.architectural declared none — so an architectural node could nest
under a functional parent but never under another architectural one, which made a quality
taxonomy inexpressible. That asymmetry was an omission, not a design: flat-by-design would have
rejected nesting under functional too. @level is now OPTIONAL on architectural
absent keeps the original flat, object-independent form (so every existing ledger stays valid),
present opts the node into a tree, and from that point the same nesting and link-floor rules
apply as to a functional node, so a grouping tier cannot quietly start naming entities. The
first cut of that fired the universality check on levelled ORGANISATIONAL nodes, which name
nothing by design; mayReferenceModel() is the right predicate, since it already encodes "is
this tier allowed to name the model at all".

Also corrects the levelling guidance in the spec, the feature doc and the metaobjects-authoring
skill: L1–L3 are levels of abstraction and ownership in the problem domain, never a directory,
package, deployable or module
— with the test that if a behaviour-preserving refactor would
force a node to move, its level is wrong. That text is byte-gated in expected-registry.json,
which is why a wording correction is a cross-port change rather than a doc edit.

Fixed — the 0.23.0 requirement vocabulary reaches C#, Java, Kotlin and Python

main was RED on four of five ports, and had been since the commit that introduced
@disposition / @trackedBy / status: planned. That change landed the vocabulary in
TypeScript and in the byte-gated fixtures/registry-conformance/expected-registry.json
without porting the registration, so every other port's RegistryManifestConformanceTest
failed on the same diff (disposition vs implementedBy) and the
requirement-disposition-and-planned conformance fixture failed with ERR_UNKNOWN_ATTR /
ERR_BAD_ATTR_VALUE. Kotlin's codegen-kotlin failure was the same root cause reaching it
through the shared JVM registry.

Each port now registers, on both subtypes:

  • @disposition — optional, closed enum accepted | deferred
  • @trackedBy — optional string array, free-form and never resolved
  • status: plannedfirst in the value set (declaration order is contractual; the
    manifest emits allowedValues unsorted)
  • @verifiedBy on architectural, which previously had it on functional only
  • @levelrequired on functional, optional on architectural, where absent
    means a flat object-independent policy and present opts the node into a levelled tree
  • the requirement.* child rule on architectural, which had it on functional only —
    an omission that made an architectural node nestable under a functional parent but never
    under another architectural one, so a quality taxonomy could not be expressed at all

The Java node class also gains the getDisposition() / getTrackedBy() / isPlanned() /
hasOutstandingWork() accessors and the STATUSES_WITH_OUTSTANDING_WORK and DISPOSITIONS
constants its TypeScript counterpart already had.

The gate did its job; nobody ran it. These lanes do not run on PRs (hosted CI runs the
non-TS ports on release tags and manual dispatch only), and the push-to-main local-ci run
is affected-ports-only. Worth noting for anyone reading a green shell: scripts/ci-local.sh
piped through tail reports the exit status of tail, so three lanes returned 0 while their
own summaries said LOCAL CI FAILED.

Verified per port after the fix: C# 872 conformance + 681 render/codegen/cli, Java + Kotlin
conformance lane green, Python 390 conformance + 1571 unit.

Changed — description and notes are split by CONTENT KIND, not by audience (all five ports)

Documentation-only: four registered attribute descriptions. No behaviour changes, no new
vocabulary, no generated-output change.
The commonAttrs block of the byte-gated
fixtures/registry-conformance/expected-registry.json moves, so all five ports carry it.

The two slots were described by who reads themdescription was "free-form user-facing
prose", notes was "internal-only rationale". Neither said the content had to differ, so
the honest way to fill both is to write the same thing twice at two levels of politeness, and
notes becomes a longer description with citations bolted on. Dogfooding put a number on
it: filling both across a 245-entry ledger produced overlap on 72 of 245 entries, and the
same overlap every time — the description opened with the disposition and then narrated the
gap that notes already held.

They are now split by content kind, with a mechanical test in the registered text itself:

  • description — what the element is and covers, for someone using it: scope and
    boundary, what it deliberately does not cover, and which sibling owns the rest. Derivable
    from the model.
  • notes — what you had to look outside the model to learn: evidence, measurements,
    citations, the control that proved an absence was real, and what breaks if this changes.
    A sentence belongs in notes exactly when it would have to change because the
    IMPLEMENTATION changed while the model did not.

title and summary had the same defect on a smaller scale — both read "short single-line",
and nothing distinguished them. title is now explicitly a noun phrase, summary a
one-line sentence, each pointing at the other.

docs/features/requirements.md and the metaobjects-authoring skill gain the
requirement-specific application, which is where the collision is sharpest: @statement
already occupies the "what is this" role a common description usually holds, so on a
requirement description narrows to scope or it is padding. Both name the two failure modes
that look like diligence — a description that paraphrases the statement, and one that
narrates the evidence.

Known duplication, unchanged here: each of these strings lives in seven places (the
root spec, three per-port spec copies, the TS embedded definition, a completeness pin, and
the generated manifest). Changing one means changing all seven and regenerating; the pin now
says so.

Fixed — a levelled architectural requirement now obeys the level rules it documents (npm)

requirement.architectural gained an optional @level in 0.22.0, opting a node into a
tree so a quality taxonomy can organise the non-functional set. @level's own registered
description promises that "PRESENT means this node sits in a levelled tree, and then the same
rules as functional apply: nesting must agree with the level". Only the link floor was
actually enforced: checkRequirements gated the level-range and nesting checks behind
if (!architectural), so a levelled architectural node could declare a level 7, or
re-ascend the tree by nesting an L1 under an L2, and meta verify said nothing.

Both checks now run whenever a level is present, on either subtype. An unlevelled
architectural requirement stays exempt — levelling is the opt-in, and enforcing the tree
rules unconditionally would break every existing flat policy, which is the whole reason the
attribute is optional.

One rule is deliberately not extended: the L4-is-an-object / L5-is-a-member grain
checks stay functional-only. On a functional requirement those levels mean those grains;
on a levelled architectural one the upper tiers are a quality taxonomy and L4/L5 retain only
their link-floor meaning, so a policy whose claim set legitimately mixes grains ("every money
field declares its currency", claimed alongside the entities holding them) is not forced to
split by grain to say so. That would be a new rule rather than the missing half of one
@level already promised.

Fixed — the requirements summary no longer contradicts the gate printed beneath it (npm)

meta verify prints a summary line (… — N/M entities claimed) above its diagnostics.
summariseRequirements computed that ratio with its own walk, and 0.22.0's coverage fix
was applied only to checkRequirements. The two therefore disagreed in two ways, both making
the summary under-report while the gate stayed silent:

  • an abstract entity was counted in the denominator, though the gate exempts it (shape,
    not data — no table, no rows);
  • an architectural claim on an abstract base did not propagate down the extends
    chain in the summary, though the gate propagates it — so a project using the documented
    BaseEntity pattern read as uncovered.

A project declaring one architectural rule on its BaseEntity could see 1/12 entities claimed above a diagnostic list naming zero unclaimed entities. Both sides of the ratio
now come from the same two helpers the gate uses, so the arithmetic and the diagnostics
cannot drift again. summariseRequirements had no test coverage at all, which is how the
divergence survived; it has some now.

Fixed — spec/capability-ledger.md still said architectural requirements carry no level

The spec asserted "no level and no nesting@level is not registered on the subtype at
all, so declaring one is ERR_UNKNOWN_ATTR" — true before 0.22.0, and directly contradicted
by the shipped registry since. Corrected, including the attribute table, with the deliberate
grain-check asymmetry stated. docs/features/requirements.md was already accurate.

All three were found by dogfooding the feature: levelling a real 245-entry ledger's nine flat
architectural requirements under an ISO/IEC 25010 tree, which is the shape the @level
description recommends and the shape nothing had run against a model larger than a fixture.