feat: fill the spec.md TODOs from implemented behaviour, and settle cycle detection - #13
Merged
Conversation
The runner skipped unimplemented phases before it read `diagnostics.json`, so a `semantic` fixture's declared codes and clause were checked by nothing at all. Issue #9 turns nearly every remaining `spec.md` TODO into a `semantic` rule, which would have landed a corpus this repository cannot execute and did not inspect either. Split the two concerns. Executing a case needs a phase implemented here; checking that a case is well-formed does not, and now runs for every case: - `metadata.phase` is one of the four, and `cases.json` agrees with it. The index entry's `phase` was previously cast and discarded. - `id` leads with its phase, per the README's `<phase>-<NNN>-<description>`. - `clause` resolves to an `<a id="…">` that exists in the cited `spec.md`. Anchors are load-bearing precisely because fixtures cite them, and nothing noticed when one went stale. - every declared `code` appears in a diagnostics table reachable from the family's own — its own additions unioned with component §8, mirroring the "The codes in component §8 apply. This family adds:" inheritance the prose already declares — and at the phase that table assigns it. The last one is the reason for the rest. A typo'd or wrong-phase code used to pass CI silently, and for a skipped phase it would have stayed silent indefinitely. No fixture changes: all 25 existing cases satisfy the new checks unmodified. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
…act clauses Five of the ten TODO markers in this file, answered from behaviour already implemented and gated in the platform. Issue #9 supplies each answer with its enforcing call site; three of its rows change meaning on the way across, because the issue is written in the platform's vocabulary and this contract does not share it. §5 Workload — a kind × field table. `endpoints` are for a SERVICE, `command` is required on a JOB and a CRON, `schedule` is required on a CRON and rejected everywhere else. The §5 TODO asked whether a misplaced field here is an error or ignored, and §2 had already answered it for every other field: a misspelled field is an error, never a silently ignored one. A `schedule` on a SERVICE fails the same way and for the same reason. Two rows differ from the platform's table deliberately: - `command` stays permitted on a SERVICE and a WORKER, where it overrides the image's default command. That is meaningful, unlike a schedule, so the symmetry with `schedule` would be false symmetry. - a SERVICE is NOT required to expose an endpoint. The platform requires one; adopting that would reject `examples/minimal.yaml` and the `structural-001-minimal-valid` fixture, so it is recorded in the prose as a candidate for a later major rather than taken now. The platform's "input keys unique within a component" row is a no-op here: `contract.inputs` is a mapping, so a repeated key is already ERR_DUPLICATE_KEY in the parser phase. §5.1 Source — image references must be pinned, split across two layers. The grammar (a tag or an @sha256 digest is present) is structural. The floating-tag blocklist is semantic, and stays out of the schema on purpose: the list is curated and will grow, and growing a `pattern` makes a previously valid document invalid. Held as prose plus a fixture, it can be extended in a minor release. §5.4 Health probes — `readiness` is required for a SERVICE exposing at least one PUBLIC endpoint, and a private-only service is exempt. Probe defaults are written down. A probe naming an endpoint that does not exist is a new semantic code, ERR_UNKNOWN_ENDPOINT. §5.5 Volumes — `mountPath` must be absolute. Volume overlap and size bounds are stated as gaps rather than described aspirationally: neither project constrains them, and a reader needs to be able to tell which silences are decisions. §6 Configuration contract — the `ui`/`suppliedBy` correspondence and the `valueFrom`/`value` correspondence were both already asserted in the schema's own descriptions and enforced by nothing. A generated input must be USER- supplied and sensitive, both written explicitly, because `isSensitive` defaults to false and that is the wrong answer for a minted secret. Issue #9 frames the last §6 rule as a `params.*`/`self.*` template namespace. There is no template language in this contract — an output is `valueFrom: DERIVED|DECLARED` plus a static string — so it is restated in the contract's own terms: an output's value is a function of the producing node alone and must not depend on a value that node received over an inbound connection. That is what makes an output referenceable. It is explicitly not a licence for a cyclic graph; blueprint §4.2 requires acyclicity on its own grounds. Prose only. Nothing in this commit changes what validates. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Seventeen structural cases and two semantic ones covering every rule the
preceding commit wrote into spec.md.
Deliberately red at this commit — that redness is the fail-before evidence.
Measured against the previous bundle, twelve of the seventeen structural
cases fail:
006 expected to fail but validated cleanly endpoints on a WORKER
008 expected to fail but validated cleanly JOB without command
010 expected to fail but validated cleanly CRON without schedule
011 expected to fail but validated cleanly schedule on a SERVICE
012 expected to fail but validated cleanly bare image name
014 expected to fail but validated cleanly GIT source without build
015 expected to fail but validated cleanly public SERVICE, no readiness
017 expected to fail but validated cleanly relative mountPath
018 expected to fail but validated cleanly generated input not sensitive
019 expected to fail but validated cleanly ui on a CONNECTION input
020 expected to fail but validated cleanly USER input without ui
021 expected to fail but validated cleanly DERIVED output with a value
The other five are green beforehand and are regression pins rather than proof
of a fix. Each exists because it is the case a careless rule would break:
007 an empty `endpoints: {}` on a WORKER — forbidden bounds the contents,
not whether the key may be written
009 a well-formed CRON, so the two CRON rules cannot be satisfied by
rejecting every CRON
013 `localhost:5000/musher-dev/api@sha256:…` — the registry port is the
case the pinning grammar has to get right, since a naive rule reads
the port colon as a tag
016 a PRIVATE-only SERVICE with no readiness probe, pinning the exemption
022 the whole contract surface at once: a generated sensitive input, a
DERIVED output, and a DECLARED one
The two semantic cases — a floating `:nightly` tag and a probe naming an
endpoint that does not exist — are skipped by this repository's adapter, which
implements the parser and structural phases only. They are not unchecked: the
preceding tools commit validates their metadata, clause anchor, and declared
codes whether or not the phase runs here.
Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Turns the preceding two commits green. Every rule here is one JSON Schema 2020-12 can carry, so it lands in the structural phase where CI actually runs it, rather than as a semantic rule this repository would skip. `ComponentWorkload` gains four conditionals under `allOf`: - endpoints are capped at `maxProperties: 0` on a WORKER, JOB and CRON. The cap rather than `endpoints: false` so an explicit empty mapping stays legal — the same spelling the blueprint examples use for an empty connections block. - `command` is required and non-empty on a JOB and a CRON. - `schedule` is required on a CRON and `type: null` everywhere else, so an absent or explicitly-null schedule is fine and a real one is not. - readiness is required for a SERVICE with at least one PUBLIC endpoint. Endpoints are a mapping and no keyword asserts "some member matches" over one, so the condition is the negation of "every member fails to match". A `$comment` says so; it is not a construction to re-derive at a glance. Each branch carries no `type`, so `lint.ts` `checkClosedObjects` skips it correctly — the object is closed once, by the schema declaring its properties. `ComponentImageSource.ref` gains the pinning grammar. Lookahead-free so it compiles under RE2 as well as ECMA-262, matching the precedent set by `BlueprintNode.component`. The load-bearing part is that the tag colon is the one after the final slash: without that, `localhost:5000/nginx` reads as an image named `localhost` tagged `5000/nginx` and an unpinned reference behind a ported registry validates. `ComponentGitSource` requires `build`, and it is no longer nullable. The "or null when defaults apply" the description promised was a default the document could not see and the platform does not implement. `ComponentInput` gains the `ui`/`suppliedBy` correspondence and the generated-input rule. `suppliedBy` defaults to USER, so the USER branch is the `else` — a default is invisible to a validator, and an input omitting `suppliedBy` must still be caught. `isSensitive` is required explicitly for the opposite reason: its default is false, which is the wrong answer for a minted secret, so absence cannot be allowed to mean it. `ComponentOutput` gains the `valueFrom`/`value` correspondence, and `ComponentVolume.mountPath` an anchored `^/`. Both only assert what the fields' own descriptions already claimed. All eight examples still validate; `examples/minimal.yaml` survives because the SERVICE-must-expose-an-endpoint rule was not adopted. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Issue #9 raised the one TODO in this repository that is a contradiction rather than a gap: §4.2 mandates an acyclic connection graph, and the platform deliberately permits cycles under ADR 0116, to support mutual service discovery. **The specification holds its position. The graph MUST be acyclic and ERR_DEPENDENCY_CYCLE stays.** ADR 0001 §1 is the grounds — "Where an implementation and the conformance suite disagree, the implementation is defective" — and the consequence is not hidden: the platform gains a cycle check that rejects a composition it accepts today. The platform's argument is recorded in the clause rather than dismissed, because it is correct as far as it goes. An output is a function of its own node alone, so a resolver that computes every output before binding any edge needs no topological order and does not fail on a cycle. §4.2 says so outright and then says why the rule is required anyway: permitting cycles obliges every implementation in every language to be that two-pass resolver in perpetuity, forecloses any later rule that needs an order — an ordered rollout, a health-gated start, a value that legitimately depends on an inbound edge — and leaves every reader of a six-node graph working out for themselves whether it terminates. One traversal is cheaper than the option it keeps open. Cycle reporting is pinned so it is comparable across implementations: the participating nodes as a closed walk from the lexicographically smallest node in the cycle, anchored at that node's `connections`. The rest of the file: §3 Identity gains its three rules — slug equals the item directory name, version agrees with the sibling listing, and every component document in the item is referenced by some node. The last is new, from issue #9, and is what stops an item silently accumulating last release's component file beside the one in use. Its diagnostic anchors at `/spec/components`, because a JSON Pointer addresses this document and the file it complains about is not in it. §3.1 defines the item directory. Nothing defined it, and §4.1 was already using "item root" as though something did — every identity rule, the reference containment rule, and listing §5's media paths are all measured against it. The section also draws the line §4.1 draws: a document handed over without a directory has no item root, and an implementation in that position MUST NOT report any of these rules rather than guess at one. §4 gains the node-name grammar, the slug grammar reused. Uniqueness needs no rule — `spec.components` is a mapping, so a repeat is ERR_DUPLICATE_KEY in the parser phase. The `size` TODO stays, narrowed: the Compute Profile vocabulary is not published in this repository and a reader outside the platform cannot resolve `general.standard.small`, which is part of what closing it means. §4.2 gains the `fromRole` and `fromOutput` resolution rules alongside the cycle clause, and a narrowed TODO for output/input type compatibility, which issue #9 does not answer. §5 Parameters gains derivation and the merge rule. Two corrections to what was there: - "An empty `parameters` mapping is not the same as an absent one" was not true and could not be made true — nothing distinguishes the two states. Both now mean derive. - issue #9's merge rule sorts by `(ordering, componentId)`. Neither field exists in this contract, so canonical order is redefined as lexicographic by node name, the only total order the document itself supplies. The platform's first-wins discards a conflicting second declaration silently. That is not blessed. An identical redeclaration is absorbed; a differing one is ERR_CONFLICTING_INPUT_SCHEMA. Silent first-wins hands the second component a value validated against the first component's rules, and the failure surfaces at deploy time inside the consuming workload with nothing pointing back at the two documents that disagreed. Prose only. Nothing in this commit changes what validates. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
…ules
012 expected to fail but validated cleanly — `Web_Server` is a legal map
key today, so the node-name grammar is the fail-before evidence.
The two semantic cases are the only rules from the preceding commit that a
single document can express. Everything else §3 added — slug against the
directory name, version against the sibling listing, the unreferenced
component document — needs a directory on disk, and the fixture contract is
one `case.yaml`. Those are deferred rather than approximated; see the
follow-up on extending the contract.
semantic-001 a connection whose `fromRole` names no node. The whole graph
is in this one document, so nothing external is needed.
semantic-002 a two-node cycle, and the case the reporting rule exists for:
`api` and `web` discover each other, and the walk must be
reported from `api` because it is lexicographically smaller —
otherwise which node a traversal happened to start from would
leak into a normative diagnostic.
Both are skipped by this repository's adapter, which implements the parser and
structural phases only. Their metadata, clause anchors and declared codes are
checked regardless.
Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
`spec.components` accepted any string as a node name. `propertyNames` now
holds it to `^[a-z][a-z0-9-]{0,61}[a-z0-9]$`, the grammar `metadata.slug`
already uses — one grammar for the names this family coins rather than two.
Turns structural-012 green. The diagnostic anchors at `/spec/components`
rather than at the offending key, which is what `propertyNames` gives; the key
itself is not addressable by a JSON Pointer when it is the thing being
rejected.
The other §3 and §4.2 rules from the prose commit are not expressible here.
Slug-versus-directory, version-versus-sibling, reference resolution and cycle
detection all need something outside the document, which is what makes them
semantic.
Refs: #9
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
§3 mirrors blueprint §3 and measures against the item root that blueprint §3.1 now defines. One nuance the issue does not cover: the version rule needs a sibling blueprint to compare against, and a `listingKind: COMPONENT` item need not contain one. The rule is conditioned on there being a blueprint, and a narrow TODO records what an item without one agrees with — not the same rule with a different sibling, since an item may hold several component documents each carrying its own version. §5 splits the media rules by what they need. Structural, because it is a grammar: relative, first segment exactly `media`, every later segment beginning with a letter or a digit, extension in `.png`, `.jpg`, `.jpeg`, `.webp`. The `media/` requirement is stronger than the "relative to the listing document" this section used to claim, and it is the rule that actually gets enforced — one fixed directory is what lets a publisher copy an item's assets without walking its listing to discover them. Semantic, because each needs the filesystem: existence, containment of the resolved target, and basename uniqueness across the whole item. `ERR_PATH_ESCAPE` survives the grammar rather than being made redundant by it. `..` is now unspellable, so traversal is gone — but a symlink under `media/` is a legal spelling resolving to an illegal target, and containment is a property of the resolved location rather than of the string. Blueprint §4.1 draws the same line for `ERR_REFERENCE_ESCAPE`. The basename rule is adopted from the platform with its provenance stated: published assets are addressed by basename, so `media/desktop/overview.png` and `media/mobile/overview.png` are one file. It is a real constraint on an author and is better written down than discovered when the second screenshot replaces the first. §4 keeps both its TODOs, as issue #9 asks, but says what is already settled: `summary` is plain text and must not be rendered as Markdown, `description` is Markdown bounded at 20 000 characters. The Markdown-subset TODO is reframed as what it is — an open security question about rendering untrusted input, naming the three cases that need deciding — rather than a behaviour someone forgot to document. Dimension and file-size bounds are recorded as gaps, as in component §5.5. Prose only. Nothing in this commit changes what validates. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
007 expected to fail but validated cleanly ../../etc/passwd.png 008 expected to fail but validated cleanly assets/overview.png 009 expected to fail but validated cleanly media/overview.gif 010 is green beforehand and is the regression pin: a nested path under `media/` and an extension in upper case. A JSON Schema pattern carries no case-insensitive flag, so a careless alternation accepts `.png` and rejects `.PNG`, and nothing else in the corpus would notice. semantic-001 is the basename rule, which needs no directory on disk — two paths in one document are enough to collide. The existence and containment rules do need one and are deferred with the rest of the tree-shaped cases. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
`icon` and `screenshots[].file` were unconstrained strings, so `../../etc/passwd.png` was a structurally valid icon and the whole media contract rested on a semantic phase nothing in this repository runs. Both now carry the §5 grammar. Lookahead-free so it compiles under RE2 as well as ECMA-262, and a segment must begin with a letter or a digit — the technique `BlueprintNode.component` already uses to exclude `.` and `..` without a negative lookahead. The extension alternation is spelled in character classes because a JSON Schema pattern has nowhere to put a case-insensitive flag. `icon` also gains the `maxLength: 512` its sibling `file` already had. It was the only unbounded string path in the family. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
The coverage note said semantic cases land "once the corresponding rules are written into spec.md". They are written now, and five semantic cases landed with them — but eight rules did not, and not because the prose is missing. Every one of them is about a document's surroundings rather than its contents: a directory with a name, a sibling document, a file on disk. A case is one `case.yaml`, so none of them is expressible, and the shortfall would otherwise read as prose still owed. Extending the contract to a case tree is ADR-gated — GOVERNANCE.md lists "Changing the conformance fixture contract" — so the note says what is blocked and on what, rather than leaving the corpus looking arbitrarily incomplete. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Two corrections to the clauses in this branch, found by checking the prose against what the schema actually accepts rather than against what it was meant to. component §5 said a forbidden field "MAY be absent, an empty mapping, or an explicit null". Only one of the three is true per field, and the schema is right: `endpoints` is a mapping and rejects null, `schedule` is a nullable block and rejects an empty mapping. Each field has one empty form and they are not interchangeable. The clause now says which is which. The §5 note on a SERVICE without endpoints claimed the rule "would reject documents this specification currently accepts, which makes it a breaking change rather than one v1 can absorb". The second half overstates: nothing is released yet, so v1 could absorb it. It was not adopted, which is a different claim, and the clause now makes that one. blueprint §5.2 leaves the merge order comparable across implementations: §4 confines a node name to lowercase ASCII, so byte order and lexicographic order coincide and no collation can change which node wins a key. Without that the merge rule is reproducible only among implementations that happen to agree on string comparison. Refs: #9 Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
This was referenced Aug 9, 2026
This was referenced Aug 9, 2026
Closed
justinmerrell
added a commit
that referenced
this pull request
Aug 9, 2026
…nd issue #9 (#25) * docs(conformance): ADR 0002, extend the fixture contract to a case tree 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> * fix(tools): reject YAML anchors and aliases in the parser phase 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> * feat(tools): implement the semantic phase and the case-tree runner 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> * fix(listing): correct the stale media-path description `$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> * test(conformance): the item-scoped semantic cases, against ADR 0002 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> * docs(conformance): record coverage as a check rather than a claim 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> --------- Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
This was referenced Aug 9, 2026
This was referenced Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes half the
TODOmarkers across the threespec.mdfiles — the half #9 supplies answers for — and settles the contradiction it raised first.The cycle decision
§4.2 mandated an acyclic connection graph; the platform deliberately permits cycles under ADR 0116, for mutual service discovery. The specification holds its position. ADR 0001 §1 is the grounds: "Where an implementation and the conformance suite disagree, the implementation is defective."
The consequence is not hidden — the platform gains a cycle check that rejects a composition it accepts today. The platform's argument is recorded in the clause rather than dismissed, because it is correct as far as it goes: an output is a function of its own node alone, so a two-pass resolver needs no topological order and does not fail on a cycle. §4.2 says so, then says why the rule stands anyway — permitting cycles obliges every implementation in every language to be that resolver in perpetuity, forecloses any later rule needing an order, and leaves every reader of a six-node graph working out whether it terminates.
Cycle reporting is pinned so it is comparable across implementations: the participating nodes as a closed walk from the lexicographically smallest node.
Why so much of this is structural rather than semantic
Nearly every rule in #9 is labelled
semantic, butconformance.tsimplementsparserandstructuralonly — a semantic fixture is skipped before itsdiagnostics.jsonis even read. Rules that JSON Schema can carry are therefore expressed in the schema, where CI actually runs them, following theBlueprintNodeprecedent from #12.Timing: no tag exists yet and the
1.0.0release PRs (#1, #2, #3) are still open, so these tightenings are free. After those merge each one is av2directory plus a migration note. This should land first.One rule stays deliberately out of the schema: the floating-tag blocklist. It is a curated list that will grow, and growing a
patternis a major version — held as asemanticrule it can be extended in a minor.Translating #9
The issue is written in platform vocabulary. Three of its rows change meaning on the way across:
buildforbidden forIMAGEComponentImageSourceis closed and has nobuildcontract.inputsis a mapping, so a repeat isERR_DUPLICATE_KEYin the parser phaseparams.*/self.*(ordering, componentId)orderPlus the ordinary renames:
imageRef→source.ref,gitRepo→source.repositoryUrl,runCommand→command,cronExpression→schedule.cron.Two rules deliberately not adopted, both worth a second opinion:
SERVICErequires ≥1 endpoint. Would invalidateexamples/minimal.yamlandstructural-001-minimal-valid. The platform is stricter than the spec here.commandforbidden onSERVICE/WORKER. The schema's description said "or null otherwise", but overriding an image's default command is meaningful, unlike a strayschedule. Kept permissive.One rule the spec refuses to bless as implemented: the parameter merge. First-wins silently discarding a conflicting second declaration hands the second component a value validated against the first component's rules, failing at deploy time inside the consuming workload. An identical redeclaration is absorbed; a differing one is
ERR_CONFLICTING_INPUT_SCHEMA.What landed
Prose — 10 of 21 TODOs closed. component §5, §5.1, §5.4, §5.5, §6; blueprint §3, §3.1, §4, §4.2, §5; listing §3, §5. New: blueprint §3.1 defines the item directory, which nothing defined even though §4.1 already said "item root" and every identity and media rule depends on it.
Schema — kind × field validity, image-ref pinning grammar,
buildrequired on a GIT source, the readiness rule,mountPathabsoluteness, theui/suppliedByandvalueFrom/valuecorrespondences, the generated-input rule, node-name grammar, media path grammar.Fixtures — 22 new cases. 16 negatives were red before their schema commit and green after; 6 positives are regression pins. 5 semantic cases cover every rule a single document can express.
Tooling —
conformance.tsvalidated nothing for a phase it does not implement, so a semantic fixture's declared codes and clause were checked by nothing. Metadata validation now runs for every case: the clause resolves to a real anchor, and every declared code appears in a diagnostics table reachable from the family's own, at the phase that table assigns it.Two corrections found on review
parametersmapping differs from an absent one. Nothing distinguishes them; both now mean derive.Deferred
Eight semantic rules need a directory on disk — slug vs directory name, version vs sibling, unreferenced components, reference resolution, media existence. A case is one
case.yaml, so none is expressible. Extending the contract is ADR-gated (GOVERNANCE.md → "Changing the conformance fixture contract");conformance/README.mdnow records exactly what is blocked and on what. #9's author offered to contribute those fixtures — worth taking up against that ADR.Also worth tracking: the examples reference files that do not exist here (
./components/postgres.yaml,media/icon.png). Harmless while only the structural phase runs; every one fails the moment semantic checking lands.Verification
task checkis green: 3 modules linted, 3 bundles match a fresh compile, 8 examples validate, 47 conformance cases pass (5 skipped — the semantic ones, asconformance/README.mdmandates).The three
test(conformance)commits are deliberately red on their own; that redness is the fail-before evidence CLAUDE.md non-negotiable #3 requires, and each commit message records which cases failed and why the green ones are pins rather than proof.Refs: #9
🤖 Generated with Claude Code