Skip to content

chore(repo): consolidate tooling configuration into .config/ - #53

Merged
justinmerrell merged 5 commits into
mainfrom
chore/consolidate-config-directory
Aug 19, 2026
Merged

chore(repo): consolidate tooling configuration into .config/#53
justinmerrell merged 5 commits into
mainfrom
chore/consolidate-config-directory

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

Consolidates every tool configuration into .config/, adopting the convention
musher-dev/development-container and musher-dev/platform already share, and
makes it a build failure rather than a comment. Along the way: two new linters,
the Taskfile.yml split into modules, and the community health files moved into
.github/.

Nothing under specifications/ or conformance/ changes behaviour, and no
commit is scoped to a family — this releases nothing.

The layout

.config/
  README.md                     the index: every file, its tool, how it is reached
  lefthook.yml                  top level by necessity -- see the trap below
  actions/actionlint.yaml       -config-file
  markdown/markdownlint.jsonc   --config
  spelling/cspell.json          --config
  spelling/musher.txt           dictionary, reached from cspell.json
taskfiles/                      Task modules included by the root Taskfile.yml

Root keeps only what can be nowhere else: Taskfile.yml, .gitignore,
.gitattributes, .editorconfig, LICENSE, NOTICE, README.md,
GOVERNANCE.md, and the two published data artifacts. tools/biome.json and
tools/tsconfig.json stay with their package.

The gate

tools/src/config.ts (task check:config) enforces CFG-01..CFG-08 with the
same codes the sibling repositories report from their Python repo config check — three repositories, one convention, one vocabulary. config.test.ts
provokes every code against a throwaway tree, because a gate that cannot fail is
indistinguishable from no gate.

Two silent failures this found

cspell's globRoot. cspell resolves ignorePaths against the config file's
own directory, so moving it two levels down re-rooted every repo-relative ignore
against .config/spelling/, where none matched. The check kept passing while
covering strictly less. Caught by comparing file counts (53 before, 53 after),
not by a red build — a check that has stopped enforcing anything does not fail.
globRoot is now explicit.

.editorconfig vs devcontainer.json. Both stated whitespace independently
and disagreed: .editorconfig exempts Markdown from trailing-whitespace
trimming because two trailing spaces are a hard line break, devcontainer.json
trimmed unconditionally. In-container VS Code was reformatting prose against the
repository's own rule. .editorconfig is now the single source.

New linters

  • actionlint gains a config. config-variables: [] turns vars.TYPO from an
    empty string that silently changes a job into a build failure — verified by
    planting one.
  • markdownlint is new, pinned in tools/. Five relaxations, each with its
    reason in the file. MD060 is off: it has no autofix, and adopting it meant
    hand-realigning 263 table pipes across three normative spec.md files for no
    rendering difference. The rest of the backlog was fixed — two bare e-mail
    addresses autolinked, three double blank lines closed.

Every new flag was verified live against a path that does not exist, so a
silently-ignored --config cannot hide behind a green build.

Taskfile split

Every pre-existing task name is unchanged; task --list differs only by
check:config, check:md and fmt:md. Two traps are recorded in the files: a
relative dir: in an included taskfile resolves against that file's
directory, and a bare task: check:format inside an included file resolves
within its own namespace.

Community health files

CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md moved to .github/.
LICENSE and NOTICE cannot — GitHub detects a licence at the root only.
GOVERNANCE.md could, but a normative spec.md links to it and so does every
ADR: it is contract surface, not a community file.

Why

Root dotfiles accumulate because every tool's README says to put them there, and
nothing distinguishes root-by-necessity (Git, EditorConfig, Task) from
root-by-default (lefthook, cspell). Platform shipped the prose version of this
rule with no gate and watched two non-configs accrete inside the directory and
four lint tools run on defaults. Rationale in
ADR 0011.

Verification

  • task check green locally, including all three new checks.
  • task --list diffed before and after the split: only the three new tasks.
  • Each CFG-0N provoked deliberately and confirmed to fire, then reverted.
  • Hooks reinstalled from scratch (rm -rf .git/hooks && task setup:hooks) and
    observed running from .config/lefthook.yml, including the new config job.
  • git status clean under conformance/; the two byte-sensitive -text parser
    fixtures untouched.

Known gap

YAML linting is not in this PR. yamllint is Python-only and this dev
container deliberately ships no Python or uv — its header says so. The agreed
alternative, google/yamlfmt in lint mode, could not be installed either: this
environment truncates large release downloads from GitHub's asset CDN, so it
could not be run or its output evaluated even once. Adding a check whose first
real execution is in CI would break the rule this PR just wrote down. Worth a
follow-up from a machine that can run it.

justinmerrell and others added 5 commits August 19, 2026 05:05
The root mixed normative content, project documents and tool configuration,
and nothing distinguished the files that must be at the root — Git,
EditorConfig, Task, none of which accepts a config-path flag — from the ones
that were merely there by default. Every new linter arrived at the root and
the set only grew.

Adopt the convention musher-dev/development-container and musher-dev/platform
already share: bucket by concern under .config/, no leading dot on filenames,
and every caller names its config with the tool's own flag. lefthook.yml stays
at .config/'s top level because lefthook's config search does not descend past
.config/lefthook.* — bucketing it would stop every hook running, silently.

