Skip to content

feat: execute the semantic phase, and close the conformance debt behind issue #9 - #25

Merged
justinmerrell merged 6 commits into
mainfrom
feat/issue-9-conformance-completion
Aug 9, 2026
Merged

feat: execute the semantic phase, and close the conformance debt behind issue #9#25
justinmerrell merged 6 commits into
mainfrom
feat/issue-9-conformance-completion

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

Second pass on #9. #13 did the prose and the schema work; what it left behind is the machinery that makes the prose enforceable, plus three defects.

The state this starts from

task check was green at be77e19, and green was not telling the truth:

Declared ERR_* codes 24
Codes with a fixture that executed 9
Codes with a fixture that was skipped 5
Codes with no fixture at all 10

Every semantic clause #13 wrote — acyclicity, the floating-tag blocklist, ERR_CONFLICTING_INPUT_SCHEMA, the media rules — was prose nothing in CI ever ran. CONTRIBUTING ground rule 2 was satisfied in letter and not in substance.

After this branch: 75 cases, 0 skipped, 1 code uncoveredERR_UNKNOWN_COMPONENT, which is capability and needs the catalog.

Three defects

1. YAML anchors and aliases were not rejected. Component §7 has said they MUST be since bootstrap. parseDocument set uniqueKeys, merge: false and strict — none of which touches an alias. A document spelling &b / *b parsed cleanly and the alias expanded. No fixture covered it.

2. ERR_INVALID_YAML was defined nowhere. validator.ts emitted it; no spec.md declared it. A fixture citing it would have been rejected by the runner's own registry check as an unknown code.

3. ListingSpec.description still said media paths are "relative to the listing document" — the exact wording listing §5 singles out as wrong. #13 updated the two per-property descriptions and missed the containing object's.

What lands

ADR 0002 — case trees. Eight codes were about a document's surroundings, and a case was one case.yaml. A case may now carry a tree/ instead, with metadata.document naming the file under test inside it. GOVERNANCE.md gates this on an ADR; it is here.

Two decisions in it worth review:

  • case.yaml is not the legacy form. It asserts the document has no item root — a real state per blueprint §3.1, and the one an API payload arrives in. Making tree/ the only shape would have traded one blind spot for another.
  • Symlinks are declared, not committed. ERR_PATH_ESCAPE is reachable only by symlink now that §5's grammar makes .. unspellable. A committed link does not survive a checkout without core.symlinks, is invisible in a diff, and would ship inside a release tarball pointing outside the archive.

The semantic phase, implemented. validator.ts claimed semantic was out of scope "per ADR 0001". ADR 0001 §6 forbids publishing a reference validator and is untouched; §7 describes tools/ as one non-normative adapter. ADR 0002 §5 records the correction.

The rules split the way the prose splits them, not the way this runner would find convenient:

  • in-document — decided by reading the document, always run;
  • item-scoped — reported only when the caller supplies an item root.

That split is why check:examples stays green. examples/ is not an item root, and its documents reference ./components/postgres.yaml and media/icon.png, neither of which exists beside them. #13's body flagged that as latent breakage; this is the shape that makes it not one — and it is blueprint §3.1's rule, not a workaround for it.

Cycle reporting follows §4.2's canonical form: SCCs, each reported as a closed walk from the lexicographically smallest node. Verified stable across all three key orderings of one three-node cycle.

The reverse coverage gate. The runner enforced fixture → spec. Nothing enforced spec → fixture, which is how ten codes reached main untested. Now: every ERR_* row in a family's own diagnostics table needs an indexed case or an UNCOVERED entry carrying a reason. Plus an orphan check — a case directory cases.json does not name is not a failing fixture, it is an invisible one.

Both verified by injecting a violation and watching the check fail.

Fail-before evidence

The six anchor/alias cases were rewritten so the only thing wrong with each document is the anchor. Against tools/ at be77e19:

component/002-reject-anchors-and-aliases   VALIDATED CLEANLY — expected to fail
component/003-reject-lone-anchor           VALIDATED CLEANLY — expected to fail
blueprint/002-reject-anchors-and-aliases   VALIDATED CLEANLY — expected to fail
blueprint/003-reject-lone-anchor           VALIDATED CLEANLY — expected to fail
listing/002-reject-anchors-and-aliases     VALIDATED CLEANLY — expected to fail
listing/003-reject-lone-anchor             VALIDATED CLEANLY — expected to fail

The three malformed-YAML cases are green beforehand and are the regression pin for the code registration. The thirteen tree cases could not exist before ADR 0002. blueprint/010-well-formed-item and listing/006-well-formed-item are the positive pins: each satisfies every item-scoped rule of its family at once, so a careless containment check or a merge rule that forgets to absorb an identical redeclaration rejects a well-formed item.

