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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -647,7 +647,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')
Expand Down
24 changes: 0 additions & 24 deletions .markdownlint-cli2.jsonc

This file was deleted.

3 changes: 0 additions & 3 deletions .markdownlint.json

This file was deleted.

106 changes: 106 additions & 0 deletions .rumdl.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# 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 `<compass-manager>` 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"

# ── 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.
# 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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/eng-docs/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ tasks:
inputs:
- 'scripts/gather.ts'
- '/**/*.md'
- '/.markdownlint-cli2.jsonc'
- '/.rumdl.toml'
outputs: ['src/content/docs', 'src/sidebar.generated.ts']

build:
Expand Down
49 changes: 21 additions & 28 deletions apps/eng-docs/scripts/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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([
{
Expand All @@ -247,7 +243,7 @@ test("changedDocPages slugifies a dotted directory segment", () => {
status: "modified",
},
],
markdownlintConfig,
exclude,
),
).toEqual([
{
Expand All @@ -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" }]);
});

Expand All @@ -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([]);
});
Expand All @@ -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([]);
});
Expand All @@ -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", () => {
Expand All @@ -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([]);
});
Expand All @@ -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([
{
Expand All @@ -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([]);
});
Expand All @@ -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([
{
Expand All @@ -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([]);
});
Expand All @@ -416,7 +409,7 @@ for (const dir of [
expect(
changedDocPages(
[{ filename: `.${dir}/notes.md`, status: "added" }],
markdownlintConfig,
exclude,
),
).toEqual([]);
});
Expand Down
27 changes: 13 additions & 14 deletions apps/eng-docs/scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -414,7 +413,7 @@ async function main(): Promise<void> {
* 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.
*/
Expand All @@ -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(
Expand Down
Loading
Loading