Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# `.config/` — Tool Configuration

Every linter, formatter, and hook config lives here. One directory, one
purpose: if a tool needs a config file and it is not provisioning the
container, it goes in here.

Policy and rationale:
[`docs/adr/0011-tooling-configuration-layout.md`](../docs/adr/0011-tooling-configuration-layout.md).
Enforcement: `task check:config` (CFG-01..CFG-08), implemented in
[`tools/src/config.ts`](../tools/src/config.ts).

The convention is shared with `musher-dev/development-container` and
`musher-dev/platform`. Keeping the three aligned is the point: a contributor
moving between them should not have to re-learn where the linter configs are.

## Index

Every file, the tool that reads it, and how that tool is pointed at it. A file
missing from this table fails CFG-03; a file no caller names fails CFG-04.

| File | Tool | How it is reached |
| --- | --- | --- |
| `lefthook.yml` | lefthook | **Auto-discovered.** Lefthook searches `.config/lefthook.*` natively |
| `lefthook-local.yml` | lefthook | Auto-discovered and merged. Gitignored; personal overrides only |
| `actions/actionlint.yaml` | actionlint | `-config-file .config/actions/actionlint.yaml` |
| `markdown/markdownlint.jsonc` | markdownlint-cli2 | `--config .config/markdown/markdownlint.jsonc` |
| `spelling/cspell.json` | cspell | `--config .config/spelling/cspell.json` |
| `spelling/musher.txt` | cspell | Resolved via `dictionaryDefinitions[].path` in `spelling/cspell.json` |

Call sites are [`Taskfile.yml`](../Taskfile.yml), [`taskfiles/`](../taskfiles/),
`.config/lefthook.yml`, and [`.github/workflows/`](../.github/workflows/).
Tool versions are pinned in
[`tools/package.json`](../tools/package.json) for anything installed by Bun,
and in [`.devcontainer/mise.toml`](../.devcontainer/mise.toml) for the rest —
with the CI workflow mirroring the same version, because CI is not a mise host
and does not read that file.

## Rules

1. **Bucket by concern.** `.config/<concern>/<tool>.<ext>`. A one-file bucket
is fine and collects siblings over time. The single exception is
`lefthook.yml`, which sits at the top level because lefthook's config search
does not descend past `.config/lefthook.*` — bucketing it would silently
stop every hook.
2. **No leading dot on filenames.** The directory is already dotted; a second
dot advertises a discovery mechanism that is deliberately not in use.
3. **Pass the path explicitly.** Except for lefthook, which finds this
directory on its own, every caller names its config with the tool's own
config flag. Never rely on default discovery — that is what put these files
at the repo root in the first place.
4. **Every file must have a caller.** A config nothing reads is dead weight
that still reads as authoritative.
5. **Every ignore needs a reason.** Suppressions, allowlists, and disabled
rules carry an inline comment explaining why the exception is acceptable.
6. **Configuration only.** No executables. A build asset belongs beside what
builds it; a repo-level runner belongs in `tools/src/`.

Adding a config? Verify the flag is live: point the tool at a nonexistent path
and confirm it fails. A silently-ignored `--config` is the failure mode this
whole directory exists to prevent, and it is one command to rule out.

## What does *not* live here

| Thing | Where | Why |
| --- | --- | --- |
| `Taskfile.yml` | Repo root | Task only discovers `Taskfile.*` at the root; `--taskfile` would break bare `task <name>` |
| `.gitignore`, `.gitattributes` | Repo root | Git reads these from the root only |
| `.editorconfig` | Repo root | EditorConfig walks up from the file being edited; no config-path flag exists. It is the *single* source for whitespace, line endings and encoding — `devcontainer.json` deliberately does not restate them |
| `LICENSE`, `NOTICE` | Repo root | GitHub detects a licence at the root only, and Apache-2.0 expects NOTICE to travel with the work |
| `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md` | `.github/` | Community health files, which GitHub resolves from there |
| `biome.json`, `tsconfig.json` | `tools/` | They belong to the `tools/` package and are resolved by it — a package's own config, not a repo-level one |
| `catalog.json`, `published.json` | Repo root | Published data artifacts, not tool configuration |
| `mise.toml`, `devcontainer.json` | `.devcontainer/` | They provision the environment rather than checking the code |
| `dependabot.yml`, `release-please/`, `rulesets/`, `workflows/` | `.github/` | GitHub reads these from fixed locations |