Compatibility

Rejecting anchors and aliases tightens the parser phase. A document with an alias validates today and will not after. That is free while no tag exists and #1/#2/#3 are open; after they merge it is a v2 directory plus a migration note. This should land before them.

Two new parser codes, ERR_INVALID_YAML and ERR_ANCHOR_OR_ALIAS. Two rather than one because they say different things to an author: one means the document is malformed, the other means it is well-formed and uses something this contract withholds.

fix(listing): is the only family-scoped change and is a patch — a description, no validation behaviour.

Issue #9

Not closed by this PR, deliberately. Ten tracking issues are open for the remaining TODO markers — #14 #15 #16 #17 #18 #19 #20 #21 #22 #23 — none of which #9 supplied an answer for. #22 (the Markdown subset) is the one with a real deadline: constraining it is breaking after the first tag.

#24 collects the three places #13 decided against the platform — cycle detection, parameter merge, and whether a SERVICE must expose an endpoint. Each is a live divergence rather than a filled gap, and #9 should not read as closing them.

Verification

task check                 # green
task check:conformance     # 75 conformance case(s) passed
task check:examples        # 8 example(s) validate
task check:drift           # 3 bundle(s) match their sources

🤖 Generated with Claude Code

Eight of the twenty-four diagnostic codes the three spec.md files declare are
about a document's surroundings rather than its contents — a directory with a
name, a sibling document, a file on disk. A case was one `case.yaml`, so none
of them had a fixture, and they reached `main` as prose with CI green. That is
the failure mode CONTRIBUTING.md ground rule 2 exists to prevent.

GOVERNANCE.md lists "Changing the conformance fixture contract" among the
changes needing an ADR. This is that change.

A case directory MAY now carry a `tree/` instead of a `case.yaml`, with
`metadata.document` naming the file under test inside it. `tree/` is the parent
of the item root rather than the item root itself: ERR_SLUG_MISMATCH tests the
item directory's *name*, so a fixture for it needs a directory that has one.

`case.yaml` is not thereby the legacy form, and the ADR says so. It asserts that
the document has no item root — a real state, per blueprint §3.1, and the one an
API payload arrives in. Making `tree/` the only form would have traded one blind
spot for another.

Symlinks are declared in metadata rather than committed. ERR_PATH_ESCAPE is
reachable only by symlink now that listing §5's grammar makes `..` unspellable,
and a committed link does not survive a checkout without `core.symlinks`, is
invisible in a diff, and would ship inside a release tarball pointing outside
the archive.

The ADR also corrects tools/src/validator.ts, which asserted that the semantic
phase was out of scope "per ADR 0001". ADR 0001 §6 forbids *publishing* a
reference validator and is untouched; §7 describes tools/ as one non-normative
adapter. Implementing more phases inside it publishes nothing. The alternative
was a corpus in which every semantic clause was checked by nothing at all.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Component §7 has said since bootstrap that the parser phase MUST reject anchors
and aliases. Nothing did. `parseDocument` set `uniqueKeys`, `merge: false` and
`strict`, none of which touches an alias — a document spelling `&b` / `*b`
parsed cleanly and the alias expanded.

The parser now walks the tree and rejects both. `maxAliasCount` would have
covered the alias and not the lone anchor, which is inert and rejected anyway:
an author who writes one is reaching for a feature this contract withholds, and
§2 already settled that a field which does nothing is an error rather than a
silence.

Two codes rather than one. `ERR_ANCHOR_OR_ALIAS` is new. `ERR_INVALID_YAML` is
not — `validator.ts` has emitted it since bootstrap and no spec.md declared it,
so a fixture citing it would have been rejected by the runner's own registry
check as an unknown code. Both are now in component §8, and the two say
different things to an author: one means the document is malformed, the other
means it is well-formed and uses something this contract does not have.

Also switches the parser from `parse` to `parseDocument`, which reports every
problem instead of throwing on the first and carries machine-readable error
codes. The previous implementation matched on message text — the one thing this
specification explicitly declares non-normative.

The parser lives in a new document.ts so that semantic.ts can read an item's
other documents without closing an import cycle.

Fail-before evidence: all six anchor/alias cases validate cleanly against
tools/ at be77e19 and fail after. The three malformed-YAML cases are green
beforehand and are the regression pin for the code registration.

This tightens the parser phase — a document with an alias validates today and
will not after. It is free while no tag exists and release PRs #1/#2/#3 are
open; after they merge it is a v2 directory plus a migration note.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Every semantic clause PR #13 wrote — acyclicity, the floating-tag blocklist,
ERR_CONFLICTING_INPUT_SCHEMA, the media rules — was prose that nothing in CI
ever executed. Five fixtures existed and all five were skipped.

New semantic.ts, split the way the prose splits the rules rather than the way
this runner would find convenient.