Enforce it rather than document it. Platform shipped the prose version of this
rule with no gate, and in that state two non-configs accreted inside the
directory and four lint tools ran on defaults. tools/src/config.ts implements
CFG-01..CFG-08 with the same codes the sibling repositories report, so the
three share a vocabulary for reporting a breach even though this one runs the
check from Bun rather than Python.

Moving cspell surfaced the hazard the rule is about. cspell resolves
ignorePaths against globRoot, which defaults to the config file's directory, so
the move re-rooted every repo-relative ignore against .config/spelling/ where
none of them matched — the check kept passing while covering strictly less.
globRoot is now set explicitly. Verified by file count, not by a red build: a
check that has stopped enforcing anything does not fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Two more tool configs into .config/, both passed by explicit path.

actionlint ran on defaults. Its config now declares an empty self-hosted-runner
label list and an empty config-variables list, which turns `vars.TYPO` from an
empty string that silently changes what a job does into a build failure --
verified by planting an undeclared variable and watching it fail.

markdownlint is new. Five relaxations, each with its reason in the file: MD033
allows `a` and `br` because the spec.md anchors are load-bearing (conformance
fixtures link to them) and `br` is the only in-cell line break; MD036 because a
bold lead-in is this repository's house style in every ADR; MD060 because it is
presentation, has no autofix, and adopting it would mean hand-realigning 263
table pipes across three normative spec.md files for no rendering difference.
pull_request_template.md is excluded from the glob: it is a form fragment
GitHub renders into a textarea, so MD041 would be asking it to grow a title.

The rest of the backlog was small enough to fix outright, and markdownlint made
both edits itself: two bare e-mail addresses are now autolinked, and three
double blank lines in spec.md are single. No prose changed.

Both flags verified live against a path that does not exist -- exit 3 and 2
respectively, not a silent fallback to defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
One 330-line file held setup, build, eighteen checks, the CI entry points and
the ledger writes. It now holds the entry points and includes the rest, which
is the shape musher-dev/development-container and musher-dev/platform already
use.

Every pre-existing task name is unchanged -- CI, the hooks and CONTRIBUTING all
call them by name. `task --list` before and after differs only by the three new
tasks. Two mechanics make that hold, and both were traps worth recording in the
files themselves:

- A relative `dir:` in an included taskfile resolves against that file's own
  directory, so `dir: tools` would have silently meant `taskfiles/tools`. Every
  moved task uses `{{.ROOT_DIR}}`.
- A bare `task: check:format` inside an included file resolves within that
  file's namespace, as `ci:check:format`, and fails. The `ci` module anchors
  each reference to the root with a leading colon.

`check` and `setup` stay in the root rather than becoming a module `default`.
An included `default` is reachable as the bare namespace, but `task --list`
renders it `check:default`, which hides the command a newcomer most needs.

Also documents the new layout: CLAUDE.md gains .config/ and taskfiles/ in the
layout block and a non-negotiable for where tool config goes, and CONTRIBUTING
gains check:config and check:md in the table of what `task check` runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md are the three GitHub
resolves from .github/ as readily as from the root, and moving them leaves the
root holding the contract and the documents that describe it. Every link that
pointed at them moved in the same change; check:links is what proves it.

Three stay at the root, and not by omission:

- LICENSE and NOTICE, because .github/ is not a supported location for either.
  GitHub's license detection reads the root only, so moving LICENSE would drop
  the licence from the repository page and the API, and Apache-2.0 expects
  NOTICE to travel with the work.
- GOVERNANCE.md, which GitHub *does* support in .github/, because a normative
  spec.md links to it and so does every ADR. It is contract surface, and ADR
  0011's own test puts it at the root: what is visible there is content you
  read and edit, what is dotted is machinery that operates on it.

Renumbers the tooling-configuration ADR from 0007 to 0011. 0007 was already
taken by the naming-conventions ADR on the commit this branch started from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
.editorconfig and devcontainer.json each stated whitespace, line endings and
encoding independently, and they disagreed. .editorconfig exempts Markdown from
trailing-whitespace trimming because two trailing spaces are a hard line break;
devcontainer.json trimmed unconditionally. In-container VS Code was quietly
reformatting prose against the rule the repository had written down.

musher-dev/development-container resolves this the other way -- it deletes
.editorconfig and keeps devcontainer.json -- and its own rationale names the
exception this repository falls under: those settings reach VS Code inside the
container and nothing else, so a project with contributors working outside it
should keep .editorconfig. CONTRIBUTING documents that path, and anyone may send
a patch to a public specification.

So .editorconfig keeps the whitespace rules and gains the reason for the
Markdown exemption, devcontainer.json drops the four duplicated keys, and the
EditorConfig extension is added so in-container VS Code actually reads the file.
devcontainer.json keeps what is genuinely VS Code's own: rulers, format-on-save,
formatter bindings, the local schema associations.

Recorded as ADR 0011 §6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@justinmerrell
justinmerrell merged commit 18788f9 into main Aug 19, 2026
9 checks passed
@justinmerrell
justinmerrell deleted the chore/consolidate-config-directory branch August 19, 2026 08:11
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