## Deliberately config-less

**shellcheck.** Its threshold is passed at the call site because `.shellcheckrc`
supports no `severity` key — the rcfile accepts only `disable`, `enable`,
`external-sources`, `source`, `source-path` and `shell`, and an unrecognised
key is silently ignored rather than rejected. A `.config/shell/shellcheckrc`
holding `severity=warning` would look like a gate and enforce nothing. If
shellcheck ever gains the key, the file becomes worth adding.

## A trap worth knowing

Lefthook's config search is **first-match-wins**, in this order:

```text
lefthook.* → .lefthook.* → .config/lefthook.*
```

A stray `lefthook.yml` at the repo root therefore **silently shadows** this
directory's copy — no warning, no error, just a different set of hooks.
`task check:config` (CFG-06) fails the build if one appears.
18 changes: 18 additions & 0 deletions .config/actions/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# actionlint configuration.
#
# Read by actionlint via an explicit -config-file path. actionlint's own
# default is .github/actionlint.yaml; this repository overrides it so every
# tool config sits in one place. See .config/README.md.
# Docs: https://github.com/rhysd/actionlint/blob/main/docs/config.md

# No self-hosted runners. GitHub-hosted labels are known to actionlint and need
# no declaration, so an empty list here is a statement rather than a stub: a
# workflow naming any other label is a typo, and actionlint will say so.
self-hosted-runner:
labels: []

# Every configuration variable this repository's workflows may read. The list
# is exhaustive on purpose -- with it, `vars.TYPO` is a build failure instead of
# an empty string that silently changes what a job does. Add a name here in the
# same change that adds the variable to the repository settings.
config-variables: []
17 changes: 17 additions & 0 deletions lefthook.yml → .config/lefthook.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Git hooks. Lefthook discovers this file itself, by searching
# `lefthook.*` -> `.lefthook.*` -> `.config/lefthook.*` and stopping at the
# first match -- which is why it is the one config here not passed by path,
# and why a stray lefthook.yml at the repo root would silently shadow it.
# `task check:config` (CFG-06) fails the build if one appears.
#
# Keep min_version in step with .devcontainer/mise.toml.
min_version: 2.1.10

pre-commit:
parallel: true
jobs:
Expand All @@ -9,6 +18,14 @@ pre-commit:
glob: 'specifications/**/*.schema.json'
run: task check:schema

- name: config
glob: '.config/**'
run: task check:config

- name: markdown
glob: '**/*.md'
run: task check:md

- name: drift
glob: 'specifications/**/*.schema.json'
run: task check:drift
Expand Down
48 changes: 48 additions & 0 deletions .config/markdown/markdownlint.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// markdownlint rules for this repository's Markdown.
//
// Read by markdownlint-cli2 via an explicit --config path; never
// auto-discovered. The call sites are `check:md` and `fmt:md` in Taskfile.yml.
// Rule reference: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
//
// Every relaxation below carries its reason, per .config/README.md rule 5.
{
"default": true,

// Prose here wraps at 80, but the limit is 120 and applies to prose only.
// spec.md is largely wide reference tables, and reflowing one to fit costs
// more readability than the long line does.
"MD013": {
"line_length": 120,
"tables": false,
"code_blocks": false,
"headings": false
},

// The `<a id="...">` anchors in spec.md are load-bearing: conformance
// fixtures link back to them, so they are part of the contract rather than
// decoration. `<br>` is the only way to break a line inside a table cell.
// No other element is used, and none should be.
"MD033": { "allowed_elements": ["a", "br"] },

// The specifications and the ADRs deliberately reuse headings such as
// "Context" and "Known debt" under different parents. Siblings must still be
// unique, which is what this setting keeps enforcing.
"MD024": { "siblings_only": true },

// A bold lead-in opening a paragraph -- "**Positive.** ..." in every ADR,
// "**In scope**" in GOVERNANCE -- is this repository's house style, and it is
// a lead-in rather than a heading: the text continues on the same line and
// the sections it sits inside already have real headings.
"MD036": false,

// A fenced block with no language is how this repository writes directory
// trees and terminal output, neither of which has a language to name.
"MD040": false,

// Table pipe alignment is presentation with no effect on rendering, and the
// rule cannot fix it -- adopting it would mean hand-realigning 263 pipes
// across three normative spec.md files, in tables that carry anchors and
// in-cell line breaks. The cost is a large hand edit to normative prose; the
// benefit is nil. Revisit if markdownlint gains an autofix for it.
"MD060": false
}
18 changes: 15 additions & 3 deletions cspell.json → .config/spelling/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"language": "en-GB",

