Consistency audit phase 5: diagram discipline and mermaid linting¶
Master plan: PLAN-consistency-audit.md
Planning effort: medium. Almost all of the content is fixed by the upstream template -- two files copy verbatim and a shared block copies verbatim -- so there is little to get wrong in the diff. The judgement is entirely about where the lane runs and what it is allowed to block: kerbside's develop branch is behind a merge queue with five named required checks, and the template ships a path-filtered workflow that must not become a sixth.
Scope¶
In scope:
- #370
(
push-audit), whose only live finding is the missingdiagram-disciplineshared block inPUSH-AUDIT.md. - #381
(
mermaid-lint-ci):tools/mermaid-lint.shand a CI workflow that runs it. - The
debian-12-dockerrunner label in.github/actionlint.yaml, without which actionlint fails on the new workflow. - Documenting the new lane where kerbside documents its lanes:
docs/testing.mdand the workflow list in.claude/CLAUDE.md.
Out of scope:
- Converting any diagram. The
diagram-formataudit already passes -- No ASCII diagrams in README.md, AGENTS.md, ARCHITECTURE.md or docs/ -- and every mermaid diagram in the repository already renders (survey finding 3). This phase adds the policy and the enforcement; there is nothing to remediate behind them. - Making the lane a required status check. Decision 2.
- The upstream
diagram-conversionskill. It exists inshakenfist/development/.claude/skills/and is referenced by the template's README, but no audit asks kerbside to carry it and there is nothing here to convert. Recorded under Future work in the master plan rather than adopted. - Teaching Renovate to bump the pinned mermaid-cli image. Decision 3.
What the survey found¶
The master plan's phase 5 sketch is accurate in its substance --
the two issues are one upstream change, and #370's body is
stale -- but wrong in two details, and it omits the three facts
that actually decide how the phase is built. All five findings
below were verified against develop at 5051f98 on
2026-09-03, and the sketch has been corrected at source as part
of the planning commit.
1. The dates in the sketch are wrong. It says both issues "arrived on 2026-08-29". #370 was filed 2026-08-26 and is not a new issue at all -- it is the pre-push audit issue phase 1 already worked on, refiled against a criterion that has since grown a block. #381 was filed 2026-08-30.
2. #370's body is stale, exactly as the sketch says. The
issue text still reads missing shared block
path-traversal-review; missing shared block
python-version-discipline; missing shared block
functional-test-coverage -- all three of which phase 1 added
and all three of which are present on develop
(PUSH-AUDIT.md:218, :302, :498). The audit files an issue
and never refreshes its body. The live checker disagrees with
the issue it filed:
That is the whole of the finding.
3. Every mermaid diagram in the repository already renders.
This is the load-bearing survey result, and it was measured
rather than assumed: the upstream mermaid-lint.sh was run
against develop from a scratch copy, and all nine
diagram-bearing files passed.
Linting 9 file(s) containing mermaid diagrams.
ok ARCHITECTURE.md
ok docs/index.md
ok docs/proxy-architecture.md
ok docs/schema.md
ok docs/spice/spice-link-protocol.md
ok docs/spice/usb-redirection.md
ok docs/spice/vd-agent-protocol.md
ok docs/use-cases/ovirt.md
ok tools/ovirt-e2e/README.md
So this phase is a pure adoption. It carries no risk of the usual failure mode -- turning on a linter and discovering the existing corpus does not pass it, then having to choose between a red lane and a sweep nobody planned.
4. actionlint really does fail without the runner label, and
the label really does exist. Both halves were tested rather
than taken from the template's README. With the workflow staged
and debian-12-docker absent from .github/actionlint.yaml:
With the label added, actionlint passes. The label is not
invented: conductor/imagebuilder.py:74 in
shakenfist/private-ci builds an image named debian-12-docker
and labels its runners with it, and ryll's ci.yml,
release.yml, supply-chain.yml and manual-build.yml all
target it today. Kerbside has never used it -- every VM job here
is debian-12 -- so this phase introduces a runner shape that is
new to this repository but not to the fleet.
Worth recording because it nearly went unnoticed: pre-commit
run --all-files operates on tracked files, so an unstaged
new workflow is silently not linted. The first attempt at this
measurement passed twice, once with the label and once without,
because actionlint never saw the file. Stage it, or measure
nothing.
5. The two template files pass kerbside's own hooks
unmodified. tools/mermaid-lint.sh copied verbatim passes
shellcheck under this repository's pin (shellcheck-py
v0.11.0.1, scoped to ^(tools|demo)/), and skillsaw and the
whitespace hooks alongside it. No per-project edit is needed to
land it, which matters because decision 3 forbids editing it.
Also corrected at source, unrelated to phase 5. The signing
count that PR #391 fixed in the phase 4 plan survived in two
other places: PLAN-consistency-audit.md and the phase 4
fragment in docs/plans/index.md both still said 30 signed
mark-adding commits. The corrected figure is 29; the extra
one was 37c11de, a merge carrying GitHub's web-flow PGP
signature rather than a gitsign x509 attestation. Re-measured on
2026-09-03 against develop:
git log --no-merges --format=%H -- REVIEWS.md '.vscode/*.weaudit*' |
while read sha; do
git cat-file commit "$sha" | grep -q '^gpgsig' &&
echo signed || echo unsigned
done | sort | uniq -c
gives 29 signed, 46 unsigned. Both stale copies are fixed in
the planning commit.
Decisions¶
Decision 1 -- take the shipped workflow, not the gate-job
fold. The template offers two shapes: its own
path-filtered mermaid-lint.yml, or the script added as a step
in a job the repository's gate already covers. Kerbside cannot
take the second without a runner change. The only lint job here
is sanity_checks in functional-tests.yml:112, which runs on
[self-hosted, static], and static runners have no docker
daemon -- mmdc renders through puppeteer and needs one. Folding
would mean moving kerbside's cheapest and most frequently run job
onto a virtual machine to lint diagrams that change on a minority
of pull requests. The shipped workflow is also the only shape
with a proven deployment: shakenfist/development runs
byte-identical copies of both files on itself, and a diff against
the template is how drift is meant to be found.
Decision 2 -- the lane stays advisory and is not added to the
ruleset. This is the decision most likely to be argued with,
because an advisory lane is one a tired person can merge past.
Three reasons it is still right here. The shipped workflow is
path-filtered to **.md, and a path-filtered workflow that a
branch ruleset requires never reports on a pull request that
touches no markdown, which blocks that pull request forever --
the template's README says so explicitly and it is the exact
shape of failure tools/check-required-checks.sh exists to
catch. Adding a sixth required check means a hand edit to the
develop ruleset, and the five current ones
(Can see status, Can enqueue, Can merge,
Can enqueue: direct-qemu, Can enqueue: sf-e2e) are gate jobs
precisely so that path filters stay inside the workflow rather
than in the ruleset. And advisory is what kerbside already does
with rust.yml and demo-compose.yml: path-filtered, real, and
read as a red X rather than a block. Adding merge_group: to the
trigger is separately refused -- paths is not supported on that
event, so every merge would spin a virtual machine to re-lint
diagrams the pull request already linted.
Decision 3 -- both files copy verbatim, pinned image included,
and Renovate is not taught to bump it. No per-project
substitution, no reflowing to kerbside's comment style, no
switching the pin to a floating tag. The template's whole
mechanism for detecting drift is that a deployed copy diffs
cleanly against templates/mermaid-lint/, and an "improvement"
made here becomes a permanent diff that the next person has to
adjudicate. The pin does not move on its own -- Renovate's stock
managers do not read a docker reference out of a shell script --
and that is upstream's deliberate choice, because a mermaid major
version can reject a diagram its predecessor accepted. Kerbside
inherits the choice rather than re-litigating it; if the fleet
wants automated bumps, that is one change upstream, not fifteen
downstream.
Decision 4 -- diagram-discipline goes immediately before
plan-phase-references, not immediately after
llm-doc-discipline. Upstream's own PUSH-AUDIT.md places it
directly after llm-doc-discipline ends, but kerbside has
twenty-two lines of its own documentation-review bullets between
those two blocks (PUSH-AUDIT.md:385-405), covering
ARCHITECTURE.md, AGENTS.md and the per-protocol pages. Those
bullets extend the prose of section 2c; interposing a shared
block would split them from what they continue. Placing the new
block just above plan-phase-references at line 406 keeps
kerbside's prose intact and preserves upstream's relative
ordering of the three documentation blocks, so a future
side-by-side against canonical still reads in the same order.
Decision 5 -- the two issues stay in one phase. Inherited
from the master plan's sketch and re-affirmed by the survey.
They are one upstream change: the diagram-discipline block is
the policy half and templates/mermaid-lint/ is the enforcement
half, and the template's README says so in its second paragraph.
Splitting them to close #370 a day earlier would land a rule
nothing checks, then a checker for a rule already written --
each half individually reviewable but neither individually
meaningful.
Step plan¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 5a | low | haiku | none | Copy the diagram-discipline shared block verbatim from templates/shared-blocks/diagram-discipline.md in shakenfist/development (read it from origin/main, not from a possibly stale working copy) into PUSH-AUDIT.md in this worktree. Insert it immediately before the <!-- shared-block: plan-phase-references v1 --> line at PUSH-AUDIT.md:406, separated by a blank line above and below, and not after the llm-doc-discipline block -- see decision 4 for why. Copy the whole block including its <!-- shared-block: diagram-discipline v1 --> and <!-- shared-block-end --> markers. Do not reflow it, do not adjust its wrapping to kerbside's 80 columns, and do not edit a word of it: the audit compares it against canonical and an edit without a version bump is invisible to every other repository. Verify with diff <(sed -n '/shared-block: diagram-discipline/,/shared-block-end/p' PUSH-AUDIT.md) <(git -C <development> show origin/main:templates/shared-blocks/diagram-discipline.md) -- expect no output. |
| 5b | medium | sonnet | none | Add the mermaid lint lane. Copy templates/mermaid-lint/mermaid-lint.sh from shakenfist/development's origin/main to tools/mermaid-lint.sh (mode 0755) and templates/mermaid-lint/mermaid-lint.yml to .github/workflows/mermaid-lint.yml, both byte for byte -- decision 3, no substitution of any kind, and in particular leave the pinned mermaid-cli:11.4.2 tag alone. Then add - debian-12-docker to the self-hosted-runner: labels: list in .github/actionlint.yaml, directly after the existing - debian-12 entry, since the new job is the only one in the repository that needs a docker daemon; without it actionlint fails with label "debian-12-docker" is unknown. git add all three files before running pre-commit -- pre-commit run --all-files reads tracked files only, so an unstaged new workflow is silently not linted and actionlint will report a false pass (survey finding 4). Then verify three things and report all three: pre-commit run --all-files is clean; ./tools/mermaid-lint.sh exits zero over all nine diagram-bearing files (check the exit status directly -- do not pipe it into tail or grep, which reports the filter's status and turns a failure green); and the audit's mermaid-lint-ci check now passes. Docker is available on this host, so the linter really can be run rather than reasoned about. |
| 5c | medium | sonnet | none | Document the new lane. In docs/testing.md, which is the authority on what runs where, add mermaid-lint.yml to the list of workflows that are in neither CI tier, saying what it does (renders every tracked markdown file's mermaid diagrams in a container and fails on a parse error), that it is path-filtered to **.md excluding REVIEWS.md, that it is deliberately not a required status check, and why -- a path-filtered workflow that a ruleset requires never reports on pull requests it skips, and blocks them forever (decision 2). Mention that it needs a docker-capable runner, which is why it is the only job here on debian-12-docker. Then add the matching one-line entry to the "Neither tier" list in .claude/CLAUDE.md, in the same clipped style as the entries around it -- one line, no rationale, since that file is loaded into every session and the reasoning belongs in docs/testing.md. Do not add anything to AGENTS.md: no convention changed. Keep to the repository's 80-column wrap. |
Each step is its own commit:
- 5a:
Adopt the diagram discipline shared block. - 5b:
Lint mermaid diagrams in CI. - 5c:
Document the mermaid lint lane.
Risks and mitigations¶
| Risk | Mitigation |
|---|---|
The debian-12-docker label is new to kerbside, and a job whose label matches no runner queues forever instead of failing. |
The label is in production use: conductor/imagebuilder.py:74 in shakenfist/private-ci builds and labels the image, and four ryll workflows target it today. If it were ever wrong, the lane is advisory and path-filtered (decision 2), so a queued job blocks no merge -- which is a second reason not to make it required in the same change that introduces the label. |
| Someone later makes the lane a required status check to stop it being ignored, and every markdown-free pull request blocks forever. | Decision 2 is written down with the mechanism, and 5c puts the same warning in docs/testing.md next to the lane itself, where a person editing the ruleset is likely to look. The template's README says the same thing a third time. |
| The template files are "improved" during the copy -- reflowed to 80 columns, or the image pin floated -- and kerbside acquires a permanent diff against canonical. | Decision 3, restated as a constraint in both 5a and 5b, with the verifying diff command in 5a's brief and the byte-for-byte check in the Definition of done. Survey finding 5 removes the usual excuse: the files pass kerbside's hooks unmodified. |
| A future mermaid-cli bump rejects a diagram this phase measured as passing. | Expected, and the reason the script lints every tracked markdown file rather than only the changed ones: a bump is caught by the next run over the whole corpus, not left for whoever next opens the page. The pin means the bump is deliberate. |
The lint is measured with a pipe -- ./tools/mermaid-lint.sh \| tail -- and reports the filter's exit status, turning every failure green. |
Called out in 5b's brief and in the Definition of done. Upstream names this as how the tool was first mis-measured, so it is a documented rather than a hypothetical mistake. |
push-audit still fails after 5a because the block was edited in transit. |
The Definition of done compares the embedded block against canonical byte for byte, and the audit's own checker is run against the branch before the phase closes. |
Definition of done¶
Every item is checkable against the branch. <development> is a
checkout of shakenfist/development. Every box below was
verified against the branch at 635cab1 on 2026-09-03, before
the pull request was opened, except the last, which cannot be
checked until it merges.
-
python3 scripts/audit-check.py --repo-path <kerbside> --repo-name kerbsidereportspush-auditpass andmermaid-lint-cipass. - The
diagram-disciplineblock embedded inPUSH-AUDIT.mdis byte-identical totemplates/shared-blocks/diagram-discipline.mdatorigin/main, and sits between the kerbside documentation bullets andplan-phase-references. -
tools/mermaid-lint.shand.github/workflows/mermaid-lint.ymlare byte-identical to theirtemplates/mermaid-lint/originals:diff tools/mermaid-lint.sh <(git -C <development> show origin/main:templates/mermaid-lint/mermaid-lint.sh)and the same for the workflow, both silent. -
tools/mermaid-lint.shis executable, and./tools/mermaid-lint.shexits zero over nine files, measured from its own exit status and not through a pipe. -
.github/actionlint.yamllistsdebian-12-docker, andpre-commit run --all-filesis clean with the three new or changed files staged. -
./tools/check-required-checks.shpasses, and the develop ruleset in.github/exported-config/is unchanged -- the lane added no required check. -
docs/testing.mdlists the lane under the workflows in neither tier and says why it is not required; the workflow list in.claude/CLAUDE.mdhas one matching line; nothing was added toAGENTS.md. - #370 and #381 close when the pull request merges, from the
Fixestrailers on 5a and 5b. This criterion originally said the issues should close from a passing audit run rather than by hand; the commits close them directly instead, which is the form the repository's commit conventions ask for. The distinction the original wording was protecting -- not closing an issue while its check still fails -- is preserved by the first criterion above, which was verified before the pull request was opened, and by the audit itself: a check that still failed would file a fresh issue the next morning rather than leave the finding lost.
Back brief¶
This phase is unusually cheap for what it closes: two issues, and
almost every line of the diff is a verbatim copy whose
correctness is a diff rather than a judgement. The survey is
where the value was, and it produced one result worth leading
with -- all nine diagram-bearing files already lint clean, so
there is no hidden remediation sitting behind the new lane.
The decision to argue with now rather than later is decision 2:
the lane is advisory. It is the weaker of the two available
guarantees, and the argument for it is not "required checks are
bad" but "this particular workflow cannot be required without
breaking every markdown-free pull request, and folding it into
the gate would move kerbside's cheapest job onto a virtual
machine". If a stronger guarantee is wanted, the honest way to
get it is a separate change that gives sanity_checks a
docker-capable runner and adds the script there -- a CI-shape
change with its own cost, not a line in this phase.
Not a gate, but decide before 5b lands. Kerbside will be the
first repository in the fleet other than development itself to
run this lane, so it is also the first real test of the template
outside its author's repository. If the workflow needs any edit
at all to work here, that edit belongs upstream in
templates/mermaid-lint/ first and is copied back down -- the
same shape phase 3 took with the skillsaw checker, and for the
same reason.