From 00fd2851251f255f2b8e3397c930c7db7389fc4d Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 11 Sep 2026 14:07:46 -0400 Subject: [PATCH 1/2] build(markdown): swap the markdown gate from markdownlint-cli2 to rumdl (RIG-3663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces markdownlint-cli2 with rumdl as compass's whole-repo markdown gate — a parity swap to one Rust binary, matching orion (RIG-1994). ### What changed - New `.rumdl.toml` at the repo root is the single place the markdown rule policy lives. rumdl prunes `[global] exclude` before reading any file, so the exclusion set is plain config, not a command-line glob chain. - `moon.yml` `root:markdownlint` now runs `rumdl check .` (no glob operands, no --config chain); task name kept as `markdownlint`. Its inputs reference `/.rumdl.toml`. - `devenv.nix` and the CI toolchain comments swap the nixpkgs attribute markdownlint-cli2 → rumdl. The ci.yml PATH derives from parity.ts's parse of devenv.nix, so no literal attr to change there. - Deleted `.markdownlint.json` and `.markdownlint-cli2.jsonc` in the same change: rumdl reads markdownlint config files and `.markdownlint-cli2.jsonc` outranks `.markdownlint.json`, so leaving either would silently re-enable MD013 and drop the exclusions. - Repointed the docsite runtime consumer (`apps/eng-docs/scripts/gather.ts` + `deploy.ts`) at `.rumdl.toml` `[global] exclude` via a native `Bun.TOML.parse`; `parseExclusions`/`changedDocPages` now take the parsed array. Tests updated. - Rule dispositions on compass's own evidence: MD046 pinned `fenced` (config fix, clears 53); MD064 (14 stray double-spaces) fixed in prose via `rumdl fmt`, with 3 dependent DECISIONS.md anchor fragments corrected; MD057/MD032/MD076/MD077/MD075/MD049 disabled as verified false positives (MD057's 28 findings are all the DECISIONS paste-ready-row convention — zero genuine broken links). - CONTRIBUTING.md, docs/architecture/build-and-ci.md, and the renovate tooling comments updated. ### Notes - MD032 decided OFF on compass's own evidence, not copied: its 6 findings are all list-continuation false positives (auto-fix splits a sentence mid-line). It changes no file's fixability here. - The vendored `forks/` trees the issue's 916-file/51-issue figures assumed are already gone from this checkout (RIG-2336 landed), so the corpus is 217 tracked md; the gate scans 212 with exclusions. Spec-impact: none. Refs RIG-3663 Co-authored-by: Matt Wilkinson --- .github/workflows/ci.yml | 4 +- .markdownlint-cli2.jsonc | 24 ----- .markdownlint.json | 3 - .rumdl.toml | 88 +++++++++++++++++++ CONTRIBUTING.md | 2 +- apps/eng-docs/moon.yml | 2 +- apps/eng-docs/scripts/deploy.test.ts | 49 +++++------ apps/eng-docs/scripts/deploy.ts | 27 +++--- apps/eng-docs/scripts/gather.test.ts | 27 ++---- apps/eng-docs/scripts/gather.ts | 31 ++++--- devenv.nix | 12 +-- docs/architecture/build-and-ci.md | 2 +- docs/designs/DECISIONS.md | 6 +- .../infra/ci/compass-local-dev/design.md | 14 +-- docs/designs/ui/compass-native-app/design.md | 14 +-- moon.yml | 13 +-- tools/renovate/config.json5 | 6 +- tools/renovate/refresh-devenv-nixpkgs.core.ts | 4 +- tools/renovate/refresh-devenv-nixpkgs.test.ts | 2 +- tools/renovate/refresh-devenv-nixpkgs.ts | 4 +- 20 files changed, 187 insertions(+), 147 deletions(-) delete mode 100644 .markdownlint-cli2.jsonc delete mode 100644 .markdownlint.json create mode 100644 .rumdl.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1da7d26ca..4967b1cf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -394,7 +394,7 @@ jobs: - name: Put the dev shell's nixpkgs tools on PATH if: matrix.run == 'true' - # Phase two. buf, protoc, biome and markdownlint have no `setup-*` action + # Phase two. buf, protoc, biome and rumdl have no `setup-*` action # that could reproduce a nixpkgs pin. Rather than pick a close-enough # release from some other channel and hope, CI resolves the SAME # derivations the dev shell does, from the nixpkgs revision devenv.lock @@ -572,7 +572,7 @@ jobs: # # Unlike the moon gate, this job carries no phase-two nixpkgs-tools step: # the real-Postgres suite runs only `go test`, which needs no buf/protoc/ - # biome/markdownlint, so the language toolchains are the whole bootstrap. + # biome/rumdl, so the language toolchains are the whole bootstrap. run: | stores=$(nix eval --json -f tools/toolchain/gate-tools.nix langs \ | jq -r '.[].store') diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc deleted file mode 100644 index 9089e91f1..000000000 --- a/.markdownlint-cli2.jsonc +++ /dev/null @@ -1,24 +0,0 @@ -{ - // Rules live in .markdownlint.json. This file scopes markdownlint-cli2 to the - // whole repo, honoring .gitignore (skips node_modules + build outputs). - // Exclusions: - // - config/prompts/** + config/agents/** — agent-context PAYLOADS (RIG-1732), - // not docsite prose: these files are loaded verbatim into a model's context - // (the config-delivery bundle, packages/compass-agent/src/config-reader.ts), - // and their shape is fixed by the frozen manager-prompt design record - // (the `` wrapper, atx role headings matching OMP's own - // block-0). Prose rules (MD033/MD041/MD025/MD003) fight that purpose, so the - // payload subtrees are exempt — the agent-context-payload basis stands - // - go/server/setup_thread.md — the root Manager's setup-thread prompt, - // //go:embed-ed as setupThreadBody (go/server/serve_seed.go) and seeded - // verbatim into a model's context. Same agent-context-payload basis as - // config/prompts + config/agents: its conversational first line is by - // design, so MD041 (first-line-heading) does not apply. - "globs": ["**/*.md"], - "gitignore": true, - "ignores": [ - "config/prompts/**", - "config/agents/**", - "go/server/setup_thread.md" - ] -} diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 87e137e42..000000000 --- a/.markdownlint.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "MD013": false -} diff --git a/.rumdl.toml b/.rumdl.toml new file mode 100644 index 000000000..27e0b5143 --- /dev/null +++ b/.rumdl.toml @@ -0,0 +1,88 @@ +# rumdl — the repo's markdown lint gate (RIG-3663), run by moon `root:markdownlint`. +# +# This file is the SINGLE place the markdown rule policy lives. rumdl replaced +# markdownlint-cli2 as a parity swap: same enforcement, one Rust binary. rumdl +# reads markdownlint config files (and `.markdownlint-cli2.jsonc` outranks +# `.markdownlint.json`), so both were deleted in the same change — leaving one +# alongside this file would silently re-enable MD013 and drop the exclusions. +# +# The old cli2 gate passed its exclusions as command-line globs because cli2's +# config `ignores` applied only AFTER every matched file was read into memory. +# rumdl prunes on `exclude` before it reads anything, so the whole exclusion set +# lives here as plain config rather than a `--config` chain on the moon line, +# keeping the policy reviewable in one place. + +[global] +# Each excluded tree is an agent-context PAYLOAD, not docsite prose — loaded +# verbatim into a model's context, its shape fixed by design, so prose rules +# fight its purpose: +# - config/prompts + config/agents — the config-delivery bundle +# (packages/compass-agent/src/config-reader.ts), shaped by the frozen +# manager-prompt record (the `` wrapper, atx role headings +# matching OMP's own block-0). +# - go/server/setup_thread.md — the root Manager's setup-thread prompt, +# //go:embed-ed as setupThreadBody (go/server/serve_seed.go) and seeded +# verbatim; its conversational first line is by design. +# - node_modules — dependency trees. +# +# KEEP config/prompts + config/agents + setup_thread IN SYNC with the docsite +# gather's own exclusion source (apps/eng-docs/scripts/gather.ts), which now +# reads this file's `exclude`. +exclude = [ + "config/prompts/**", + "config/agents/**", + "go/server/setup_thread.md", + "**/node_modules/**", +] + +# Line length: prose in this repo wraps by meaning, not column. Carried over +# from the deleted `.markdownlint.json`, which set exactly this one rule. +[MD013] +enabled = false + +# Fence style: rumdl's default is `consistent`, which flags a file's FIRST +# fenced block. Pinning `fenced` is a config fix, not a disable — the repo uses +# fenced blocks throughout, and this clears 53 findings with no rule turned off. +[MD046] +style = "fenced" + +# ── Disabled rules ── +# Each is a rumdl-only rule markdownlint never had, verified false on compass's +# own files. Reasons recorded so a future reader need not re-derive them. + +# MD057 (relative-link existence): ALL 28 findings are the DECISIONS.md +# paste-ready-row convention — a design record quotes a `| DL-N | … | [record +# §x](compass-/design.md#…) |` row whose link is relative to the +# destination ledger (`docs/designs//DECISIONS.md`), not the record +# quoting it. Correct as written, only apparently broken when resolved from the +# record's own directory. Zero genuine broken links on this corpus, so the rule +# buys nothing here and misfires on a house convention across 11 records. +[MD057] +enabled = false + +# False: flags a list item's own continuation line (indented prose wrapping one +# bullet) as an unpadded list — the auto-fix inserts a blank line mid-sentence. +# 6 findings, all this pattern. +[MD032] +enabled = false + +# False: flags the paragraph break inside a multi-paragraph list item as an +# "unexpected blank line between list items"; the blank is required to keep the +# item's second paragraph. 17 findings. +[MD076] +enabled = false + +# False: flags continuation lines under deeply-nested bullets. 12 findings. +[MD077] +enabled = false + +# False: reads a TypeScript union type in prose (e.g. `"files" | "vcs" | "pr"`) +# as pipe-formatted table rows. 1 finding. +[MD075] +enabled = false + +# False: pairs an emphasis marker ACROSS an inline code span — an odd count of +# literal `_` in prose (a snake_case `filterAttrs` example) marries markers on +# opposite sides of a code span. 1 finding. +[MD049] +enabled = false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efbeaba97..72ef8e497 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ same check. It covers, across the workspace: drift gate (regenerate + `git diff`), and a fence rejecting internal-only symbols in the public generated trees. - **TypeScript:** `tsc --noEmit` and `bun test` per package, plus the UI build. -- **Whole repo:** `biome check` and `markdownlint`. +- **Whole repo:** `biome check` and `rumdl` (the `root:markdownlint` task). - **Toolchain parity:** the versions on PATH must match the dev shell's pins. Two things run only in CI. The **real-Postgres suites** are build-tagged diff --git a/apps/eng-docs/moon.yml b/apps/eng-docs/moon.yml index 7817e5ca0..4c3b0baa2 100644 --- a/apps/eng-docs/moon.yml +++ b/apps/eng-docs/moon.yml @@ -26,7 +26,7 @@ tasks: inputs: - 'scripts/gather.ts' - '/**/*.md' - - '/.markdownlint-cli2.jsonc' + - '/.rumdl.toml' outputs: ['src/content/docs', 'src/sidebar.generated.ts'] build: diff --git a/apps/eng-docs/scripts/deploy.test.ts b/apps/eng-docs/scripts/deploy.test.ts index 6bf537825..15c4c89c2 100644 --- a/apps/eng-docs/scripts/deploy.test.ts +++ b/apps/eng-docs/scripts/deploy.test.ts @@ -208,14 +208,10 @@ test("parsePreviewUrl returns null for empty output", () => { // ── changedDocPages ────────────────────────────────────────────────────────── -// Hermetic markdownlint config literal mirroring the real .markdownlint-cli2.jsonc -// shape (no file/network read). parseExclusions() reads `ignores` and appends -// `**/outputs/**`; isExcluded() additionally hard-excludes `apps/eng-docs/**`. -const markdownlintConfig = JSON.stringify({ - globs: ["**/*.md"], - gitignore: true, - ignores: ["config/prompts/**"], -}); +// Hermetic exclusion set mirroring the real `.rumdl.toml` `[global] exclude` +// (no file/network read). parseExclusions() appends `**/outputs/**`; +// isExcluded() additionally hard-excludes `apps/eng-docs/**`. +const exclude = ["config/prompts/**"]; test("changedDocPages maps a docs/ file to its site route", () => { // classify("docs/designs/repo/foo.md") → destRel "designs/repo/foo.md" @@ -224,7 +220,7 @@ test("changedDocPages maps a docs/ file to its site route", () => { expect( changedDocPages( [{ filename: "docs/designs/repo/foo.md", status: "modified" }], - markdownlintConfig, + exclude, ), ).toEqual([ { @@ -247,7 +243,7 @@ test("changedDocPages slugifies a dotted directory segment", () => { status: "modified", }, ], - markdownlintConfig, + exclude, ), ).toEqual([ { @@ -262,10 +258,7 @@ test("changedDocPages maps a package doc via packages/", () => { // packagePath → { id: "go", rest: "README.md" } → destRel // "packages/go/README.md"; routeSlug lowercases → "/packages/go/readme". expect( - changedDocPages( - [{ filename: "go/README.md", status: "added" }], - markdownlintConfig, - ), + changedDocPages([{ filename: "go/README.md", status: "added" }], exclude), ).toEqual([{ sourcePath: "go/README.md", route: "/packages/go/readme" }]); }); @@ -274,18 +267,18 @@ test("changedDocPages drops a non-markdown file", () => { expect( changedDocPages( [{ filename: "docs/designs/repo/diagram.png", status: "added" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); -test("changedDocPages drops a file excluded by the markdownlint ignores", () => { - // "config/prompts/example.md" matches the "config/prompts/**" ignore glob → +test("changedDocPages drops a file excluded by the rumdl exclude set", () => { + // "config/prompts/example.md" matches the "config/prompts/**" exclude glob → // isExcluded true. expect( changedDocPages( [{ filename: "config/prompts/example.md", status: "added" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); @@ -296,7 +289,7 @@ test("changedDocPages drops the docsite's own tree (apps/eng-docs/**)", () => { expect( changedDocPages( [{ filename: "apps/eng-docs/src/content/x.md", status: "modified" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); @@ -306,13 +299,13 @@ test("changedDocPages drops a deleted (removed) markdown file", () => { expect( changedDocPages( [{ filename: "docs/specs/web/gone.md", status: "removed" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); test("changedDocPages returns [] for empty input", () => { - expect(changedDocPages([], markdownlintConfig)).toEqual([]); + expect(changedDocPages([], exclude)).toEqual([]); }); test("changedDocPages returns [] when every file is dropped", () => { @@ -322,9 +315,9 @@ test("changedDocPages returns [] when every file is dropped", () => { { filename: "docs/designs/repo/diagram.png", status: "added" }, // non-md { filename: "docs/specs/web/gone.md", status: "removed" }, // removed { filename: "apps/eng-docs/src/content/x.md", status: "modified" }, // docsite tree - { filename: "config/prompts/example.md", status: "added" }, // markdownlint-excluded + { filename: "config/prompts/example.md", status: "added" }, // exclude-set match ], - markdownlintConfig, + exclude, ), ).toEqual([]); }); @@ -342,7 +335,7 @@ test("changedDocPages preserves input order and multiplicity, dropping in place" { filename: "docs/designs/repo/foo.md", status: "added" }, // duplicate { filename: "docs/specs/web/api.md", status: "modified" }, ], - markdownlintConfig, + exclude, ), ).toEqual([ { @@ -364,7 +357,7 @@ test("changedDocPages drops an uppercase .MD file (case-SENSITIVE extension gate expect( changedDocPages( [{ filename: "docs/designs/repo/FOO.MD", status: "added" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); @@ -375,7 +368,7 @@ test("changedDocPages keeps a renamed markdown file (only 'removed' is dropped)" expect( changedDocPages( [{ filename: "docs/designs/repo/foo.md", status: "renamed" }], - markdownlintConfig, + exclude, ), ).toEqual([ { @@ -394,7 +387,7 @@ test("changedDocPages drops node_modules/ and /dist/ paths (gather secondary ski { filename: "node_modules/p/readme.md", status: "added" }, { filename: "packages/x/dist/gen.md", status: "modified" }, ], - markdownlintConfig, + exclude, ), ).toEqual([]); }); @@ -416,7 +409,7 @@ for (const dir of [ expect( changedDocPages( [{ filename: `.${dir}/notes.md`, status: "added" }], - markdownlintConfig, + exclude, ), ).toEqual([]); }); diff --git a/apps/eng-docs/scripts/deploy.ts b/apps/eng-docs/scripts/deploy.ts index 799e1ed6a..09d7904a8 100644 --- a/apps/eng-docs/scripts/deploy.ts +++ b/apps/eng-docs/scripts/deploy.ts @@ -164,16 +164,15 @@ export interface ChangedPage { * resolve on the deployed preview. Pure: the gh lookup + config read live in * fetchChangedDocPages (the thin `$` runner below). * - * @param changed the PR's changed files (filename + status) - * @param markdownlintConfig the raw .markdownlint-cli2.jsonc (source of the - * exclusion set, via parseExclusions) — the single source of truth for what - * the gather drops. + * @param exclude the markdown gate's `.rumdl.toml` `[global] exclude` list + * (source of the exclusion set, via parseExclusions) — the single source of + * truth for what the gather drops. */ export function changedDocPages( changed: readonly ChangedFile[], - markdownlintConfig: string, + exclude: readonly string[], ): ChangedPage[] { - const exclusions = parseExclusions(markdownlintConfig); + const exclusions = parseExclusions(exclude); const pages: ChangedPage[] = []; for (const { filename, status } of changed) { // Deleted files no longer render — linking them would 404. @@ -414,7 +413,7 @@ async function main(): Promise { * The docsite pages a PR changed, resolved live: list the PR's changed files * via the GitHub API (not a local `git diff` — the CI agent clone is shallow, * so local history is unreliable), then map them through the pure - * changedDocPages against the repo's canonical markdownlint exclusion set. + * changedDocPages against the repo's canonical `.rumdl.toml` exclusion set. * Returns [] (no section) on any lookup failure — the deep-links are a * convenience, never worth failing an otherwise-good deploy over. */ @@ -438,23 +437,23 @@ async function fetchChangedDocPages( ); return []; } - // The exclusion set's single source of truth is the repo-root markdownlint - // config (the same file gather.ts reads). This file is apps/eng-docs/scripts/ + // The exclusion set's single source of truth is the repo-root `.rumdl.toml` + // (the same file gather.ts reads). This file is apps/eng-docs/scripts/ // deploy.ts, so the repo root is three dirname hops up from its path // (scripts → eng-docs → apps → root), matching gather.ts's own walk. - // Guarded like the gh call above: a missing/renamed config, a JSONC parse + // Guarded like the gh call above: a missing/renamed config, a TOML parse // failure, or a classify throw must also omit the section, never fail the // deploy — the docstring's "any lookup failure" contract covers this half too. try { const repoRoot = dirname( dirname(dirname(dirname(Bun.fileURLToPath(import.meta.url)))), ); - const markdownlintConfig = await Bun.file( - join(repoRoot, ".markdownlint-cli2.jsonc"), - ).text(); + const rumdlConfig = Bun.TOML.parse( + await Bun.file(join(repoRoot, ".rumdl.toml")).text(), + ) as { global?: { exclude?: string[] } }; return changedDocPages( parseChangedFiles(files.stdout.toString()), - markdownlintConfig, + rumdlConfig.global?.exclude ?? [], ); } catch (err) { console.log( diff --git a/apps/eng-docs/scripts/gather.test.ts b/apps/eng-docs/scripts/gather.test.ts index ac3ebf18f..e5d578111 100644 --- a/apps/eng-docs/scripts/gather.test.ts +++ b/apps/eng-docs/scripts/gather.test.ts @@ -239,34 +239,21 @@ describe("classify", () => { }); // -- parseExclusions ---------------------------------------------------------- -// Reads the canonical .markdownlint-cli2.jsonc `ignores` (single source of -// truth), tolerating // line comments, and always appends the one glob the -// gather owns: generated outputs/. +// Takes the canonical `.rumdl.toml` `[global] exclude` (single source of truth, +// parsed from TOML by the caller) and always appends the one glob the gather +// owns: generated outputs/. describe("parseExclusions", () => { - test("strips // line comments, keeps ignores, appends the owned glob", () => { - const config = [ - "{", - "\t// scopes the linter to the repo", - '\t"globs": ["**/*.md"],', - "\t// exclusions below", - '\t"ignores": [', - '\t\t"config/prompts/**",', - '\t\t"config/agents/**"', - "\t]", - "}", - ].join("\n"); - expect(parseExclusions(config)).toEqual([ + test("keeps the exclude list and appends the owned glob", () => { + expect(parseExclusions(["config/prompts/**", "config/agents/**"])).toEqual([ "config/prompts/**", "config/agents/**", "**/outputs/**", ]); }); - test("still yields the owned glob when the config has no ignores", () => { - expect(parseExclusions('{\n\t"globs": ["**/*.md"]\n}')).toEqual([ - "**/outputs/**", - ]); + test("still yields the owned glob when the exclude list is empty", () => { + expect(parseExclusions([])).toEqual(["**/outputs/**"]); }); }); diff --git a/apps/eng-docs/scripts/gather.ts b/apps/eng-docs/scripts/gather.ts index c99668166..9045d7911 100644 --- a/apps/eng-docs/scripts/gather.ts +++ b/apps/eng-docs/scripts/gather.ts @@ -6,7 +6,7 @@ // GitHub blobs. // // Coverage is the whole monorepo: every tracked `*.md` except the canonical -// top-level exclusion set (`.markdownlint-cli2.jsonc` `ignores`, the single +// top-level exclusion set (`.rumdl.toml` `[global] exclude`, the single // source of truth) and generated `outputs/`. Each file is classified into one // nav SECTION (Designs, Specs, Architecture, Packages, Contributing); the // section drives both its dest path under the content root and the generated @@ -114,17 +114,14 @@ export function classify(sourcePath: string): Classified { } /** - * The top-level exclusion globs, read from the canonical `.markdownlint-cli2.jsonc` - * `ignores` list (the single source of truth) plus the one the gather always - * adds: generated `outputs/`. Declared once there, not duplicated here — a new - * excluded dir is added in that config. + * The top-level exclusion globs, read from the canonical `.rumdl.toml` + * `[global] exclude` list (the single source of truth — the markdown gate's own + * exclusion set) plus the one the gather always adds: generated `outputs/`. + * Declared once there, not duplicated here — a new excluded dir is added in + * that config. */ -export function parseExclusions(markdownlintConfig: string): string[] { - // The file is JSONC; strip // line comments before parsing. - const stripped = markdownlintConfig.replace(/^\s*\/\/.*$/gm, ""); - const parsed = JSON.parse(stripped) as { ignores?: string[] }; - const ignores = parsed.ignores ?? []; - return [...ignores, "**/outputs/**"]; +export function parseExclusions(exclude: readonly string[]): string[] { + return [...exclude, "**/outputs/**"]; } // ── Pure construction ─────────────────────────────────────────────────────── @@ -414,11 +411,13 @@ async function main(): Promise { // Idempotent rebuild: clear the generated mirror, then repopulate. await rm(contentRoot, { recursive: true, force: true }); - // The canonical exclusion set lives in .markdownlint-cli2.jsonc (+ the one - // the gather always adds); read it so a new excluded dir is declared once. - const exclusions = parseExclusions( - await readFile(join(repoRoot, ".markdownlint-cli2.jsonc"), "utf8"), - ); + // The canonical exclusion set is the markdown gate's own `.rumdl.toml` + // `[global] exclude` (+ the one the gather always adds); read it so a new + // excluded dir is declared once. Bun parses TOML natively. + const rumdlConfig = Bun.TOML.parse( + await readFile(join(repoRoot, ".rumdl.toml"), "utf8"), + ) as { global?: { exclude?: string[] } }; + const exclusions = parseExclusions(rumdlConfig.global?.exclude ?? []); // All markdown under the repo, minus node_modules/build output and the // exclusion set. Sorted for deterministic per-section "first page" + output. diff --git a/devenv.nix b/devenv.nix index 85029f950..7505da705 100644 --- a/devenv.nix +++ b/devenv.nix @@ -95,13 +95,13 @@ in # sqlc 1.31.1. sqlc - # Lint gate. biome + markdownlint-cli2 are nixpkgs derivations here, not - # `bunx` — one nixpkgs pin means `moon run root:lint` / `root:markdownlint` - # resolve the identical binary and version for everyone, with no drift - # between a contributor's node_modules and anyone else's. `@biomejs/biome` - # stays a package.json devDep for the editor LSP. + # Lint gate. biome + rumdl are nixpkgs derivations here, not `bunx` — one + # nixpkgs pin means `moon run root:lint` / `root:markdownlint` resolve the + # identical binary and version for everyone, with no drift between a + # contributor's node_modules and anyone else's. `@biomejs/biome` stays a + # package.json devDep for the editor LSP. biome - markdownlint-cli2 + rumdl # actionlint: static checker for the GitHub Actions workflows under # .github/workflows/ (ci, eng-docs-deploy, publish-agent-image, renovate). diff --git a/docs/architecture/build-and-ci.md b/docs/architecture/build-and-ci.md index 3fb1c420e..b32292ed0 100644 --- a/docs/architecture/build-and-ci.md +++ b/docs/architecture/build-and-ci.md @@ -94,7 +94,7 @@ runtimes (bun, node, moon, go) from `tools/toolchain/versions/*.nix`; the Go analysis battery (golangci-lint, govulncheck, go-licenses, nilaway), each rebuilt against that go toolchain so analyzer and compiler share one Go version (`tools/toolchain/go-analysis.nix`); and the remaining nixpkgs-provided tools — -buf, protoc, biome, markdownlint — from the nixpkgs revision `devenv.lock` pins +buf, protoc, biome, rumdl — from the nixpkgs revision `devenv.lock` pins (`tools/toolchain/gate-tools.nix`). Their `bin/` dirs go on PATH; there are no `setup-*` actions. One owner, one activation path, remotely and locally. diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index 2c7bd800f..f6f481288 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -301,9 +301,9 @@ check enforces the mechanical half. Full rationale: | DL-107 | One framework-neutral shell IPC contract (`compass_rpc` + `head\|body\|end\|error` frames) carries gRPC-Web over a custom `fetch` for both modes — embedded pumps to the UDS, native-client to the TLS network door with shell-side private-CA trust and a shell-injected bearer | Active (Matt, 2026-08-02) | [native app §A2](ui/compass-native-app/design.md#a2--the-transport-seam-one-frame-contract-two-bridges) | | DL-108 | Embedded lifecycle is a Go stack supervisor (`go/internal/stack` + `compass-stack up\|down\|status`) of spawned children (never in-process), linger-by-default, productizing the dogfood chain with a supervised private Postgres, expiry-aware cert rotation, and lockfiled attach | Active (Matt, 2026-08-02) | [native app §A3](ui/compass-native-app/design.md#a3--embedded-lifecycle-a-go-stack-supervisor-spawned-by-the-shell) | | DL-109 | Native-app mode selection is `$XDG_CONFIG_HOME/compass/app.toml` (absent → embedded default); the native-client bearer is entered in a connect screen and stored keychain-first (0600-file fallback), never in config-file or argv | Active (Matt, 2026-08-02) | [native app §A4](ui/compass-native-app/design.md#a4--mode-selection-and-native-client-configuration) | -| DL-110 | The Compass native shell is Wails v3 (Go), its module under `go/` (`go/cmd/compass-app`) importing `go/internal/stack` and the bridge pump directly; re-decides DL-044's framework on the Go stack (Wails, not Tauri) | Active (Matt, 2026-08-02) | [native app §OQ1](ui/compass-native-app/design.md#oq1--shell-framework-tauri-rust-vs-wails-go-vs-other--blocks-t3) | -| DL-111 | A new `compass.v1` `WhoAmI` RPC returns the caller's account id from its own credential (embedded: socket ambient-admin; native-client: bearer subject), retiring the parked `caller_account_id` seam and the connect-screen caller-id field | Active (Matt, 2026-08-02) | [native app §OQ7](ui/compass-native-app/design.md#oq7--embedded-caller-identity-mechanism--blocks-t4-cross-lane-compass-server) | -| DL-112 | The `compass-agent` image is published to GHCR and `podman pull`ed by `compass-stack` at first run (not bundled in the app), making a GHCR publish lane for `compass-agent` a native-app packaging dependency | Active (Matt, 2026-08-02) | [native app §OQ6](ui/compass-native-app/design.md#oq6--agent-image-distribution-for-embedded-mode--blocks-t4-reshapes-t6) | +| DL-110 | The Compass native shell is Wails v3 (Go), its module under `go/` (`go/cmd/compass-app`) importing `go/internal/stack` and the bridge pump directly; re-decides DL-044's framework on the Go stack (Wails, not Tauri) | Active (Matt, 2026-08-02) | [native app §OQ1](ui/compass-native-app/design.md#oq1--shell-framework-tauri-rust-vs-wails-go-vs-other-blocks-t3) | +| DL-111 | A new `compass.v1` `WhoAmI` RPC returns the caller's account id from its own credential (embedded: socket ambient-admin; native-client: bearer subject), retiring the parked `caller_account_id` seam and the connect-screen caller-id field | Active (Matt, 2026-08-02) | [native app §OQ7](ui/compass-native-app/design.md#oq7--embedded-caller-identity-mechanism-blocks-t4-cross-lane-compass-server) | +| DL-112 | The `compass-agent` image is published to GHCR and `podman pull`ed by `compass-stack` at first run (not bundled in the app), making a GHCR publish lane for `compass-agent` a native-app packaging dependency | Active (Matt, 2026-08-02) | [native app §OQ6](ui/compass-native-app/design.md#oq6--agent-image-distribution-for-embedded-mode-blocks-t4-reshapes-t6) | | DL-183 | Cross-process embedded-stack teardown: `up` persists each supervised child's process-group ID plus a start-time identity token to an atomic `stack.pgids` record beside the lockfile; a fresh `down` refuses while an `up` holds the lock, then reads the record, verifies each group's identity before signaling, SIGTERMs each exact persisted pgid in reverse start order with bounded SIGKILL escalation, and confirms teardown per component (server/postgres socket quiescence, runner group-ESRCH) — scoped to the three supervised stack children (postgres, compass-server, compass-runner); live agent containers escape the process group via conmon double-fork and are OUT of scope (RIG-1884). Refines DL-108 (which stays Active) and realizes the frozen record's `down`-SIGTERMs-the-tree claim for the supervised children | Active (Matt, 2026-08-05) | [cross-process teardown §Approach](ui/compass-stack-cross-process-teardown/design.md#approach--option-a-persist-child-pgids-at-up-down-signals-the-persisted-groups) | | DL-214 | The T6 Linux app artifact is a versioned tarball of a nix-closure-backed bundle directory (store-rpathed binaries via the devenv.lock-pinned GTK closure + cc-wrapper, `.desktop` inside), not AppImage/`nix bundle`/`wails3 package`; no-nix-store end-user distribution is the A5 installer follow-up's concern, which wraps this same bundle dir | Active (Matt, 2026-08-19) | [packaging §A1](ui/compass-native-packaging/design.md#a1--bundle-format-a-versioned-tarball-of-a-nix-closure-backed-bundle-dir-fork-1-resolved) | | DL-215 | Bundle binary carriage is the sidecar `bin/` layout the shell already resolves (stack sibling + `dist` beside the executable), completed by the app prepending its executable dir to the spawned `compass-stack`'s PATH so the stack's LookPath children resolve in-bundle — never Go `embed` of ELF sidecars, never Wails asset bundling | Superseded by DL-238 (Matt, 2026-08-23) | [packaging §A2](ui/compass-native-packaging/design.md#a2--binary-embedding-sidecar-bin-layout--path-threading-fork-2-resolved) | diff --git a/docs/designs/infra/ci/compass-local-dev/design.md b/docs/designs/infra/ci/compass-local-dev/design.md index 037855745..e2a3d718e 100644 --- a/docs/designs/infra/ci/compass-local-dev/design.md +++ b/docs/designs/infra/ci/compass-local-dev/design.md @@ -405,7 +405,7 @@ Order: T1/T3 (UI-in-up — the compass-critical path), T2 (the direct-dial streaming smoke), then T4/T5/T6 (macOS), T7 (gate-hazard docs, small, can land any time). T3–T5 rebase onto RIG-1983 per GC-1. -### T1 — Wire the UI's base URL to the dev-http door (owner: compass-repo) +### T1 — Wire the UI's base URL to the dev-http door (owner: compass-repo) Direct-dial per §A1 (decided): a one-line env wire — `VITE_COMPASS_BASE_URL = "http://127.0.0.1:${toString @@ -435,7 +435,7 @@ carries no proxy work.) compass-ui:dev` task (`apps/ui/moon.yml:11-12`, exists today); T3 is that value's placement into the `devenv up` process definition. -### T2 — UI streaming e2e smoke against the dev door (owner: compass-repo) +### T2 — UI streaming e2e smoke against the dev door (owner: compass-repo) Direct-dial (decided) drops the proxy-streaming leg this task originally existed to verify — there is no middlebox to prove unbuffered. What remains @@ -504,7 +504,7 @@ compass-ui:dev` (or the T3 `devenv up` `compass-ui` process), load live-updates as the stream tails — the browser dialing the dev door directly, no middlebox in the path. -### T3 — `compass-ui` process in `devenv up` (owner: compass-repo) +### T3 — `compass-ui` process in `devenv up` (owner: compass-repo) Add the `compass-ui` process per §A1 inside the existing Linux guard: `exec bunx vite` from `cwd = ${config.devenv.root}/apps/ui`, env @@ -524,7 +524,7 @@ cutover leaves.** kill compass-server and verify the UI process survives (no spurious dependency beyond start ordering). -### T4 — macOS native services: relax the guards (owner: platform) +### T4 — macOS native services: relax the guards (owner: platform) Per §A2a: unguard `services.postgres`, `compass-server` (+ `compass-ui`), `dogfood:gen-cert`, `dogfood:mint-runner-token`; keep `compass-runner`, @@ -545,7 +545,7 @@ onto RIG-1983 (GC-1).** `devenv processes list` before/after); on a mac, `devenv up` reaches compass-ui ready and the T1 probe answers. -### T5 — macOS runner loop in the Linux VM (owner: compass-runner) +### T5 — macOS runner loop in the Linux VM (owner: compass-runner) Per §A2b: a darwin-guarded `dogfood:vm-runner` task that (a) asserts a running `podman machine` (fail-fast with setup instructions), (b) @@ -580,7 +580,7 @@ guard move; both tasks' interfaces acknowledge the co-edit). with the VM stopped, the task fails with the instruction message, not a hang. -### T6 — Darwin native-shell entrypoint + `compass-go:app` build task (owner: compass-app) +### T6 — Darwin native-shell entrypoint + `compass-go:app` build task (owner: compass-app) Per §A2c: retag `main.go` to `linux && gtk3`, and the welded trio `embedded.go` + `lifecycle.go` + `preflight_adapters.go` (all `unix && @@ -615,7 +615,7 @@ through to it. (the main.go:10-12 posture: "the window points at a daemon a developer starts by hand"). Existing Go gate battery (`compass-go:ci`) green. -### T7 — Gate-hazard documentation (owner: compass-repo; small, independent) +### T7 — Gate-hazard documentation (owner: compass-repo; small, independent) Per §A3: extend the `hk.pkl` header comment (hk.pkl:1-17) with the temp-worktree drift hazard, the interim workaround (verify inside the devenv diff --git a/docs/designs/ui/compass-native-app/design.md b/docs/designs/ui/compass-native-app/design.md index 705b6311a..f4bf2d784 100644 --- a/docs/designs/ui/compass-native-app/design.md +++ b/docs/designs/ui/compass-native-app/design.md @@ -598,7 +598,7 @@ Plan above (cited by its DL row) and stamped as an `Active` row in Question/Options that led to the ruling. The resolutions froze on merge and are the contract the executing lanes read. -### OQ1 — Shell framework: Tauri (Rust) vs Wails (Go) vs other *(blocks T3)* +### OQ1 — Shell framework: Tauri (Rust) vs Wails (Go) vs other *(blocks T3)* **Question:** which framework hosts the webview and implements the IPC bridge? @@ -652,7 +652,7 @@ and the bridge pump directly. Supersedes DL-044's Tauri framework choice (DL-110). `apps/ui`'s `@tauri-apps/*` deps are dropped in T1; `daemon-transport.ts` is re-pointed at Wails runtime events behind `ShellIpc`. -### OQ2 — Embedded server lifecycle: in-process vs spawned; linger-on-quit *(blocks T4 final behavior; T2 proceeds either way)* +### OQ2 — Embedded server lifecycle: in-process vs spawned; linger-on-quit *(blocks T4 final behavior; T2 proceeds either way)* **Question:** (a) does the embedded server run in-process (Wails-only option) or as spawned child processes; (b) does the stack outlive the app on quit? @@ -689,7 +689,7 @@ process. If Matt prefers die-with-app, only the T4 quit handler and the always (never in-process), linger-by-default with an explicit "Quit and stop stack". Parameter of the supervisor decision (DL-108), not a separate ledger row. -### OQ3 — Native-client token acquisition and storage *(blocks T5)* +### OQ3 — Native-client token acquisition and storage *(blocks T5)* **Question:** how does the remote client obtain and store its bearer? @@ -717,7 +717,7 @@ the shell over IPC for keychain write + header injection; the UI-side 0600-file fallback on keyring-less hosts. Covered by DL-109 (credentials keychain-first, never config/argv). -### OQ4 — Embedded database: what does "ensure Postgres" mean in an installed app? *(blocks T2 `ensure database` + T4 preflight)* +### OQ4 — Embedded database: what does "ensure Postgres" mean in an installed app? *(blocks T2 `ensure database` + T4 preflight)* **Question:** the server refuses to start without a Postgres DSN (`compass-server/main.go:108-109`; store is pgx-only, @@ -745,7 +745,7 @@ field either way, so the choice does not reshape the design. Postgres in the app state dir (host-Postgres acceptable only as a beta stopgap). Folded into the supervisor decision (DL-108, the "ensure database" step). -### OQ5 — uid-1000 preflight vs fix *(sub-fork found in source; blocks T4 preflight copy only)* +### OQ5 — uid-1000 preflight vs fix *(sub-fork found in source; blocks T4 preflight copy only)* **Question:** the runner refuses any uid but 1000 (`verifyRunnerUID`, `compass-runner/main.go:178-188`) because the agent image bakes uid 1000 and @@ -765,7 +765,7 @@ is runner/image scope, not shell scope, and must not ride this record. Dogfood; arbitrary-uid support filed as a named GA-blocking follow-up issue (runner/image scope, not this record). No ledger row — inherited constraint. -### OQ6 — Agent-image distribution for embedded mode *(blocks T4; reshapes T6)* +### OQ6 — Agent-image distribution for embedded mode *(blocks T4; reshapes T6)* **Question:** the runner refuses to boot without an agent container image (`compass-runner/main.go:111-114`) and every agent session runs inside one — but @@ -794,7 +794,7 @@ a legible error when the pull is unavailable offline. New ledger row DL-112 (cross-lane: compass-runner owns the image build + publish, T6 owns the pull step + packaging). -### OQ7 — Embedded caller-identity mechanism *(blocks T4; cross-lane compass-server)* +### OQ7 — Embedded caller-identity mechanism *(blocks T4; cross-lane compass-server)* **Question:** the UI's `Connection` needs a caller account id, but account ids are 128 random bits minted per database and cannot be guessed diff --git a/moon.yml b/moon.yml index e37388d79..552381fac 100644 --- a/moon.yml +++ b/moon.yml @@ -65,14 +65,15 @@ tasks: options: runInCI: false - # markdownlint-cli2 reads /.markdownlint.json (rules) + /.markdownlint-cli2.jsonc - # (globs, gitignore). + # rumdl reads /.rumdl.toml (rules + exclude). It prunes `exclude` before + # reading any file, so the policy lives entirely in that config — no glob + # operands, no --config chain. markdownlint: - command: 'markdownlint-cli2' - inputs: ['**/*.md', '!**/node_modules/**', '/.markdownlint.json', '/.markdownlint-cli2.jsonc'] + command: 'rumdl check .' + inputs: ['**/*.md', '!**/node_modules/**', '/.rumdl.toml'] options: - # Never cache the whole-repo gate. Runs the nixpkgs `markdownlint-cli2` on - # PATH (provided by devenv), not `bunx`. + # Never cache the whole-repo gate. Runs the nixpkgs `rumdl` on PATH + # (provided by devenv), not `bunx`. cache: false ci: diff --git a/tools/renovate/config.json5 b/tools/renovate/config.json5 index 47eede704..a6b7cc691 100644 --- a/tools/renovate/config.json5 +++ b/tools/renovate/config.json5 @@ -147,7 +147,7 @@ }, { // ── devenv nixpkgs channel ── - // The dev shell bakes biome + markdownlint-cli2 from nixpkgs + // The dev shell bakes biome + rumdl from nixpkgs // (devenv.nix:75-81), whose versions are governed by devenv's nixpkgs channel // (devenv.yaml → github:cachix/devenv-nixpkgs/rolling), locked by git rev in // devenv.lock. The native `nix` manager can't see it — there is no root @@ -158,7 +158,7 @@ // Renovate tracks it; the coupled postUpgradeTasks rule below re-locks // devenv.lock and rewrites the biome catalog pin to the version the new rev // bakes, so parity holds in one PR. (Only biome is dual-sourced here — - // markdownlint-cli2 has no catalog pin — so the relock rewrites one catalog + // rumdl has no catalog pin — so the relock rewrites one catalog // pin, not two.) // // Anchor: `"repo": "devenv-nixpkgs"` immediately followed by `"rev"`. @@ -447,7 +447,7 @@ // fences the OSV security path for biome: a biome CVE is remediated only when // the channel bakes a fixed version, not by an immediate security PR — an // acceptable tradeoff for a build-time devDependency with no runtime attack - // surface. (markdownlint-cli2 is baked from the channel too but has no catalog + // surface. (rumdl is baked from the channel too but has no catalog // pin, so no generic manager reaches it — no rule needed.) Remove only if // biome ever stops being baked from the channel. matchManagers: ["bun", "npm", "custom.regex"], diff --git a/tools/renovate/refresh-devenv-nixpkgs.core.ts b/tools/renovate/refresh-devenv-nixpkgs.core.ts index 05672955e..8e3d20e9a 100644 --- a/tools/renovate/refresh-devenv-nixpkgs.core.ts +++ b/tools/renovate/refresh-devenv-nixpkgs.core.ts @@ -9,8 +9,8 @@ // The catalog key whose pin mirrors the baked biome linter (package.json // workspaces.catalog). Exact-version pin today; the dev-shell parity story // keeps it string-equal to the biome baked from the devenv-nixpkgs channel -// (devenv.nix). Compass bakes markdownlint-cli2 from the same channel, but it -// carries no catalog pin (package.json has no markdownlint-cli2 entry), so the +// (devenv.nix). Compass bakes rumdl from the same channel, but it +// carries no catalog pin (package.json has no rumdl entry), so the // relock rewrites this one pin only. export const BIOME_CATALOG_KEY = "@biomejs/biome"; diff --git a/tools/renovate/refresh-devenv-nixpkgs.test.ts b/tools/renovate/refresh-devenv-nixpkgs.test.ts index e5952a1d5..2b57350a1 100644 --- a/tools/renovate/refresh-devenv-nixpkgs.test.ts +++ b/tools/renovate/refresh-devenv-nixpkgs.test.ts @@ -103,7 +103,7 @@ function devenvLock(outerRev: string, innerRev: string): string { // Minimal root package.json with a catalog block carrying the biome pin plus a // same-named `catalog:` CONSUMER ref that must survive untouched. Compass bakes -// markdownlint-cli2 from the same channel, but it carries NO catalog pin, so +// rumdl from the same channel, but it carries NO catalog pin, so // the fixture — like the real manifest — only pins biome. function packageJson(biome: string): string { return `${JSON.stringify( diff --git a/tools/renovate/refresh-devenv-nixpkgs.ts b/tools/renovate/refresh-devenv-nixpkgs.ts index b054f861c..6b6140122 100644 --- a/tools/renovate/refresh-devenv-nixpkgs.ts +++ b/tools/renovate/refresh-devenv-nixpkgs.ts @@ -1,7 +1,7 @@ // Renovate postUpgradeTask: lockstep the baked-biome catalog pin to a // devenv-nixpkgs channel bump (RIG-2432). // -// Context. The dev shell bakes biome + markdownlint-cli2 from nixpkgs +// Context. The dev shell bakes biome + rumdl from nixpkgs // (devenv.nix), whose versions are governed by devenv's nixpkgs channel // (devenv.yaml → github:cachix/devenv-nixpkgs/rolling), locked by git rev in // devenv.lock. The customManager in config.json5 surfaces that rev as a @@ -24,7 +24,7 @@ // so this is a pure fetch+eval (seconds, no build) for every channel rev // — the patch-independent path. // 4. Rewrite the biome catalog pin in the root package.json to the evaluated -// version (no-op when unchanged). Compass bakes markdownlint-cli2 from the +// version (no-op when unchanged). Compass bakes rumdl from the // same channel, but it carries no catalog pin, so only biome is rewritten. // 5. `bun install --lockfile-only` — re-resolve bun.lock so the fail-closed // `bun install --frozen-lockfile` root-check passes. From c2e4f94f307cbf48929b82deb1fea01d75d32dce Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 11 Sep 2026 14:42:44 -0400 Subject: [PATCH 2/2] fix(markdown): enable MD057, exempt the 11 ledger-delta records per-file (RIG-3663) Review fix (Main): the repo-wide MD057 disable was wrong. MD057 is a valuable rule (broken relative links) and is now ENABLED, exempted only for the 11 design records whose `## Ledger delta` sections quote a paste-ready DECISIONS.md row. Those rows' links are written relative to the ledger's own directory (`docs/designs/`), so they resolve from `DECISIONS.md` (which passes MD057) but not from the record's own directory, which is where rumdl resolves a relative link. The authored ledger copies are correct; the record-body quotes only appear broken from their own vantage. A `[per-file-ignores]` scope keeps MD057 live everywhere else and keeps every other rule live on these records. Controls: a broken link injected into a non-exempt file (README.md) still fires MD057; MD001 still fires inside an exempt record (exemption is MD057-only). Spec-impact: none. Refs RIG-3663 Co-authored-by: Matt Wilkinson --- .rumdl.toml | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/.rumdl.toml b/.rumdl.toml index 27e0b5143..4b71ff8b5 100644 --- a/.rumdl.toml +++ b/.rumdl.toml @@ -46,19 +46,37 @@ enabled = false [MD046] style = "fenced" -# ── Disabled rules ── -# Each is a rumdl-only rule markdownlint never had, verified false on compass's -# own files. Reasons recorded so a future reader need not re-derive them. - -# MD057 (relative-link existence): ALL 28 findings are the DECISIONS.md -# paste-ready-row convention — a design record quotes a `| DL-N | … | [record -# §x](compass-/design.md#…) |` row whose link is relative to the -# destination ledger (`docs/designs//DECISIONS.md`), not the record -# quoting it. Correct as written, only apparently broken when resolved from the -# record's own directory. Zero genuine broken links on this corpus, so the rule -# buys nothing here and misfires on a house convention across 11 records. -[MD057] -enabled = false +# ── MD057 (relative-link existence) is ENABLED: adopted with this swap ── +# A rule markdownlint never had. It stays on repo-wide to catch genuine broken +# relative links; the per-file exemptions below are its only carve-outs. +# +# Every current finding (28 across 11 records) is the same house convention: a +# design record's `## Ledger delta` section quotes its paste-ready +# `DECISIONS.md` row, whose link is written relative to the ledger's directory +# (`docs/designs/`) — e.g. `[board view §Approach](ui/compass-board-view/…)`. +# That link resolves correctly from `DECISIONS.md` (which lives in +# `docs/designs/` and passes MD057), but rumdl resolves a relative link from the +# RECORD's own directory, where the ledger-relative path does not exist. The +# domain-prefixed and bare forms both misfire this way — it is the resolution +# root, not a missing segment. So these are false positives from the record's +# vantage, not broken links: the authored ledger copies resolve. +# +# `[per-file-ignores]` scopes the MD057 exemption to exactly those records, +# leaving MD057 live on every other file (and every OTHER rule live on these). +# These are frozen/Active records; the Ledger-delta rows are append-only, so a +# per-file exemption is the right tool rather than editing the rows. +[per-file-ignores] +"docs/designs/agent/compass-agent-trees/design.md" = ["MD057"] +"docs/designs/infra/ci/compass-ci-check-naming/design.md" = ["MD057"] +"docs/designs/server/compass-comms-multi-actor-test-coverage/design.md" = ["MD057"] +"docs/designs/server/compass-forge-write-path/design.md" = ["MD057"] +"docs/designs/server/compass-issue-model/design.md" = ["MD057"] +"docs/designs/server/compass-linear-agent-responder/design.md" = ["MD057"] +"docs/designs/server/compass-server-ownership-layer-amendment/design.md" = ["MD057"] +"docs/designs/ui/compass-board-view/design.md" = ["MD057"] +"docs/designs/ui/compass-native-packaging/design.md" = ["MD057"] +"docs/designs/ui/compass-sidebar-pins/design.md" = ["MD057"] +"docs/designs/ui/compass-sidebar-pins-unreachable-amendment/design.md" = ["MD057"] # False: flags a list item's own continuation line (indented prose wrapping one # bullet) as an unpadded list — the auto-fix inserts a blank line mid-sentence.