// This config sits two levels below the repo root, and cspell resolves the
// globs below against the config file's own directory. Without globRoot,
// every repo-relative ignorePath would be matched against .config/spelling/,
// match nothing, and the check would keep passing while covering less.
"globRoot": "../..",

"dictionaryDefinitions": [
{
"name": "musher",
"path": "./.cspell/musher.txt",
"path": "./musher.txt",
"addWords": true,
"description": "Musher domain vocabulary and the tooling this repository uses. A word belongs here when it is a term of art, not when it is merely long."
}
Expand All @@ -19,13 +26,18 @@
"companies"
],
"ignorePaths": [
// Vendored or generated: not this repository's prose to correct.
"**/node_modules/**",
"site/**",
"tools/bun.lock",
".cspell/**",
"**/schemas/dist/**",
// The dictionary itself, and a licence whose wording is not ours to edit.
".config/spelling/**",
"LICENSE",
// Checksums and opaque identifiers, not words.
"published.json",
"**/schemas/dist/**",
// Conformance vectors are chosen for their bytes, including deliberate
// misspellings and malformed input. Correcting them would destroy the test.
"conformance/**/case.yaml",
"conformance/**/tree/**"
],
Expand Down
4 changes: 4 additions & 0 deletions .cspell/musher.txt → .config/spelling/musher.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ webp
xlarge

