Single source of truth, no width/tabs/brace-style configuration.
Context
Most modern languages that ship a first-party formatter (gofmt, rustfmt with default config, dotnet format, csharpier) converged on the same answer to the bikeshed question of formatter style: pick one and don't expose knobs. Languages that allow per-project style files (older prettier configs, clang-format) routinely waste cycles in code review on layout disagreements and create friction for cross-project contributions.
The Beskid formatter starts from a clean slate. The reference implementation in compiler/crates/beskid_analysis/src/format/ is already opinionated (four-space indent, brace on same line, one blank line between members) and ships through a single CLI surface (beskid format / beskid fmt). The question this ADR resolves is whether to expose layout configuration to consumers.
Decision
The Beskid formatter must be a canonical, knob-free pretty-printer. The platform ships exactly one layout style. The formatter:
Must not expose user-configurable width, indent unit, brace style, member ordering, blank-line policy, or comment placement.
Must not read project-level style files (no .beskid-format.toml, no [format] section in Project.proj).
Must be a pure function of the parsed AST and the formatter version compiled into the CLI.
May change formatter output between releases under the rules in the parent hub's Compatibility and versioning section (release notes + idempotency preserved + ADR when the layout policy changes substantively).
The CLI must continue to support the operational flags --write, --check, and --output; these affect where the output goes, not what it looks like.
Consequences
Pros:
Zero bikeshedding in code review: there is exactly one correct way for any given input.
Deterministic CI:beskid format --check is a reliable PR gate; no per-repo style file ambiguity.
Simpler editor integrations: every plugin can shell out to the canonical CLI without re-implementing layout logic.
Faster onboarding: new Beskid contributors don't need to learn project-specific style files.
Cons / accepted trade-offs:
Teams with strong existing house style (especially "tabs" or "2-space") cannot satisfy that preference via the formatter; this is by design.
Future syntactic features that might benefit from style-aware emission (for example, partial-application chains) must take a layout decision once and ship it as part of the formatter, not defer to user preference.
Some downstream tools that wrap the formatter and want to expose style flags must either drop those flags or layer them outside the canonical formatter.
Verification anchors
beskid_analysis::format::policy — single file that owns layout policy; no other formatter file emits \n or indent runs directly.
beskid_cli::commands::format::FormatArgs — the clap surface contains only --write, --check, --output; the absence of style flags is observable from beskid format --help.
Parent hub Layout policy section — enumerates the fixed indent unit, blank-line rules, and brace style.
Two anti-patterns would make this hard to evolve safely:
Scattered layout policy — per-construct emitters writing raw \n or four-space runs make a policy change (for example, "double blank between top-level items") a multi-file refactor.
God-trait Emit — putting blank-line policy inside Emit::emit parameters would force the AST-side modules to know about higher-level layout intent.
This ADR records the chosen split.
Decision
The formatter must preserve the following module decomposition:
Emit trait in format/emit.rs defines a single method emit(&self, w, cx). Per-construct modules must implement Emit for the AST node they own.
EmitCtx in format/emit.rs is the only sanctioned place to mutate indent depth, write newlines, write indents, or open/close braces. AST-side modules must call its helpers (write_indent, nl, ln, space, token, open_brace, close_brace) rather than writing raw layout characters.
policy.rs is the only place that owns inter-declaration, inter-member, and inter-statement blank-line decisions. AST-side modules call cx.between_top_level_declarations(...), cx.between_members(...), and cx.between_block_items(...); they do not duplicate the policy.
mod.rs stays as exports-only. New construct emitters are added as siblings under items/ (or as new files when the construct does not fit an existing role).
A change that violates rule (2) or (3) must be revised before merging; the parent Verification and traceability article enumerates the file anchors.
Consequences
Pros:
A new layout rule lands as one edit in policy.rs and an optional method on EmitCtx; downstream emitters don't need to know.
The formatter is easier to audit: anyone reviewing layout output knows to read policy.rs first.
The per-construct emitters stay readable — each function reads as "what does this node look like in source", not "how do I implement blank-line policy".
Cons / accepted trade-offs:
AST-side modules cannot work around policy by writing raw characters; this is intentional and is enforced by code review.
EmitCtx accrues helpers over time. We accept this as the lesser evil compared to scattered policy.
Verification anchors
compiler/crates/beskid_analysis/src/format/emit.rs — single owner of Emit, EmitCtx, Emitter, EmitError, and format_program.
compiler/crates/beskid_analysis/src/format/policy.rs — single owner of the three policy helpers.
compiler/crates/beskid_analysis/src/format/mod.rs — mod declarations and pub use re-exports only; no business logic.
This article enumerates the concrete artifacts that demonstrate formatter conformance — test paths, CI commands, code anchors — so that anyone modifying beskid_analysis::format or beskid_cli::commands::format can verify they have not regressed the parent hub contract.
It complements the design model which explains how the formatter is built; this article focuses on how it is verified.
The following commands must pass before any change to the formatter is merged:
Crate compile + unit tests:
Terminal window
cdcompiler && cargotest-pbeskid_analysisformat::
CLI compile + filter-named tests:
Terminal window
cdcompiler && cargotest-pbeskid_cliformat
The filter currently matches no test names, so this acts as a compile gate; future format-specific CLI tests must match this filter so they run automatically.
Strict platform-spec verification (covers this hub + the parent feature):
This must be enforced by at least one test in beskid_analysis that feeds a curated representative source corpus (functions, types, attributes, tests, control flow, generics) and asserts the equality after two formatter passes.
Any change to format/policy.rs or format/emit.rs::EmitCtxmust be accompanied by a test run that demonstrates the idempotency property still holds after fixtures are regenerated.
PR drift check — the Beskid CI pipeline must include a beskid format --check step against the workspace source tree. A drift hit must fail the PR.
Spec verification — the verify:trudoc -- --preset ci step covers PSC003 (Standard feature hubs require ## Decisions) and PSC006 (ADR pages require ## Context / ## Decision). This hub satisfies both via the embedded Decisions section and the ADR bundle under adr/.