Skip to content

fix(blueprint): stop §10 rejecting a cycle §4.2 permits - #48

Merged
justinmerrell merged 2 commits into
mainfrom
fix/blueprint-cycle-rejection-clause
Aug 19, 2026
Merged

fix(blueprint): stop §10 rejecting a cycle §4.2 permits#48
justinmerrell merged 2 commits into
mainfrom
fix/blueprint-cycle-rejection-clause

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

Blueprint §10's "Graph traversal" paragraph said a cycle is rejected with
ERR_CONNECTION_CYCLE, contradicting §4.2 of the same document — which says the
graph MAY contain a cycle and an implementation MUST NOT reject a composition
for that reason alone — and naming a code no diagnostics table has ever
declared. The traversal requirement around it is kept and now cites §4.2 rather
than contradicting it. Two adjacent artifacts of the same withdrawn rule go with
it: conformance/README.md's phases table, and the absence of any gate that
would have caught a phantom code.

Why

Closes #46.

The corpus already sides with §4.2 — semantic/002-connection-cycle and
semantic/011-three-node-cycle both declare expected: "pass" — so §10 was the
outlier, and a disagreement between two normative artifacts blocks a release
here rather than leaving an implementation to choose.

One correction to the issue's diagnosis, and it changes what the fix should
say.
This is not text left behind by the acyclicity reversal. git log -S
puts §4.2's permission in af2dec0 (#41, the ADR 0005 commit that withdrew
ERR_DEPENDENCY_CYCLE) and the whole of §10 — this paragraph included — in
e840e6d (#42), afterwards. That commit's own message states the intent
plainly:

Cycles MUST be detected rather than bounded by a recursion limit, because a
stack overflow is a crash and not a diagnostic.

So the rejection clause reinstated a withdrawn rule under a new code name one
commit after it was withdrawn, while the sentence around it is intended and
correct.

That is why this is not the suggested one-line deletion. A reader who arrives at
§10 first must not be able to re-infer rejection from silence, so the paragraph
now names §4.2's permission and says what detection does not mean — a walk
that meets a cycle terminates, it does not report:

Graph traversal. §4.2 makes the component graph a directed
graph an implementation walks, and permits that graph to contain a cycle. An
implementation MUST therefore detect cycles rather than relying on a recursion
limit to stop it — a stack overflow is a crash, not a diagnostic. Detecting one
means terminating the walk, not rejecting the document: that clause forbids
rejecting a composition for containing a cycle, so a traversal that meets one
MUST finish rather than report. …

conformance/README.md's phases table still named "dependency cycles" as
something the semantic phase enforces — the same withdrawn rule surviving in a
second document. That phase enforces cross-document agreement.

No ADR: this corrects a slip inside ADR 0005 §2's decision rather than deciding
policy, and that section reads true afterwards. Same reasoning as #45.

The gate that was missing

Worth more than the clause itself. tools/src/conformance.ts checked two
directions and not a third:

Direction Check Covers
fixture → registry checkCaseShape a code a diagnostics.json declares must exist
registry → fixture checkCoverage a code a §7 table declares must be exercised
prose → registry nothing a code named in a sentence

DIAGNOSTIC_ROW is anchored to ^\|, so an inline `ERR_CONNECTION_CYCLE`
was invisible to every gate — which is exactly why this reached main with CI
green. A code named in normative prose is the same promise to an implementer as
a row in the registry, and an implementer who looked this one up found nothing.

checkProseCodes closes it: every backticked ERR_ in the three spec.md
files must be declared by some registry, or recorded in HYPOTHETICAL with a
written reason, in the shape UNCOVERED and UNPINNED already use.

Two design points the diff does not make obvious:

  • It compares against the union of all three registries, not
    registryFor's reachable set. Component §3 and §10 legitimately name
    blueprint's ERR_UNKNOWN_COMPONENT, ERR_COMPONENT_NOT_PUBLISHED and
    ERR_UNKNOWN_COMPUTE_PROFILE, and listing §4 names
    ERR_UNREFERENCED_COMPONENT. Those are citations, and the question is whether
    the code exists at all.
  • It is scoped to spec.md. An ADR is immutable and records withdrawn codes
    as history, so ERR_DEPENDENCY_CYCLE in ADR 0005 is correctly a code that
    no longer exists. A named code is a promise only in a normative document.

HYPOTHETICAL has exactly one entry. Component §3 names ERR_SCHEMA_TOO_OLD as
a code that deliberately does not exist, to explain why a field from a newer
release is reported as ERR_UNKNOWN_FIELD. Run against the whole corpus, those
two were the only unresolved mentions in the repository.

Compatibility

  • Correction — does not change what validates.

No schema change and no validation behaviour change. The rule §10 named was
never implemented by this repository's validator, never had a registry entry,
and never had a fixture; §4.2 is unchanged and is what the corpus has always
enforced.

Verification

task check green — all 16 checks, including check:standards (6 schemas
meta-validated) and check:parity (133 subjects, 0 Ajv/Blaze disagreements);
neither self-skipped. 145 conformance cases pass, profile offline, 33/37
requirements pinned — unchanged from main.

The new gate was confirmed to fail closed three ways before being committed,
each restored afterwards:

Probe Reported
Reinstate the ERR_CONNECTION_CYCLE wording specifications/blueprint/v1/spec.md:751 — the exact line the issue names
Empty HYPOTHETICAL specifications/component/v1/spec.md:100 — proving the allowlist is load-bearing, not decorative
Insert `ERR_MADE_UP` into a spec.md reported at its line

grep -rn ERR_CONNECTION_CYCLE now returns nothing outside .git.

No fixture, deliberately

Nothing about validation changes, and the surviving rule is already pinned by
the two cycle cases the issue cites. The traversal requirement stays without a
stable ID by #42's own boundary: rules about what an implementation does don't
get one, because a document cannot violate them. The new gate is verified by
fail-closed demonstration, the way #42 verified its four lint rules.

docs/traceability.md is unchanged — no requirement IDs move.

Checklist

  • task check passes locally
  • schemas/dist/ regenerated with task bundle and committed (never edited by hand) — n/a, no schema change
  • Conformance fixtures added for every behavioural change, each citing a clause — n/a, see above
  • Normative prose updated in the affected spec.md — schema descriptions are explanatory, not normative
  • Commit messages are Conventional and correctly scoped (the scope drives release-please)
  • Commits are DCO signed off (git commit -s)

🤖 Generated with Claude Code

§10 said "A cycle is rejected with ERR_CONNECTION_CYCLE" while §4.2 of the same
document says the graph MAY contain a cycle and an implementation MUST NOT
reject a composition for that reason alone. The corpus sides with §4.2 —
semantic/002-connection-cycle and semantic/011-three-node-cycle both declare
expected: pass — so §10 was the outlier, and a disagreement between two
normative artifacts blocks a release here rather than leaving an implementation
to choose.

It is not residue from the acyclicity reversal, which matters for what the fix
should say. §4.2's permission landed in af2dec0, the ADR 0005 commit that
withdrew ERR_DEPENDENCY_CYCLE, and §10 was written afterwards in e840e6d, whose
own message states the intent: cycles MUST be detected rather than bounded by a
recursion limit, because a stack overflow is a crash and not a diagnostic. The
rejection clause reinstated a withdrawn rule under a new code name one commit
later. ERR_CONNECTION_CYCLE was in no registry and occurred nowhere else in the
repository.

So the traversal requirement is kept, and now cites §4.2 rather than
contradicting it. It also says what detection does not mean: a walk that meets
a cycle terminates, it does not report. Deleting the clause outright would have
left a reader who arrives at §10 first able to re-infer rejection from silence.

conformance/README.md's phases table still named "dependency cycles" as
something the semantic phase enforces — the same withdrawn rule surviving in a
second document. That phase enforces cross-document agreement.

No fixture: nothing about validation changes, and the surviving rule is already
pinned by the two cycle cases. No ADR: this corrects a slip inside ADR 0005 §2
rather than deciding policy, and that section reads true afterwards.

Refs #46

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
…registry

Two directions were checked and a third was not. checkCaseShape asks whether
every code a fixture declares is defined by the prose; checkCoverage asks
whether every code the prose defines is exercised by a fixture. Neither looks
at a code named in a sentence, because DIAGNOSTIC_ROW is anchored to ^| and so
sees only the registry tables.

That gap is how blueprint §10 shipped naming ERR_CONNECTION_CYCLE — a code no
table has ever defined, contradicting §4.2 in the same document — with every
check green. A code named in normative prose is the same promise to an
implementer as a row in the registry, and an implementer who looks this one up
finds nothing.

checkProseCodes closes it. Every backticked ERR_ in the three spec.md files
must be declared by some registry, or recorded in HYPOTHETICAL with a written
reason, in the shape UNCOVERED and UNPINNED already use.

The set compared against is the union of all three registries rather than
registryFor's reachable set. Component §3 and §10 legitimately name blueprint's
codes and listing §4 names ERR_UNREFERENCED_COMPONENT; those are citations, not
declarations, and the question being asked is whether the code exists at all.

One entry in HYPOTHETICAL. Component §3 names ERR_SCHEMA_TOO_OLD as a code that
deliberately does not exist, to explain why a field from a newer release is
reported as ERR_UNKNOWN_FIELD — a validator holding neither definition cannot
tell that case from a misspelling.

Scoped to spec.md. An ADR is immutable and records withdrawn codes as history,
so ERR_DEPENDENCY_CYCLE in ADR 0005 is correctly a code that no longer exists;
a named code is a promise only in a normative document.

Confirmed to fail closed three ways before being committed: the reinstated §10
wording is reported at blueprint spec.md:751, an emptied HYPOTHETICAL is
reported at component spec.md:100, and an invented code anywhere in a spec.md
is reported.

Refs #46

Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit 234a173 into main Aug 19, 2026
6 checks passed
@justinmerrell
justinmerrell deleted the fix/blueprint-cycle-rejection-clause branch August 19, 2026 00:01
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.

blueprint §10 still rejects connection cycles, contradicting §4.2, and names a code absent from the registry

1 participant