From 8737169c69f5fba6cfd4b941e227e924c8fea12e Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Wed, 19 Aug 2026 05:05:33 +0000 Subject: [PATCH 1/5] chore(repo): consolidate tooling configuration into .config/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Signed-off-by: Justin Merrell --- .config/README.md | 90 ++++++ lefthook.yml => .config/lefthook.yml | 13 + cspell.json => .config/spelling/cspell.json | 18 +- {.cspell => .config/spelling}/musher.txt | 4 + .devcontainer/scripts/post-create.sh | 4 +- .gitignore | 3 + Taskfile.yml | 10 +- docs/adr/0007-tooling-configuration-layout.md | 172 +++++++++++ tools/src/commits.ts | 14 +- tools/src/config.test.ts | 164 +++++++++++ tools/src/config.ts | 272 ++++++++++++++++++ tools/src/testing/fixture.ts | 5 + 12 files changed, 759 insertions(+), 10 deletions(-) create mode 100644 .config/README.md rename lefthook.yml => .config/lefthook.yml (77%) rename cspell.json => .config/spelling/cspell.json (54%) rename {.cspell => .config/spelling}/musher.txt (96%) create mode 100644 docs/adr/0007-tooling-configuration-layout.md create mode 100644 tools/src/config.test.ts create mode 100644 tools/src/config.ts diff --git a/.config/README.md b/.config/README.md new file mode 100644 index 0000000..65759ba --- /dev/null +++ b/.config/README.md @@ -0,0 +1,90 @@ +# `.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/0007-tooling-configuration-layout.md`](../docs/adr/0007-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 | +| `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), `.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//.`. 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 ` | +| `.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 | +| `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. diff --git a/lefthook.yml b/.config/lefthook.yml similarity index 77% rename from lefthook.yml rename to .config/lefthook.yml index 6fdaf77..27c8368 100644 --- a/lefthook.yml +++ b/.config/lefthook.yml @@ -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: @@ -9,6 +18,10 @@ pre-commit: glob: 'specifications/**/*.schema.json' run: task check:schema + - name: config + glob: '.config/**' + run: task check:config + - name: drift glob: 'specifications/**/*.schema.json' run: task check:drift diff --git a/cspell.json b/.config/spelling/cspell.json similarity index 54% rename from cspell.json rename to .config/spelling/cspell.json index dc1e7e5..4a6db0b 100644 --- a/cspell.json +++ b/.config/spelling/cspell.json @@ -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." } @@ -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/**" ], diff --git a/.cspell/musher.txt b/.config/spelling/musher.txt similarity index 96% rename from .cspell/musher.txt rename to .config/spelling/musher.txt index 2485ae3..4ffe50d 100644 --- a/.cspell/musher.txt +++ b/.config/spelling/musher.txt @@ -41,12 +41,16 @@ webp xlarge # --- Tooling and JSON Schema terms ------------------------------------------- +codespell commonmark gpgsign metaschema nojekyll +pycache subschema subschemas +Taskfile +taskfiles unindexed unioned vendored diff --git a/.devcontainer/scripts/post-create.sh b/.devcontainer/scripts/post-create.sh index 9e2ed36..93faf24 100644 --- a/.devcontainer/scripts/post-create.sh +++ b/.devcontainer/scripts/post-create.sh @@ -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 } diff --git a/.gitignore b/.gitignore index 1f88158..6084e22 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,8 @@ Thumbs.db .idea/ *.swp +# Personal lefthook overrides, auto-merged beside .config/lefthook.yml. +/.config/lefthook-local.yml + # Task runner cache .task/ diff --git a/Taskfile.yml b/Taskfile.yml index 2bf7105..e179f9b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -89,6 +89,7 @@ tasks: desc: Run every check CI runs cmds: - task: check:format + - task: check:config - task: check:types - task: check:schema - task: check:drift @@ -152,7 +153,7 @@ tasks: cmds: - >- tools/node_modules/.bin/cspell --no-progress --no-summary --unique - --config cspell.json "**/*.md" "tools/src/**/*.ts" "**/*.schema.json" + --config .config/spelling/cspell.json "**/*.md" "tools/src/**/*.ts" "**/*.schema.json" check:test: desc: Run the tooling test suite @@ -178,6 +179,12 @@ tasks: cmds: - bun run src/lint.ts + check:config: + desc: Verify the .config/ layout (CFG-01..CFG-08) + dir: '{{.TOOLS_DIR}}' + cmds: + - bun run src/config.ts + check:drift: desc: Fail if a committed bundle does not match a fresh compile dir: '{{.TOOLS_DIR}}' @@ -274,6 +281,7 @@ tasks: desc: Lint suite as run in CI cmds: - task: check:format + - task: check:config - task: check:commits - task: check:links - task: check:spelling diff --git a/docs/adr/0007-tooling-configuration-layout.md b/docs/adr/0007-tooling-configuration-layout.md new file mode 100644 index 0000000..3042295 --- /dev/null +++ b/docs/adr/0007-tooling-configuration-layout.md @@ -0,0 +1,172 @@ +# ADR 0007: Tool configuration lives in `.config/`, passed by path + +- **Status:** Accepted +- **Date:** 2026-08-19 +- **Refines:** [ADR 0001](0001-canonical-repository-architecture.md) + +## Context + +A repository root accumulates dotfiles the way a desktop accumulates icons. +Nothing forces them elsewhere, every tool's README says "drop the dotfile at the +repo root", and once five or six are there the listing reads as a junk drawer. +This one held `lefthook.yml`, `cspell.json`, `.cspell/`, `.editorconfig`, +`.gitignore` and `.gitattributes` beside the normative content and the project +documents, and a reader could not tell which of those *must* be at the root from +which merely *default* to it. + +That distinction is the whole problem. Git, EditorConfig and Task genuinely have +nowhere else to go: none accepts a flag naming its config. Lefthook and cspell +were there because nobody told them otherwise. The two look identical in a +directory listing, so every new linter arrives at the root by default and the +set only grows. + +`musher-dev/development-container` and `musher-dev/platform` already solved +this, and share one convention between them. Adopting a third variant here would +cost a contributor moving between the three repositories the one thing the +convention buys: knowing where the linter configs are without looking. + +Platform's own history supplies the warning about how to adopt it. Its rule +shipped as prose with no mechanical enforcement, and in that state two +non-configuration files accreted inside the directory and four lint tools ran on +defaults with no config at all. A convention that only review enforces decays at +the speed of review. + +## Decision + +### 1. Four homes, and an ordered rule for choosing between them + +For a given configuration file, ask in order and stop at the first "yes": + +1. Can the tool **only** load from a fixed location, with no flag pointing + elsewhere? → the repo root. `Taskfile.yml`, `.gitignore`, `.gitattributes`, + `.editorconfig`. No alternative exists. +2. Does it configure a linter, formatter, or the git hooks? → + `.config//`. +3. Does it provision the container? → `.devcontainer/`. +4. Does it belong to the `tools/` package — declared by it, resolved by it? → + `tools/`. See §5. + +The directory is dotted because it is machinery, and it sits beside the +machinery the repository already has: `.devcontainer/`, `.github/`. What is +visible at the root is content you edit; what is dotted operates on it. + +### 2. Bucket by concern, with one forced exception + +`.config//.`. A one-file bucket is fine and collects +siblings over time. Filenames carry no leading dot — the directory is already +dotted, and a second dot advertises a discovery mechanism deliberately not in +use. + +`lefthook.yml` sits at the top level, and that is a constraint rather than a +preference. Lefthook's config search is first-match-wins over `lefthook.*` → +`.lefthook.*` → `.config/lefthook.*` and does not descend further, so bucketing +it would make it undiscoverable and every hook would stop running with no +warning. The same search is why a stray `lefthook.yml` at the root would +silently shadow this one, which is what CFG-06 exists to catch. + +### 3. Every caller passes the path explicitly + +Except lefthook, every caller names its config with the tool's own flag. Default +discovery is what scattered these files to the root to begin with; it is also +what lets a developer's personal `~/.config` file change what CI accepts. + +Adding a config carries one obligation beyond writing it: **verify the flag is +live** by pointing the tool at a path that does not exist and confirming it +fails. A silently-ignored `--config` is precisely the failure this directory +exists to prevent, and it is one command to rule out. + +Moving cspell demonstrated the sharper version of the same hazard. cspell +resolves `ignorePaths` against `globRoot`, which defaults to the config file's +own 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. It was caught by comparing the file count before and +after the move, not by the check failing, because a check that has stopped +enforcing anything does not fail. `globRoot` is now set explicitly, with a +comment saying why. + +### 4. The layout is a build failure, not a convention + +`tools/src/config.ts`, run as `task check:config`, enforces CFG-01..CFG-08: the +directory and its index exist; every file has an index row and a caller; no +leading-dot filenames; no root file shadowing lefthook; nothing but the index +and lefthook at the top level; no stray tool config at the root; no executables. + +The codes are deliberately identical to the ones the two sibling repositories +report from their Python `repo config check`. Three repositories, one +convention, one vocabulary for reporting a breach of it. + +There is **no** automated check that a config's *contents* are right — only that +it exists, is indexed, is reachable, and is a declaration. Judging whether a +given relaxation is justified is review's job, which is why every suppression +carries a written reason. + +### 5. Package configuration stays with its package + +`tools/biome.json` and `tools/tsconfig.json` do not move. They configure the +`tools/` package, are resolved relative to it, and travel with `package.json` +and the lockfile. `.config/` is for repo-level files passed by an explicit path; +a package's own config is neither. + +This mirrors the carve-out platform reached from the opposite direction, where +shared frontend configs outgrew `.config/` and became a workspace package once +they needed to own their dependencies. + +## Alternatives considered + +**Leave the configs at the root.** Zero migration cost, and every tool's +documented default. Rejected because it is the state that produced the problem: +the root grows monotonically, and nothing distinguishes root-by-necessity from +root-by-default until someone tries to move one and discovers which it was. + +**A `config/` directory without the leading dot.** More discoverable to a +newcomer, and it would sort with the content directories. Rejected for that +same reason — it would sort with `specifications/` and `conformance/`, which +are the contract. +Tooling configuration is not content, and the root listing is more useful when +the two are visibly different kinds of thing. + +**Port the sibling repositories' Python `repo` CLI.** It is the same rules +already written and already maintained. Rejected because this repository has no +Python: adding an interpreter, a dependency manager and an isolated environment +to CI to run one static check would be a larger and longer-lived cost than the +~200 lines of TypeScript that sit beside the eleven checks already in +`tools/src/`. The +codes are shared instead of the implementation, which is the part a contributor +actually reads. + +**Adopt the sibling repositories' codespell rather than keeping cspell.** It +would make the spelling bucket byte-identical across the three. Rejected because +cspell already carries this repository's domain dictionary and its +`ignoreRegExpList` is tuned to schema and anchor syntax; converging on the tool +would cost more than the divergence does. The bucket, the flag and the rules are +shared; the tool inside it need not be. + +## Consequences + +- The root holds normative content, project documents, and the four files that + can be nowhere else. Every other tool config is one `ls .config/` away, and + `.config/README.md` names the flag that reaches each one. +- Adding a linter now has a fixed shape: config in a bucket, a row in the index, + an explicit path at the call site, a pinned version, and a written reason for + every suppression. `task check:config` fails the build if any of the first + three is missing. +- A config nothing reads can no longer sit in the tree looking authoritative, + and a root `lefthook.yml` can no longer silently replace the hooks. +- `tools/src/config.test.ts` provokes every code against a throwaway tree. A + gate that cannot fail is indistinguishable from no gate, and two of these + rules guard failures that are silent by nature. +- The convention is now shared with `musher-dev/development-container` and + `musher-dev/platform`. That is an obligation as much as a benefit: a change to + the shape here should be raised against the other two rather than forked. + +## Follow-ups + +1. **The spelling tool diverges from the siblings.** They run codespell; this + runs cspell. The bucket and the rules match, the tool does not, so a + contributor moving between repositories still learns two commands. Revisit if + the domain dictionary can be expressed in codespell's format without loss. +2. **Version pinning is split across two files.** Bun-installed tools are pinned + in `tools/package.json`; the rest are pinned in `.devcontainer/mise.toml` and + mirrored in `.github/workflows/ci.yml`, because CI is not a mise host and + does not read that file. The mirrors are held in step by comment, not by a + check. The sibling repositories have the same seam. diff --git a/tools/src/commits.ts b/tools/src/commits.ts index 9c2b5fc..1256509 100644 --- a/tools/src/commits.ts +++ b/tools/src/commits.ts @@ -4,8 +4,9 @@ * `.github/conventional-commits.yaml` calls itself the single source of truth * and says it is "consumed by .github/workflows/lint-pr.yml". It is not * consumed by anything: the workflow inlines the same lists in its `with:` - * block, and the lefthook commit-msg hook inlines the types again inside a - * POSIX regex. Three copies, and a comment asking people to keep them in step. + * block, and the .config/lefthook.yml commit-msg hook inlines the types again + * inside a POSIX regex. Three copies, and a comment asking people to keep them + * in step. * * That is the same defect as a ruleset file documenting a rule it does not * carry — a claim a reader will believe and not think to check. The lists @@ -27,7 +28,7 @@ import { Failures, REPO_ROOT } from './spec.ts' const SOURCE = join(REPO_ROOT, '.github', 'conventional-commits.yaml') const WORKFLOW = join(REPO_ROOT, '.github', 'workflows', 'lint-pr.yml') -const HOOKS = join(REPO_ROOT, 'lefthook.yml') +const HOOKS = join(REPO_ROOT, '.config', 'lefthook.yml') /** A `key:` followed by an indented `- item` list, in a small YAML file. */ function yamlList(source: string, key: string): string[] { @@ -106,7 +107,12 @@ function main(): void { // The hook checks types only — a scope is optional, and the hook's own error // text lists the scopes for a human rather than enforcing them. - compare('lefthook.yml commit-msg types', types, hookTypes(readFileSync(HOOKS, 'utf8')), failures) + compare( + '.config/lefthook.yml commit-msg types', + types, + hookTypes(readFileSync(HOOKS, 'utf8')), + failures, + ) failures.report( `Conventional Commits vocabulary agrees across 3 file(s): ${types.length} type(s), ` + diff --git a/tools/src/config.test.ts b/tools/src/config.test.ts new file mode 100644 index 0000000..89361bb --- /dev/null +++ b/tools/src/config.test.ts @@ -0,0 +1,164 @@ +/** + * Each `.config/` layout rule, exercised against a throwaway tree. + * + * A gate that cannot fail is indistinguishable from no gate, and the two rules + * worth having here — CFG-04 and CFG-06 — both guard against failures that are + * silent by nature. So every code gets a case that provokes it, and a clean + * tree gets one that proves it stays quiet. + */ +import { afterEach, describe, expect, test } from 'bun:test' +import { configViolations } from './config.ts' +import { FixtureRepo } from './testing/fixture.ts' + +let repo: FixtureRepo | null = null + +afterEach(() => { + repo?.cleanup() + repo = null +}) + +const INDEX = [ + '# `.config/` — Tool Configuration', + '', + '| File | Tool | How it is reached |', + '| --- | --- | --- |', + '| `lefthook.yml` | lefthook | Auto-discovered |', + '| `spelling/cspell.json` | cspell | `--config .config/spelling/cspell.json` |', +].join('\n') + +/** A tree that satisfies every rule, as the starting point for each case. */ +function intact(): FixtureRepo { + repo = new FixtureRepo() + repo.writeFile('.config/README.md', INDEX) + repo.writeFile('.config/lefthook.yml', 'pre-commit:\n jobs: []\n') + repo.writeFile('.config/spelling/cspell.json', '{ "version": "0.2" }\n') + repo.writeFile('Taskfile.yml', 'version: "3"\n# cspell --config .config/spelling/cspell.json\n') + return repo +} + +/** The codes reported, so a case asserts on the rule rather than the prose. */ +function codes(root: string): string[] { + return configViolations(root).map((problem) => problem.slice(0, 6)) +} + +describe('an intact tree', () => { + test('reports nothing', () => { + expect(configViolations(intact().root)).toEqual([]) + }) +}) + +describe('CFG-01/02 — the directory and its index', () => { + test('CFG-01 fires when .config/ is absent, and reports nothing else', () => { + repo = new FixtureRepo() + repo.writeFile('Taskfile.yml', 'version: "3"\n') + // The remaining rules all describe the contents of a directory that is not + // there, so reporting them too would bury the one finding that matters. + expect(codes(repo.root)).toEqual(['CFG-01']) + }) + + test('CFG-02 fires when the index is missing', () => { + const fx = intact() + fx.remove('.config/README.md') + expect(codes(fx.root)).toContain('CFG-02') + }) + + test('a missing index does not also report every file as unindexed', () => { + const fx = intact() + fx.remove('.config/README.md') + expect(codes(fx.root)).not.toContain('CFG-03') + }) +}) + +describe('CFG-03/04 — indexed, and reachable', () => { + test('CFG-03 fires for a file with no index row', () => { + const fx = intact() + fx.writeFile('.config/yaml/yamllint.yaml', 'extends: default\n') + fx.writeFile('Taskfile.yml', 'version: "3"\n# yamllint -c .config/yaml/yamllint.yaml\n') + expect(codes(fx.root)).toEqual(['CFG-03']) + }) + + test('CFG-04 fires for a config no caller names', () => { + const fx = intact() + fx.writeFile('.config/yaml/yamllint.yaml', 'extends: default\n') + fx.writeFile('.config/README.md', `${INDEX}\n| \`yaml/yamllint.yaml\` | yamllint | \`-c\` |`) + expect(codes(fx.root)).toEqual(['CFG-04']) + }) + + test('lefthook is exempt from CFG-04, being auto-discovered', () => { + // Nothing names .config/lefthook.yml by path anywhere, by design. + expect(configViolations(intact().root)).toEqual([]) + }) + + test('a bucket sibling counts as a caller', () => { + // cspell reaches its dictionary through a path relative to the config's own + // directory, so the reference reads `./musher.txt` and never the repo path. + const fx = intact() + fx.writeFile('.config/spelling/musher.txt', 'blueprint\n') + fx.writeFile( + '.config/spelling/cspell.json', + '{ "dictionaryDefinitions": [{ "path": "./musher.txt" }] }\n', + ) + fx.writeFile('.config/README.md', `${INDEX}\n| \`spelling/musher.txt\` | cspell | path |`) + expect(configViolations(fx.root)).toEqual([]) + }) + + test('a sibling in another bucket does not count', () => { + const fx = intact() + fx.writeFile('.config/yaml/musher.txt', 'blueprint\n') + fx.writeFile('.config/README.md', `${INDEX}\n| \`yaml/musher.txt\` | cspell | path |`) + expect(codes(fx.root)).toEqual(['CFG-04']) + }) +}) + +describe('CFG-05 — no leading dot', () => { + test('fires on a dotted filename', () => { + const fx = intact() + fx.writeFile('.config/spelling/.cspell.json', '{}\n') + expect(codes(fx.root)).toContain('CFG-05') + }) +}) + +describe('CFG-06 — the shadowing trap', () => { + test.each(['lefthook.yml', 'lefthook.yaml', 'lefthook.toml', '.lefthook.yml', '.lefthook.json'])( + 'a root %s shadows the real config', + (name) => { + const fx = intact() + fx.writeFile(name, 'pre-commit:\n jobs: []\n') + expect(codes(fx.root)).toContain('CFG-06') + }, + ) +}) + +describe('CFG-07 — placement', () => { + test('fires on a config at the top level of .config/', () => { + const fx = intact() + fx.writeFile('.config/yamllint.yaml', 'extends: default\n') + fx.writeFile('.config/README.md', `${INDEX}\n| \`yamllint.yaml\` | yamllint | \`-c\` |`) + fx.writeFile('Taskfile.yml', 'version: "3"\n# yamllint -c .config/yamllint.yaml\n') + expect(codes(fx.root)).toEqual(['CFG-07']) + }) + + test('fires on a stray tool config at the repo root', () => { + const fx = intact() + fx.writeFile('cspell.json', '{}\n') + expect(codes(fx.root)).toEqual(['CFG-07']) + }) + + test('leaves root-only configs alone', () => { + // Git, Task and EditorConfig have no config-path flag, so the root is the + // only place they can be. Flagging them would make the gate impossible to pass. + const fx = intact() + for (const name of ['.gitignore', '.gitattributes', '.editorconfig']) { + fx.writeFile(name, '\n') + } + expect(configViolations(fx.root)).toEqual([]) + }) +}) + +describe('CFG-08 — configuration only', () => { + test.each(['run.sh', 'check.py', 'build.ts'])('fires on %s', (name) => { + const fx = intact() + fx.writeFile(`.config/spelling/${name}`, '\n') + expect(codes(fx.root)).toContain('CFG-08') + }) +}) diff --git a/tools/src/config.ts b/tools/src/config.ts new file mode 100644 index 0000000..5cda103 --- /dev/null +++ b/tools/src/config.ts @@ -0,0 +1,272 @@ +/** + * Detect `.config/` layout drift. + * + * Tool configuration lives in one place, and every caller names its config + * with the tool's own flag. That convention is worth exactly as much as its + * enforcement: `musher-dev/platform` shipped the prose version of this rule + * with no mechanical gate, and in that state two non-configs accreted inside + * the directory and four lint tools quietly ran on defaults. The rules below are + * what stops the same drift here. + * + * Two of them earn their place by catching failures that are silent rather + * than loud. CFG-06: lefthook's config search is first-match-wins over + * `lefthook.*` → `.lefthook.*` → `.config/lefthook.*`, so a stray root file + * shadows this directory's copy with no warning — a different set of hooks + * runs and nothing says so. CFG-04: a config nothing reads still reads as + * authoritative to the next person to open it. + * + * The codes are shared with `musher-dev/development-container` and + * `musher-dev/platform`, which run the same rules from a Python CLI. Three + * repositories, one convention, one vocabulary for reporting a breach of it. + * + * NON-NORMATIVE, like everything under tools/. + */ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import { join, relative } from 'node:path' +import { Failures, REPO_ROOT } from './spec.ts' + +const CONFIG_DIR = '.config' + +/** + * Configs the tool finds on its own, and so cannot be required to have a + * caller. Keep this short — every entry is a dependency on discovery + * behaviour that a tool upgrade could change underneath us. + */ +const AUTO_DISCOVERED: { readonly [file: string]: string } = { + 'lefthook.yml': 'lefthook searches .config/ natively', +} + +/** Gitignored personal overrides. Present or absent, never indexed. */ +const LOCAL_OVERRIDES = ['lefthook-local.yml', 'lefthook-local.yaml'] + +/** + * The only files allowed at the top level of `.config/` rather than inside a + * concern bucket. Lefthook qualifies solely because its config search does not + * descend past `.config/lefthook.*` — bucketing it would stop every hook. + */ +const TOP_LEVEL_ALLOWED = ['README.md', ...Object.keys(AUTO_DISCOVERED), ...LOCAL_OVERRIDES] + +/** + * Suffixes that make a file a program rather than a declaration. A denylist + * rather than an allowlist of config extensions, because a legitimate config + * may carry no extension at all and the drift to prevent is specifically an + * executable arriving. + */ +const EXECUTABLE_SUFFIXES = ['.sh', '.bash', '.zsh', '.py', '.mjs', '.cjs', '.js', '.ts', '.rb'] + +/** Every root filename that would win lefthook's first-match-wins search. */ +const SHADOWING = [ + 'lefthook.yml', + 'lefthook.yaml', + 'lefthook.json', + 'lefthook.jsonc', + 'lefthook.toml', + '.lefthook.yml', + '.lefthook.yaml', + '.lefthook.json', + '.lefthook.jsonc', + '.lefthook.toml', +] + +/** + * Tool configs that belong in `.config/` and must never reappear at the root. + * + * Git, Task and EditorConfig files are deliberately absent: they are root-only + * by their own tools' rules, with no flag that could point elsewhere. So are + * `tools/biome.json` and `tools/tsconfig.json`, which belong to the `tools/` + * package and are resolved by it. + */ +const STRAY_ROOT_CONFIGS = [ + 'cspell.json', + 'cspell.jsonc', + '.cspell.json', + 'cspell.config.json', + '.markdownlint.json', + '.markdownlint.jsonc', + '.markdownlint.yaml', + '.markdownlint-cli2.jsonc', + '.markdownlint-cli2.yaml', + '.yamllint', + '.yamllint.yml', + '.yamllint.yaml', + 'actionlint.yaml', + 'actionlint.yml', + '.shellcheckrc', + '.prettierrc', + '.eslintrc', + '.eslintrc.json', +] + +/** Files scanned for an explicit `.config/` reference. */ +const CALLER_GLOBS = [ + 'Taskfile.yml', + 'taskfiles', + '.github/workflows', + '.config', + '.devcontainer/scripts', +] + +/** Directory names never worth walking. */ +const EXCLUDED_DIRS = ['node_modules', '__pycache__', '.git'] + +/** Every file under `dir`, recursively, as paths relative to `dir`. */ +function walkFiles(dir: string, base: string = dir): string[] { + const found: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => + a.name.localeCompare(b.name), + )) { + if (EXCLUDED_DIRS.includes(entry.name)) continue + const absolute = join(dir, entry.name) + if (entry.isDirectory()) { + found.push(...walkFiles(absolute, base)) + continue + } + found.push(relative(base, absolute).split('\\').join('/')) + } + return found +} + +/** The concatenated text of every file that could name a config by path. */ +function callerText(repoRoot: string): string { + const chunks: string[] = [] + for (const entry of CALLER_GLOBS) { + const absolute = join(repoRoot, entry) + if (!existsSync(absolute)) continue + if (statSync(absolute).isDirectory()) { + for (const rel of walkFiles(absolute)) { + chunks.push(readFileSync(join(absolute, rel), 'utf8')) + } + continue + } + chunks.push(readFileSync(absolute, 'utf8')) + } + return chunks.join('\n') +} + +/** + * The other files in the same bucket, concatenated. + * + * Not every config is reached from a command line. A tool's own config may + * name a companion file — cspell's dictionary is reached only through + * `dictionaryDefinitions[].path` in `spelling/cspell.json` — and because that + * path resolves against the config's own directory it is written `./musher.txt`, + * never `.config/spelling/musher.txt`. Such a file has a caller; it is simply + * a bucket-internal one. The lookup is deliberately confined to the same + * bucket, so this cannot excuse an orphan elsewhere under `.config/`. + */ +function bucketSiblings(configDir: string, rel: string): string { + if (!rel.includes('/')) return '' + const bucket = join(configDir, rel.slice(0, rel.lastIndexOf('/'))) + const chunks: string[] = [] + for (const sibling of readdirSync(bucket, { withFileTypes: true })) { + if (!sibling.isFile() || join(bucket, sibling.name) === join(configDir, rel)) continue + chunks.push(readFileSync(join(bucket, sibling.name), 'utf8')) + } + return chunks.join('\n') +} + +/** The file names the index quotes in code spans, which is where it names them. */ +function indexedNames(index: string): Set { + return new Set(Array.from(index.matchAll(/`([^`]+)`/g), (match) => match[1] as string)) +} + +/** + * Every violation of the layout, as `CFG-NN: ` messages. + * + * Exported and taking `repoRoot` so the test suite can exercise each rule + * against a throwaway tree rather than against this repository. + */ +export function configViolations(repoRoot: string = REPO_ROOT): string[] { + const problems: string[] = [] + const configDir = join(repoRoot, CONFIG_DIR) + + if (!existsSync(configDir) || !statSync(configDir).isDirectory()) { + problems.push( + `CFG-01: ${CONFIG_DIR}/ does not exist. Tool configuration lives there — see ` + + 'docs/adr/0007-tooling-configuration-layout.md.', + ) + return problems + } + + const indexPath = join(configDir, 'README.md') + const hasIndex = existsSync(indexPath) + if (!hasIndex) { + problems.push( + `CFG-02: ${CONFIG_DIR}/README.md is missing. It is the index: one row per file, ` + + 'naming its tool and the flag that reaches it.', + ) + } + const indexed = hasIndex ? indexedNames(readFileSync(indexPath, 'utf8')) : new Set() + const callers = callerText(repoRoot) + + for (const rel of walkFiles(configDir)) { + const name = rel.split('/').pop() as string + if (name === 'README.md' || LOCAL_OVERRIDES.includes(name)) continue + + if (name.startsWith('.')) { + problems.push( + `CFG-05: ${CONFIG_DIR}/${rel} has a leading dot. The directory is already dotted; ` + + 'a second dot advertises auto-discovery that is deliberately not in use.', + ) + } + + const suffix = name.includes('.') ? `.${name.split('.').pop()}` : '' + if (EXECUTABLE_SUFFIXES.includes(suffix)) { + problems.push( + `CFG-08: ${CONFIG_DIR}/${rel} is a program, not a declaration. A build asset ` + + 'belongs beside what builds it; a repo-level runner belongs in tools/src/.', + ) + } + + if (!rel.includes('/') && !TOP_LEVEL_ALLOWED.includes(name)) { + problems.push( + `CFG-07: ${CONFIG_DIR}/${name} sits at the top level. Bucket it by concern: ` + + `${CONFIG_DIR}//${name}.`, + ) + } + + if (hasIndex && !indexed.has(rel) && !indexed.has(name)) { + problems.push( + `CFG-03: ${CONFIG_DIR}/${rel} has no row in ${CONFIG_DIR}/README.md. A config ` + + 'the index does not name is invisible to the next reader.', + ) + } + + const called = + AUTO_DISCOVERED[name] !== undefined || + callers.includes(`${CONFIG_DIR}/${rel}`) || + bucketSiblings(configDir, rel).includes(name) + if (!called) { + problems.push( + `CFG-04: ${CONFIG_DIR}/${rel} is named by no caller. Pass it explicitly with the ` + + "tool's own config flag, or delete it — dead config still reads as authoritative.", + ) + } + } + + for (const name of SHADOWING) { + if (!existsSync(join(repoRoot, name))) continue + problems.push( + `CFG-06: ${name} at the repo root shadows ${CONFIG_DIR}/lefthook.yml. Lefthook's ` + + 'search is first-match-wins, so a different set of hooks runs and nothing says so.', + ) + } + + for (const name of STRAY_ROOT_CONFIGS) { + if (!existsSync(join(repoRoot, name))) continue + problems.push( + `CFG-07: ${name} at the repo root belongs in ${CONFIG_DIR}//, passed to ` + + 'its tool by path. Default discovery is what scattered these files to begin with.', + ) + } + + return problems +} + +function main(): void { + const failures = new Failures() + for (const problem of configViolations()) failures.add(problem) + failures.report(`${CONFIG_DIR}/ layout is intact (CFG-01..CFG-08).`) +} + +if (import.meta.main) main() diff --git a/tools/src/testing/fixture.ts b/tools/src/testing/fixture.ts index d1e45ae..a448e9d 100644 --- a/tools/src/testing/fixture.ts +++ b/tools/src/testing/fixture.ts @@ -44,6 +44,11 @@ export class FixtureRepo { writeFileSync(absolute, contents, 'utf8') } + /** Delete a file relative to the repository root. */ + remove(path: string): void { + rmSync(join(this.root, path), { force: true }) + } + /** Write a family's committed bundle, at the layout the tooling expects. */ writeBundle(family: string, major: string, doc: Json): string { const path = join('specifications', family, major, 'schemas', 'dist', `${family}.schema.json`) From fab3062efa8840fa054723d7fbc6276bff2d5d25 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Wed, 19 Aug 2026 05:16:11 +0000 Subject: [PATCH 2/5] ci: give actionlint a config and adopt markdownlint 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) Signed-off-by: Justin Merrell --- .config/README.md | 2 + .config/actions/actionlint.yaml | 18 +++ .config/lefthook.yml | 4 + .config/markdown/markdownlint.jsonc | 48 ++++++++ CODE_OF_CONDUCT.md | 2 +- SECURITY.md | 2 +- Taskfile.yml | 25 +++- specifications/blueprint/v1/spec.md | 1 - specifications/component/v1/spec.md | 1 - specifications/listing/v1/spec.md | 1 - tools/bun.lock | 173 +++++++++++++++++++++++++++- tools/package.json | 1 + 12 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 .config/actions/actionlint.yaml create mode 100644 .config/markdown/markdownlint.jsonc diff --git a/.config/README.md b/.config/README.md index 65759ba..ec972aa 100644 --- a/.config/README.md +++ b/.config/README.md @@ -22,6 +22,8 @@ missing from this table fails CFG-03; a file no caller names fails CFG-04. | --- | --- | --- | | `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` | diff --git a/.config/actions/actionlint.yaml b/.config/actions/actionlint.yaml new file mode 100644 index 0000000..8bb5291 --- /dev/null +++ b/.config/actions/actionlint.yaml @@ -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: [] diff --git a/.config/lefthook.yml b/.config/lefthook.yml index 27c8368..cc4a4fd 100644 --- a/.config/lefthook.yml +++ b/.config/lefthook.yml @@ -22,6 +22,10 @@ pre-commit: glob: '.config/**' run: task check:config + - name: markdown + glob: '**/*.md' + run: task check:md + - name: drift glob: 'specifications/**/*.schema.json' run: task check:drift diff --git a/.config/markdown/markdownlint.jsonc b/.config/markdown/markdownlint.jsonc new file mode 100644 index 0000000..f8d63a8 --- /dev/null +++ b/.config/markdown/markdownlint.jsonc @@ -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 `` anchors in spec.md are load-bearing: conformance + // fixtures link back to them, so they are part of the contract rather than + // decoration. `
` 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 +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5aa18a8..86053f5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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 ****. All complaints will be reviewed and investigated promptly and fairly. Maintainers are obligated to respect the privacy and security of the reporter. diff --git a/SECURITY.md b/SECURITY.md index 65d8e1b..f7ef819 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 ****. Please do not open a public issue for a security report. diff --git a/Taskfile.yml b/Taskfile.yml index e179f9b..86d09d3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -102,6 +102,7 @@ tasks: - task: check:test - task: check:commits - task: check:links + - task: check:md - task: check:spelling - task: check:shell - task: check:workflow @@ -148,6 +149,17 @@ tasks: cmds: - bun run src/links.ts + check:md: + desc: Lint Markdown against .config/markdown/markdownlint.jsonc + cmds: + # pull_request_template.md is excluded because it is a form fragment, not a + # document: GitHub renders it into a textarea, so it has no title and MD041 + # would be asking it to grow one. + - >- + tools/node_modules/.bin/markdownlint-cli2 + --config .config/markdown/markdownlint.jsonc + "**/*.md" "#tools/node_modules" "#site" "#.github/pull_request_template.md" + check:spelling: desc: Spell-check prose, tooling sources, and schema descriptions cmds: @@ -228,12 +240,12 @@ tasks: check:workflow: desc: Lint the GitHub Actions workflows cmds: - - actionlint + - actionlint -config-file .config/actions/actionlint.yaml check:workflow:files: desc: Lint specific workflow files (used by the pre-commit hook) cmds: - - actionlint {{.CLI_ARGS}} + - actionlint -config-file .config/actions/actionlint.yaml {{.CLI_ARGS}} # =========================================================================== # Formatting @@ -245,6 +257,14 @@ tasks: cmds: - bun run biome format --write . + fmt:md: + desc: Apply the Markdown fixes markdownlint can make itself + cmds: + - >- + tools/node_modules/.bin/markdownlint-cli2 --fix + --config .config/markdown/markdownlint.jsonc + "**/*.md" "#tools/node_modules" "#site" "#.github/pull_request_template.md" + # =========================================================================== # Environment # =========================================================================== @@ -284,6 +304,7 @@ tasks: - task: check:config - task: check:commits - task: check:links + - task: check:md - task: check:spelling - task: check:shell - task: check:workflow diff --git a/specifications/blueprint/v1/spec.md b/specifications/blueprint/v1/spec.md index 996be6b..7ebaafa 100644 --- a/specifications/blueprint/v1/spec.md +++ b/specifications/blueprint/v1/spec.md @@ -705,7 +705,6 @@ An implementation MUST declare the **profile** it claims, as [conformance/README.md](../../../conformance/README.md#profiles) defines. A skipped case is never a passed one. - ##
9. Known debt Seeded from the platform's generated schema. The naming that arrived with it diff --git a/specifications/component/v1/spec.md b/specifications/component/v1/spec.md index 0bad8f4..ad830b3 100644 --- a/specifications/component/v1/spec.md +++ b/specifications/component/v1/spec.md @@ -867,7 +867,6 @@ The profiles, and the report shape a claim should take, are defined in A skipped case is never a passed one. An implementation MUST NOT claim a profile while skipping any case in a phase that profile requires. - ## 10. Known debt This schema was seeded from the platform's Pydantic-generated catalog schema. diff --git a/specifications/listing/v1/spec.md b/specifications/listing/v1/spec.md index 7588e2f..2934ffb 100644 --- a/specifications/listing/v1/spec.md +++ b/specifications/listing/v1/spec.md @@ -414,7 +414,6 @@ An implementation MUST declare the **profile** it claims, as [conformance/README.md](../../../conformance/README.md#profiles) defines. A skipped case is never a passed one. - ## 9. Known debt Seeded from the platform's generated schema. The naming that arrived with it diff --git a/tools/bun.lock b/tools/bun.lock index c9b477e..210402f 100644 --- a/tools/bun.lock +++ b/tools/bun.lock @@ -15,6 +15,7 @@ "@types/bun": "1.3.14", "@types/commonmark": "0.27.10", "cspell": "9", + "markdownlint-cli2": "0.22.1", "typescript": "7.0.2", }, }, @@ -182,14 +183,30 @@ "@cspell/url": ["@cspell/url@9.8.0", "", {}, "sha512-LY1lFiZLTQF/ma1ilfKmRmFmEOw0RfYhyl0UMhY7/d93b+kiDMhxP/9Qir4+5LyiRncaE3++ZcWno9Hya+ssRg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@sourcemeta/jsonschema": ["@sourcemeta/jsonschema@16.7.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "jsonschema": "npm/cli.js" } }, "sha512-r+vFn/wfOvDO04k44WBAPB6RwiT09yca2xH3LbcKAQ0RdoudeTli4dzOcNIWdUytqWPw9nZMBd4Y83YEU8dyzA=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/commonmark": ["@types/commonmark@0.27.10", "", {}, "sha512-iEZobUnvlM+UX5fXWCmC4eQXwCs01Z8Xa1W0VjiWUF/XsNy4BHtskqJ9MyLZVMHbA0ezhyonCDqz3hMvsCm6Hg=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -234,8 +251,12 @@ "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -244,6 +265,12 @@ "chalk-template": ["chalk-template@1.1.2", "", { "dependencies": { "chalk": "^5.2.0" } }, "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "clear-module": ["clear-module@4.1.3", "", { "dependencies": { "parent-module": "^2.0.0", "resolve-from": "^5.0.0" } }, "sha512-XdLrg7BnbXKntyrbs2dNjDN9CVoTQ+WV0i7jT5/r9ahzAaSDSzC9e2OVZB/QVwbxBb1/1AeObzjlxsYk5HFvww=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], @@ -270,6 +297,14 @@ "cspell-trie-lib": ["cspell-trie-lib@9.8.0", "", { "peerDependencies": { "@cspell/cspell-types": "9.8.0" } }, "sha512-GXIyqxya8QLp6SjKsAN9w3apvt1Ww7GKcZvTBaP76OfLoyb1QC6unwmObY2cZs1manCntGwHrgU6vFNuXnTzpw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], "env-paths": ["env-paths@4.0.0", "", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="], @@ -280,50 +315,176 @@ "fast-equals": ["fast-equals@6.0.2", "", {}, "sha512-sAjhj9ZhOxYCGiNMnZLaucOqf5ZeFnHNoKoAZiD9thhJ0N8RP85qJK759/97C/3L7NzzmGVB5uiX9AUpySZmUQ=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], "gensequence": ["gensequence@8.0.8", "", {}, "sha512-omMVniXEXpdx/vKxGnPRoO2394Otlze28TyxECbFVyoSpZ9H3EO7lemjcB12OpQJzRW4e5tt/dL1rOxry6aMHg=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "global-directory": ["global-directory@5.0.0", "", { "dependencies": { "ini": "6.0.0" } }, "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w=="], + "globby": ["globby@16.2.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" } }, "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q=="], + + "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + "is-safe-filename": ["is-safe-filename@0.1.1", "", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="], + + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + + "markdownlint": ["markdownlint@0.40.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2", "string-width": "8.1.0" } }, "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg=="], + + "markdownlint-cli2": ["markdownlint-cli2@0.22.1", "", { "dependencies": { "globby": "16.2.0", "js-yaml": "4.1.1", "jsonc-parser": "3.3.1", "jsonpointer": "5.0.1", "markdown-it": "14.1.1", "markdownlint": "0.40.0", "markdownlint-cli2-formatter-default": "0.0.6", "micromatch": "4.0.8", "smol-toml": "1.6.1" }, "bin": { "markdownlint-cli2": "markdownlint-cli2-bin.mjs" } }, "sha512-X14ZbytybDCXAViDmtN4DKLt9ZTrRn+oOrxTYlg3a65jS6QcYYbAkGPh/En2L/GDNbFYJ6lKaQSUNrrbN1bPrw=="], + + "markdownlint-cli2-formatter-default": ["markdownlint-cli2-formatter-default@0.0.6", "", { "peerDependencies": { "markdownlint-cli2": ">=0.0.4" } }, "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ=="], + "mdurl": ["mdurl@1.0.1", "", {}, "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "parent-module": ["parent-module@2.0.0", "", { "dependencies": { "callsites": "^3.1.0" } }, "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="], + "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], @@ -332,8 +493,18 @@ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "cspell-config-lib/smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="], + "import-fresh/parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "markdown-it/mdurl": ["mdurl@2.1.0", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], } } diff --git a/tools/package.json b/tools/package.json index d8cf239..602cdc0 100644 --- a/tools/package.json +++ b/tools/package.json @@ -30,6 +30,7 @@ "@types/bun": "1.3.14", "@types/commonmark": "0.27.10", "cspell": "9", + "markdownlint-cli2": "0.22.1", "typescript": "7.0.2" } } From 2a0b054fe8f5ee31828bcf1b93dc1a4b70f5cdf5 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Wed, 19 Aug 2026 06:00:03 +0000 Subject: [PATCH 3/5] chore(repo): split Taskfile.yml into taskfiles/ modules 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) Signed-off-by: Justin Merrell --- .config/README.md | 5 +- CLAUDE.md | 6 + CONTRIBUTING.md | 7 + Taskfile.yml | 247 ++++----------------------------- taskfiles/build.Taskfile.yml | 41 ++++++ taskfiles/check.Taskfile.yml | 158 +++++++++++++++++++++ taskfiles/ci.Taskfile.yml | 38 +++++ taskfiles/release.Taskfile.yml | 23 +++ taskfiles/setup.Taskfile.yml | 27 ++++ 9 files changed, 329 insertions(+), 223 deletions(-) create mode 100644 taskfiles/build.Taskfile.yml create mode 100644 taskfiles/check.Taskfile.yml create mode 100644 taskfiles/ci.Taskfile.yml create mode 100644 taskfiles/release.Taskfile.yml create mode 100644 taskfiles/setup.Taskfile.yml diff --git a/.config/README.md b/.config/README.md index ec972aa..b5ed1ff 100644 --- a/.config/README.md +++ b/.config/README.md @@ -27,8 +27,9 @@ missing from this table fails CFG-03; a file no caller names fails CFG-04. | `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), `.config/lefthook.yml`, and -[`.github/workflows/`](../.github/workflows/). Tool versions are pinned in +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 diff --git a/CLAUDE.md b/CLAUDE.md index 9fd289c..3a85fef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,10 @@ implementation of what is defined here. 7. **Exact-version URLs are rebuilt from tags, never from `main`.** `tools/src/site.ts` extracts each release from its own tag. Nothing in the working tree may feed a path that has already been published. +8. **Tool configuration lives in `.config//`, passed by path.** Never + add a config to the repo root when the tool accepts a config flag, and never + rely on default discovery — every caller names its config explicitly. + `task check:config` enforces this (CFG-01..CFG-08). See docs/adr/0007. ## Layout @@ -44,6 +48,8 @@ conformance//v/ // metadata.json + case.yaml + diagnostics.json tools/src/*.ts non-normative Bun scripts — the only language-bound code docs/adr/ architecture decision records +.config/ every linter, formatter, and hook config (see its README) +taskfiles/ Task modules included by the root Taskfile.yml ``` Three families, versioned independently: `component`, `blueprint`, `listing`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86d5e55..d679099 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -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 | @@ -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` | diff --git a/Taskfile.yml b/Taskfile.yml index 86d09d3..f805ff2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,6 +1,12 @@ # Musher Specification Taskfile # Usage: task [task-name] # List tasks: task --list +# +# This file stays at the repo root because Task only discovers `Taskfile.*` +# there: `--taskfile` would work but would break bare `task `. It holds +# the entry points; the tasks themselves live in taskfiles/, included below. +# Everything else this repository configures lives in .config/ — see +# .config/README.md and docs/adr/0007-tooling-configuration-layout.md. version: '3' @@ -13,6 +19,19 @@ env: # already provisions, so a rebuild does not re-download the world. BUN_INSTALL_CACHE_DIR: '{{.BUN_INSTALL_CACHE_DIR | default "/home/vscode/.cache/bun"}}' +# Namespaced except for build, which is flattened so `task bundle` and +# `task site:build` keep the names CI, the hooks and CONTRIBUTING use. An +# included file's `default` becomes the bare namespace, so `task setup` and +# `task check` still work. +includes: + setup: taskfiles/setup.Taskfile.yml + build: + taskfile: taskfiles/build.Taskfile.yml + flatten: true + check: taskfiles/check.Taskfile.yml + ci: taskfiles/ci.Taskfile.yml + ledger: taskfiles/release.Taskfile.yml + tasks: default: desc: List available tasks @@ -21,7 +40,12 @@ tasks: silent: true # =========================================================================== - # Setup + # Entry points + # + # The two aggregates live here rather than as a `default` inside their + # modules: an included `default` is reachable as the bare namespace, but + # `task --list` shows it as `check:default`, which hides the command a + # newcomer most needs to find. # =========================================================================== setup: @@ -30,61 +54,6 @@ tasks: - task: setup:tools - task: setup:hooks - setup:tools: - desc: Install the specification tooling dependencies - dir: '{{.TOOLS_DIR}}' - cmds: - - bun install - - setup:hooks: - desc: Install repository git hooks - status: - - test -f .git/hooks/pre-commit - cmds: - - cmd: git config --unset core.hooksPath - ignore_error: true - - lefthook install - - # =========================================================================== - # Build - # =========================================================================== - - bundle: - desc: Compile schemas/src into the published schemas/dist bundles - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/bundle.ts - - bun run src/catalog.ts - - bun run src/traceability.ts - - traceability: - desc: Regenerate the requirement traceability matrix - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/traceability.ts - - changes: - desc: Report what this branch does to the published contract (BASE=origin/main) - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/changes.ts {{.BASE | default "origin/main"}} - - catalog: - desc: Regenerate the SchemaStore catalog index - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/catalog.ts - - site:build: - desc: Assemble the publication tree served at schemas.musher.dev - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/site.ts - - # =========================================================================== - # Checks - # =========================================================================== - check: desc: Run every check CI runs cmds: @@ -107,146 +76,6 @@ tasks: - task: check:shell - task: check:workflow - check:standards: - desc: Meta-validate schemas with an independent JSON Schema toolchain - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/standards.ts - - check:parity: - desc: Require Ajv and Blaze to agree on every structural verdict - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/parity.ts - - check:published: - desc: Verify every released version against the publication ledger - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/published.ts - - check:ledger: - desc: Fail if published.json changed anything already recorded - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/ledger.ts check - - check:compat: - desc: Replay every released version's accepted documents against the candidate schema - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/compat.ts - - check:commits: - desc: Hold the Conventional Commits vocabulary in step across its three copies - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/commits.ts - - check:links: - desc: Resolve every internal Markdown link and anchor - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/links.ts - - check:md: - desc: Lint Markdown against .config/markdown/markdownlint.jsonc - cmds: - # pull_request_template.md is excluded because it is a form fragment, not a - # document: GitHub renders it into a textarea, so it has no title and MD041 - # would be asking it to grow one. - - >- - tools/node_modules/.bin/markdownlint-cli2 - --config .config/markdown/markdownlint.jsonc - "**/*.md" "#tools/node_modules" "#site" "#.github/pull_request_template.md" - - check:spelling: - desc: Spell-check prose, tooling sources, and schema descriptions - cmds: - - >- - tools/node_modules/.bin/cspell --no-progress --no-summary --unique - --config .config/spelling/cspell.json "**/*.md" "tools/src/**/*.ts" "**/*.schema.json" - - check:test: - desc: Run the tooling test suite - dir: '{{.TOOLS_DIR}}' - cmds: - - bun test - - ledger:record: - desc: Record the pending release from the release-please manifest - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/ledger.ts record - - ledger:sync: - desc: Backfill ledger entries for tags that exist but were never recorded - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/ledger.ts sync - - check:schema: - desc: Meta-validate schema modules, $id uniqueness, and $ref locality - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/lint.ts - - check:config: - desc: Verify the .config/ layout (CFG-01..CFG-08) - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/config.ts - - check:drift: - desc: Fail if a committed bundle does not match a fresh compile - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/drift.ts - - check:examples: - desc: Validate every example document against its family bundle - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/examples.ts - - check:conformance: - desc: Execute the conformance corpus - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run src/conformance.ts - - check:format: - desc: Check formatting and lint of the tooling sources - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run biome ci . - - check:types: - desc: Typecheck the tooling sources - dir: '{{.TOOLS_DIR}}' - cmds: - - bun run tsc --noEmit - - check:shell: - desc: ShellCheck the dev container scripts - cmds: - - shellcheck -x --source-path=SCRIPTDIR .devcontainer/scripts/*.sh .devcontainer/scripts/lib/*.sh - - check:shell:files: - desc: ShellCheck specific files (used by the pre-commit hook) - cmds: - - shellcheck -x --source-path=SCRIPTDIR {{.CLI_ARGS}} - - check:workflow: - desc: Lint the GitHub Actions workflows - cmds: - - actionlint -config-file .config/actions/actionlint.yaml - - check:workflow:files: - desc: Lint specific workflow files (used by the pre-commit hook) - cmds: - - actionlint -config-file .config/actions/actionlint.yaml {{.CLI_ARGS}} - # =========================================================================== # Formatting # =========================================================================== @@ -294,33 +123,9 @@ tasks: - task: check # =========================================================================== - # CI entry points + # Housekeeping # =========================================================================== - ci:lint: - desc: Lint suite as run in CI - cmds: - - task: check:format - - task: check:config - - task: check:commits - - task: check:links - - task: check:md - - task: check:spelling - - task: check:shell - - task: check:workflow - - ci:test: - desc: Schema and conformance suite as run in CI - cmds: - - task: check:schema - - task: check:drift - - task: check:examples - - task: check:conformance - - task: check:standards - - task: check:parity - - task: check:compat - - task: check:test - clean: desc: Remove generated output cmds: diff --git a/taskfiles/build.Taskfile.yml b/taskfiles/build.Taskfile.yml new file mode 100644 index 0000000..8672403 --- /dev/null +++ b/taskfiles/build.Taskfile.yml @@ -0,0 +1,41 @@ +# Build and reporting tasks — everything that regenerates a committed artifact. +# +# Included flattened, so these keep their bare names (`task bundle`, not +# `task build:bundle`): CI, the hooks and CONTRIBUTING all call them by name. +# +# `dir:` is absolute via {{.ROOT_DIR}} throughout this directory. A relative +# `dir:` in an included taskfile resolves against *that file's* directory. +version: '3' + +tasks: + bundle: + desc: Compile schemas/src into the published schemas/dist bundles + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/bundle.ts + - bun run src/catalog.ts + - bun run src/traceability.ts + + traceability: + desc: Regenerate the requirement traceability matrix + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/traceability.ts + + catalog: + desc: Regenerate the SchemaStore catalog index + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/catalog.ts + + changes: + desc: Report what this branch does to the published contract (BASE=origin/main) + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/changes.ts {{.BASE | default "origin/main"}} + + site:build: + desc: Assemble the publication tree served at schemas.musher.dev + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/site.ts diff --git a/taskfiles/check.Taskfile.yml b/taskfiles/check.Taskfile.yml new file mode 100644 index 0000000..5439a7c --- /dev/null +++ b/taskfiles/check.Taskfile.yml @@ -0,0 +1,158 @@ +# Every gate this repository runs, under the `check` namespace. +# +# The aggregate entry point is `task check`, defined in the root Taskfile.yml: +# it runs all of these in the order CI does -- cheap static checks first, then +# the normative gates, then the ones that need git history. +# +# Each check names its config with the tool's own flag. Default discovery is +# never relied on -- see .config/README.md and docs/adr/0007. +# +# `dir:` is absolute via {{.ROOT_DIR}} throughout this directory. A relative +# `dir:` in an included taskfile resolves against *that file's* directory, so +# `dir: tools` here would silently mean `taskfiles/tools`. +version: '3' + +tasks: + # --- The non-normative surface: formatting, types, and repository layout --- + + format: + desc: Check formatting and lint of the tooling sources + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run biome ci . + + types: + desc: Typecheck the tooling sources + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run tsc --noEmit + + config: + desc: Verify the .config/ layout (CFG-01..CFG-08) + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/config.ts + + # --- The normative gates --------------------------------------------------- + + schema: + desc: Meta-validate schema modules, $id uniqueness, and $ref locality + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/lint.ts + + drift: + desc: Fail if a committed bundle does not match a fresh compile + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/drift.ts + + examples: + desc: Validate every example document against its family bundle + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/examples.ts + + conformance: + desc: Execute the conformance corpus + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/conformance.ts + + standards: + desc: Meta-validate schemas with an independent JSON Schema toolchain + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/standards.ts + + parity: + desc: Require Ajv and Blaze to agree on every structural verdict + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/parity.ts + + test: + desc: Run the tooling test suite + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun test + + # --- The publication record ------------------------------------------------ + + published: + desc: Verify every released version against the publication ledger + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/published.ts + + ledger: + desc: Fail if published.json changed anything already recorded + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/ledger.ts check + + compat: + desc: Replay every released version's accepted documents against the candidate schema + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/compat.ts + + # --- Prose, workflows, and shell ------------------------------------------- + + commits: + desc: Hold the Conventional Commits vocabulary in step across its three copies + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/commits.ts + + links: + desc: Resolve every internal Markdown link and anchor + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/links.ts + + md: + desc: Lint Markdown against .config/markdown/markdownlint.jsonc + dir: '{{.ROOT_DIR}}' + cmds: + # pull_request_template.md is excluded because it is a form fragment, not + # a document: GitHub renders it into a textarea, so it has no title and + # MD041 would be asking it to grow one. + - >- + tools/node_modules/.bin/markdownlint-cli2 + --config .config/markdown/markdownlint.jsonc + "**/*.md" "#tools/node_modules" "#site" "#.github/pull_request_template.md" + + spelling: + desc: Spell-check prose, tooling sources, and schema descriptions + dir: '{{.ROOT_DIR}}' + cmds: + - >- + tools/node_modules/.bin/cspell --no-progress --no-summary --unique + --config .config/spelling/cspell.json "**/*.md" "tools/src/**/*.ts" "**/*.schema.json" + + shell: + desc: ShellCheck the dev container scripts + dir: '{{.ROOT_DIR}}' + cmds: + # --severity is passed here rather than in a config file: .shellcheckrc + # has no severity key and ignores unrecognised ones silently. + - shellcheck -x --source-path=SCRIPTDIR .devcontainer/scripts/*.sh .devcontainer/scripts/lib/*.sh + + shell:files: + desc: ShellCheck specific files (used by the pre-commit hook) + dir: '{{.ROOT_DIR}}' + cmds: + - shellcheck -x --source-path=SCRIPTDIR {{.CLI_ARGS}} + + workflow: + desc: Lint the GitHub Actions workflows + dir: '{{.ROOT_DIR}}' + cmds: + - actionlint -config-file .config/actions/actionlint.yaml + + workflow:files: + desc: Lint specific workflow files (used by the pre-commit hook) + dir: '{{.ROOT_DIR}}' + cmds: + - actionlint -config-file .config/actions/actionlint.yaml {{.CLI_ARGS}} diff --git a/taskfiles/ci.Taskfile.yml b/taskfiles/ci.Taskfile.yml new file mode 100644 index 0000000..8891226 --- /dev/null +++ b/taskfiles/ci.Taskfile.yml @@ -0,0 +1,38 @@ +# The two entry points .github/workflows/ci.yml calls, under the `ci` namespace. +# +# They exist so the workflow names one task per job rather than a list of +# checks: a check added to a job is added here, where a developer can run the +# same job locally, instead of only in YAML nobody executes before pushing. +# +# `task check` runs everything; these two are its split into the Lint and +# Schema jobs. Every check in either must also be in `check`. +# +# The leading colon on each reference is load-bearing: inside an included +# taskfile a bare `task: check:format` resolves within this file's own +# namespace, as `ci:check:format`, and fails. `:` anchors it to the root. +version: '3' + +tasks: + lint: + desc: Lint suite as run in CI + cmds: + - task: :check:format + - task: :check:config + - task: :check:commits + - task: :check:links + - task: :check:md + - task: :check:spelling + - task: :check:shell + - task: :check:workflow + + test: + desc: Schema and conformance suite as run in CI + cmds: + - task: :check:schema + - task: :check:drift + - task: :check:examples + - task: :check:conformance + - task: :check:standards + - task: :check:parity + - task: :check:compat + - task: :check:test diff --git a/taskfiles/release.Taskfile.yml b/taskfiles/release.Taskfile.yml new file mode 100644 index 0000000..46be6ef --- /dev/null +++ b/taskfiles/release.Taskfile.yml @@ -0,0 +1,23 @@ +# Publication ledger tasks. +# +# Included under the `ledger` namespace: `task ledger:record`, `ledger:sync`. +# `ledger:check` lives with the other checks, in check.Taskfile.yml, because it +# is a gate rather than a write. +# +# published.json is append-only (see docs/adr/0006). Nothing here edits an +# entry; `record` adds the pending release, `sync` backfills tags that exist +# but were never recorded. +version: '3' + +tasks: + record: + desc: Record the pending release from the release-please manifest + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/ledger.ts record + + sync: + desc: Backfill ledger entries for tags that exist but were never recorded + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun run src/ledger.ts sync diff --git a/taskfiles/setup.Taskfile.yml b/taskfiles/setup.Taskfile.yml new file mode 100644 index 0000000..b3cf00e --- /dev/null +++ b/taskfiles/setup.Taskfile.yml @@ -0,0 +1,27 @@ +# Setup tasks: everything needed to work in this repository. +# +# Included by the root Taskfile.yml under the `setup` namespace. The aggregate +# entry point is `task setup`, defined there. +# +# `dir:` is absolute via {{.ROOT_DIR}} throughout this directory. A relative +# `dir:` in an included taskfile resolves against *that file's* directory, so +# `dir: tools` here would silently mean `taskfiles/tools`. +version: '3' + +tasks: + tools: + desc: Install the specification tooling dependencies + dir: '{{.ROOT_DIR}}/{{.TOOLS_DIR}}' + cmds: + - bun install + + hooks: + desc: Install repository git hooks + dir: '{{.ROOT_DIR}}' + status: + - test -f .git/hooks/pre-commit + cmds: + # Lefthook reads .config/lefthook.yml, which it finds itself. + - cmd: git config --unset core.hooksPath + ignore_error: true + - lefthook install From 2abff9ba8a37d41e155b033154d31ec30df184c4 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Wed, 19 Aug 2026 06:15:50 +0000 Subject: [PATCH 4/5] docs: move the community health files into .github/ 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) Signed-off-by: Justin Merrell --- .config/README.md | 2 +- CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 14 +++++++------- SECURITY.md => .github/SECURITY.md | 0 .github/workflows/dco.yml | 2 +- CLAUDE.md | 2 +- GOVERNANCE.md | 4 ++-- README.md | 2 +- Taskfile.yml | 2 +- docs/adr/0002-conformance-case-trees.md | 2 +- .../adr/0004-listing-description-trust-boundary.md | 2 +- docs/adr/0007-naming-conventions.md | 2 +- docs/adr/0008-effective-values.md | 2 +- ...out.md => 0011-tooling-configuration-layout.md} | 2 +- specifications/component/v1/spec.md | 2 +- taskfiles/check.Taskfile.yml | 2 +- tools/src/config.ts | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (92%) rename SECURITY.md => .github/SECURITY.md (100%) rename docs/adr/{0007-tooling-configuration-layout.md => 0011-tooling-configuration-layout.md} (99%) diff --git a/.config/README.md b/.config/README.md index b5ed1ff..9ad8915 100644 --- a/.config/README.md +++ b/.config/README.md @@ -5,7 +5,7 @@ purpose: if a tool needs a config file and it is not provisioning the container, it goes in here. Policy and rationale: -[`docs/adr/0007-tooling-configuration-layout.md`](../docs/adr/0007-tooling-configuration-layout.md). +[`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). diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 92% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index d679099..b4fa41f 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,8 +30,8 @@ ordinary codebase. version, a change that would reject a previously valid document needs neither the `v` 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 @@ -51,9 +51,9 @@ task setup # install tool dependencies and git hooks task check # run everything CI runs ``` -The tasks live in [`taskfiles/`](taskfiles/), included by the root +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 +[`.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 @@ -81,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. @@ -144,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 diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml index 49ec0fb..dafe498 100644 --- a/.github/workflows/dco.yml +++ b/.github/workflows/dco.yml @@ -66,7 +66,7 @@ jobs: git rebase --signoff origin/main && git push --force-with-lease - See CONTRIBUTING.md → Sign your work. + See .github/CONTRIBUTING.md → Sign your work. MSG exit 1 fi diff --git a/CLAUDE.md b/CLAUDE.md index 3a85fef..7eeec19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ implementation of what is defined here. 8. **Tool configuration lives in `.config//`, passed by path.** Never add a config to the repo root when the tool accepts a config flag, and never rely on default discovery — every caller names its config explicitly. - `task check:config` enforces this (CFG-01..CFG-08). See docs/adr/0007. + `task check:config` enforces this (CFG-01..CFG-08). See docs/adr/0011. ## Layout diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 7d74057..e32e4f7 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -29,7 +29,7 @@ specification disputes. Current maintainers are listed in [`.github/CODEOWNERS`](.github/CODEOWNERS). **Contributors** are anyone opening an issue or pull request. No agreement -beyond the [DCO](CONTRIBUTING.md#sign-your-work) is required. +beyond the [DCO](.github/CONTRIBUTING.md#sign-your-work) is required. ## Decision process @@ -183,4 +183,4 @@ passed. ## Security -See [SECURITY.md](SECURITY.md). +See [SECURITY.md](.github/SECURITY.md). diff --git a/README.md b/README.md index 2853d3d..49145b5 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ fails the build if your commit does not match. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) and [GOVERNANCE.md](GOVERNANCE.md). +See [CONTRIBUTING.md](.github/CONTRIBUTING.md) and [GOVERNANCE.md](GOVERNANCE.md). ```sh task setup # install tooling and git hooks diff --git a/Taskfile.yml b/Taskfile.yml index f805ff2..a5c6859 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -6,7 +6,7 @@ # there: `--taskfile` would work but would break bare `task `. It holds # the entry points; the tasks themselves live in taskfiles/, included below. # Everything else this repository configures lives in .config/ — see -# .config/README.md and docs/adr/0007-tooling-configuration-layout.md. +# .config/README.md and docs/adr/0011-tooling-configuration-layout.md. version: '3' diff --git a/docs/adr/0002-conformance-case-trees.md b/docs/adr/0002-conformance-case-trees.md index 6a865cf..c183433 100644 --- a/docs/adr/0002-conformance-case-trees.md +++ b/docs/adr/0002-conformance-case-trees.md @@ -24,7 +24,7 @@ The `semantic` phase does not. Of the twenty-four diagnostic codes the three None is expressible as a single document, so none had a fixture. They reached `main` as prose with CI green, which is the failure mode -[CONTRIBUTING.md](../../CONTRIBUTING.md) ground rule 2 exists to prevent: "no +[CONTRIBUTING.md](../../.github/CONTRIBUTING.md) ground rule 2 exists to prevent: "no schema change without conformance fixtures." The rules themselves are not in doubt. Blueprint §3.1 defines the **item root** diff --git a/docs/adr/0004-listing-description-trust-boundary.md b/docs/adr/0004-listing-description-trust-boundary.md index 208cd2d..380ca47 100644 --- a/docs/adr/0004-listing-description-trust-boundary.md +++ b/docs/adr/0004-listing-description-trust-boundary.md @@ -13,7 +13,7 @@ origin — the classic shape of a stored cross-site scripting vulnerability. Nothing constrains it today. CommonMark permits raw HTML by design, so a conforming listing may contain `