An **in-document** rule is decided by reading the document: a floating image
tag, a probe naming an endpoint that is not declared, a `fromRole` naming no
node, a cycle, two screenshots sharing a basename.

An **item-scoped** rule is measured against the item root, and is reported only
when the caller supplies one. That is blueprint §3.1's rule, not a concession:
"an implementation handed a document rather than a directory MUST NOT report any
of them … a diagnostic it cannot substantiate is worse than a silence." It is
also why `check:examples` stays green — `examples/` is not an item root, and its
documents reference `./components/postgres.yaml` and `media/icon.png`, neither
of which exists beside them. PR #13's body flagged that as latent breakage;
this is the shape that makes it not one.

Cycle reporting follows §4.2's canonical form exactly: strongly connected
components, each reported as a closed walk from the lexicographically smallest
node, choosing the smallest successor at each step. Verified stable across the
three key orderings of one three-node cycle — the walk is a property of the
graph, not of the traversal, which is the whole point of pinning it.

conformance.ts gains the ADR 0002 case-tree form: `tree/` materialised to a
scratch directory with its declared symlinks created there, validated with the
item root set to the document's own directory. A case must declare exactly one
of `case.yaml` or `tree/`; declaring both leaves it ambiguous which one the
diagnostics describe.

`IMPLEMENTED_PHASES` gains `semantic`. `capability` stays out — it needs an
account, a region and a quota, which is a server.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
`$defs.ListingSpec.description` still told readers that `icon` and
`screenshots[].file` are "paths relative to the listing document" — the exact
wording §5 now singles out as wrong: "`media/` is a stronger rule than 'relative
to the listing document'. That was this section's earlier wording, and it is not
what anything enforces."

PR #13 updated the two per-property descriptions and missed the containing
object's, so the schema contradicted itself and the prose in one file.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Thirteen new cases covering every diagnostic code that needed a directory to
exist. All are trees; none was expressible before.

blueprint 003–009 take the identity, reference and merge rules one at a time —
slug against the directory name, version against the sibling listing, a
component document nothing references, a reference resolving to nothing, a
reference escaping the item root, a `fromOutput` naming no output of the
component it resolves to, and two nodes declaring one input key with differing
schemas.

Three deserve their reasons stated:

  007  the reference is `../shared/postgres.yaml` and the document it lands on
       exists. A leading `../` is legal spelling — it is how a blueprint reaches
       a sibling directory — so only a containment check on the resolved path
       catches this one.

  009  `api` and `db` both declare `adminPassword`, STRING against INTEGER.
       `api` sorts first, so `api`'s declaration stands and `db`'s is reported.
       The case exists because silent first-wins is defensible until you write
       down which schema the second component's value was validated against.

  011  a three-node cycle written as queue, db, cache. semantic-002 cannot make
       this case: with two nodes, "smallest first" and "first declared" can
       coincide by luck. Here they do not, so an implementation reporting from
       wherever its traversal started produces the wrong walk and fails.

listing 002–005 mirror the identity rules and add the two media rules that need
the filesystem. 005 is the symlink case, declared in metadata rather than
committed per ADR 0002 §3.

blueprint 010 and listing 006 are the positive cases, and they are not padding.
Each satisfies every item-scoped rule of its family at once, which makes them
the regression pin for the class: a careless containment check or a merge rule
that forgets to absorb an identical redeclaration rejects a well-formed item,
and nothing else in the corpus would notice.

`ERR_UNKNOWN_COMPONENT` remains uncovered. It is `capability` — resolving a
published reference needs the catalog, and no phase a client runs may reach the
network.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
The runner enforced fixture → spec: every code a case declares must exist in a
reachable diagnostics table, at the right phase. Nothing enforced spec →
fixture, which is how ten codes reached `main` untested with CI green.

Two checks close it. Every `ERR_*` row in a family's own diagnostics table must
be exercised by an indexed case or appear in `UNCOVERED` with a reason — after
this branch that list holds one entry. And every case directory on disk must be
named by `cases.json`, since the runner reads the index rather than walking the
tree: an unindexed directory is not a failing fixture, it is an invisible one.

Both were verified by injecting a violation and watching the check fail.

The README's "Coverage status" described a limitation ADR 0002 removes, so it is
rewritten around what is now true: three phases covered, one code uncovered, and
a check that derives the list instead of a paragraph someone has to remember to
update. It also documents the `tree/` form, `document`, and `symlinks`.

CONTRIBUTING gains the obligation this creates: adding a diagnostic code to a
spec.md means adding a case for it.

Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit 5be1e0a into main Aug 9, 2026
4 checks passed
@justinmerrell
justinmerrell deleted the feat/issue-9-conformance-completion branch August 9, 2026 05:40
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.

1 participant