# --- Tooling and JSON Schema terms -------------------------------------------
codespell
commonmark
gpgsign
metaschema
nojekyll
pycache
subschema
subschemas
Taskfile
taskfiles
unindexed
unioned
vendored
13 changes: 9 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
"customizations": {
"vscode": {
"extensions": [
// VS Code does not read .editorconfig on its own, and .editorconfig is
// this repository's single source for whitespace and line endings.
"editorconfig.editorconfig",
"redhat.vscode-yaml",
"biomejs.biome",
"timonwong.shellcheck",
Expand All @@ -99,10 +102,12 @@
"settings": {
"editor.formatOnSave": true,
"editor.rulers": [80, 100],
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.eol": "\n",
"files.encoding": "utf8",

// Whitespace, line endings and encoding are NOT set here. .editorconfig
// states them once, for every editor, and the EditorConfig extension
// above applies them inside the container. Restating them made VS Code
// trim trailing whitespace from Markdown, where two trailing spaces are
// a hard line break and .editorconfig deliberately preserves them.

"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/scripts/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ on_error() {
trap 'on_error ${LINENO} "${BASH_COMMAND}"' ERR

# Installs lefthook git hooks for this repo. Best-effort: silently
# skips if lefthook isn't on PATH yet or no lefthook.yml exists.
# skips if lefthook isn't on PATH yet or no .config/lefthook.yml exists.
#
# Outputs:
# Writes progress to stderr via log()
install_lefthook_hooks() {
command -v lefthook >/dev/null 2>&1 || return 0
[[ -f "${SCRIPT_DIR}/../../lefthook.yml" ]] || return 0
[[ -f "${SCRIPT_DIR}/../../.config/lefthook.yml" ]] || return 0
log "Installing lefthook git hooks..."
(cd "${SCRIPT_DIR}/../.." && lefthook install >/dev/null 2>&1) || true
}
Expand Down
16 changes: 13 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Editor behaviour, for every editor.
#
# This is the single source for whitespace, line endings and encoding. The
# devcontainer deliberately does NOT restate these in
# customizations.vscode.settings: two statements of the same rule drift, and
# these two already had -- VS Code trimmed trailing whitespace from Markdown
# while the rule below preserves it.
#
# It stays at the repo root because EditorConfig walks up from the file being
# edited and accepts no config-path flag, which is also why it is one of the
# few things not under .config/. See docs/adr/0011.
root = true

[*]
Expand All @@ -8,8 +19,7 @@ trim_trailing_whitespace = true
indent_style = space
indent_size = 2

# Two trailing spaces are a hard line break in Markdown, so trimming them
# silently changes how the prose renders.
[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Unacceptable behaviour:
## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behaviour may be
reported to the maintainers at **conduct@musher.dev**. All complaints will be
reported to the maintainers at **<conduct@musher.dev>**. All complaints will be
reviewed and investigated promptly and fairly. Maintainers are obligated to
respect the privacy and security of the reporter.

Expand Down
17 changes: 12 additions & 5 deletions CONTRIBUTING.md → .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ ordinary codebase.
version, a change that would reject a previously valid document needs
neither the `v<N>` directory nor a migration note — there is no released
version to have validated against, so there is nothing to migrate from.
[ADR 0005](docs/adr/0005-platform-divergence-reconciliation.md) §1 sets the
rule out and [GOVERNANCE.md](GOVERNANCE.md#compatibility-review) carries it.
[ADR 0005](../docs/adr/0005-platform-divergence-reconciliation.md) §1 sets the
rule out and [GOVERNANCE.md](../GOVERNANCE.md#compatibility-review) carries it.
What the
window does **not** remove is maintainer approval, or the obligation to
declare the change as breaking in the commit trailer. It closes for a family
Expand All @@ -51,6 +51,11 @@ task setup # install tool dependencies and git hooks
task check # run everything CI runs
```

The tasks live in [`taskfiles/`](../taskfiles/), included by the root
`Taskfile.yml`. Every linter, formatter and hook config lives in
[`.config/`](../.config/README.md), and every caller names its config with the
tool's own flag — adding one has a fixed shape, described there.

## Making a change

```sh
Expand All @@ -76,7 +81,7 @@ runs it on every pull request and writes it to the job summary.
A fixture is a `case.yaml` when the rule is decided by reading one document,
and a `tree/` when it is decided by reading the item the document sits in —
a slug against its directory, a reference against a file. See
[conformance/README.md](conformance/README.md#case-trees).
[conformance/README.md](../conformance/README.md#case-trees).

Adding a diagnostic code or a requirement ID to a `spec.md` obliges you to add a
case for it.
Expand All @@ -89,6 +94,7 @@ runner's `UNCOVERED` list saying why the code cannot be exercised.
|---|---|
| `check:format` | Biome formatting and lint of `tools/` |
| `check:types` | TypeScript typecheck of `tools/` |
| `check:config` | The `.config/` layout: every file indexed, reachable, and a declaration (CFG-01..CFG-08) |
| `check:schema` | Every `src/` module is valid JSON Schema 2020-12; `$id`s are unique and canonical; no remote `$ref` |
| `check:drift` | The committed `dist/` bundle matches a fresh compile of `src/` |
| `check:examples` | Every file in `examples/` validates against its family's bundle |
Expand All @@ -100,6 +106,7 @@ runner's `UNCOVERED` list saying why the code cannot be exercised.
| `check:test` | The tooling test suite, including the publication-immutability regressions |
| `check:commits` | The Conventional Commits vocabulary agrees across its three copies |
| `check:links` | Every internal Markdown link and anchor resolves |
| `check:md` | markdownlint over every Markdown file |
| `check:spelling` | Prose, tooling, and schema descriptions spell-check clean |
| `check:shell` | ShellCheck over `.devcontainer/scripts` |
| `check:workflow` | actionlint over `.github/workflows` |
Expand Down Expand Up @@ -137,8 +144,8 @@ git commit -s -m "feat(component): add restartPolicy"
## Proposing a structural change

Changes to the repository architecture, the release model, or the family
taxonomy need an ADR in [`docs/adr/`](docs/adr/). Copy the format of
[ADR 0001](docs/adr/0001-canonical-repository-architecture.md), open it as a PR
taxonomy need an ADR in [`docs/adr/`](../docs/adr/). Copy the format of
[ADR 0001](../docs/adr/0001-canonical-repository-architecture.md), open it as a PR
on its own, and get it accepted before writing the implementation.

## Reporting a problem in the specification
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md → .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Report security issues privately through
[GitHub Security Advisories](https://github.com/musher-dev/spec/security/advisories/new),
or by email to **security@musher.dev**.
or by email to **<security@musher.dev>**.

Please do not open a public issue for a security report.

Expand Down
Loading