| name | formatter |
| description | Grow Fatou's own Julia formatter, one construct at a time, against hand-authored fixtures. rules::lower (crates/fatou-formatter/src/formatter/rules.rs) lowers the CST into the layout IR (ir.rs) printed by printer.rs; the gate in crates/fatou-formatter/tests/formatter.rs diffs each fixture's format(input.jl) against a hand-written expected.jl. There is no external reference formatter: you propose a formatting, the user edits expected.jl to the desired form, you push back if it breaks a tenet or conflicts with an existing rule, then you implement the rule. Parser/lexer blockers stop and hand off to parser-parity. |
Use this skill when asked to advance Fatou's formatter or "take the next
construct." Read RECAP.md first for the latest session, the rule inventory,
and the traps.
The formatter in one paragraph
format(text) parses to the lossless rowan CST, lowers it via rules::lower
(crates/fatou-formatter/src/formatter/rules.rs) into the Wadler/Prettier Ir
(crates/fatou-formatter/src/formatter/ir.rs), and prints it with the best-fit
engine (crates/fatou-formatter/src/formatter/printer.rs). Fatou owns its style
— there is no external reference formatter (we used to track Runic.jl; that
target is gone). The gate is hand-authored fixtures:
crates/fatou-formatter/tests/fixtures/formatter/<slug>/ holds input.jl plus
a hand-written expected.jl, and crates/fatou-formatter/tests/formatter.rs
asserts format(input.jl) == expected.jl. Presence of expected.jl is gate
membership — a fixture with only input.jl is a construct still being
authored, not yet gated. No allowlist, no blocked list.
The two invariants (crates/fatou-formatter/tests/formatter.rs)
- Gate (
formatter_fixtures_match_expected): every fixture with an
expected.jl must format to it exactly.
- Stability (
formatter_is_idempotent_and_stable): over every
input.jl (gated or not), format(format(x)) == format(x) and the output
parses cleanly. This guards against mangling any curated input as rules land.
The authoring workflow (per construct)
This is a human-in-the-loop loop. You do not invent the canonical form
alone; you propose, the user decides, you implement.
-
Read RECAP.md (traps, latest session, rule inventory, ranked targets).
Prefer a user-named target. Baseline: cargo test --workspace is green.
-
Surface candidate inputs and propose a formatting. Pick a small set of
representative input.jl snippets for the construct. Inspect the CST
(cargo run -q -- parse <file>) and current output
(cargo run -q -- format < file). Propose the expected.jl you believe is
right under Tenet 1 (deterministic full reflow, input-independent), and
explain the reasoning. Hand it to the user.
-
The user hand-edits expected.jl to the form they want.
-
Push back when warranted. If the user's choice is unprincipled, breaks a
tenet (especially Tenet 1 determinism), or conflicts with an existing rule or
another fixture, raise friction: name the conflict, show the affected
rule/fixture, and resolve it together before writing code. Diverging from a
prior decision is allowed but must be conscious and recorded, never silent.
-
Implement the rule. Add a lower_<construct> arm (recipe below) in
rules.rs; touch ir.rs/printer.rs only if the layout genuinely needs a
new primitive. Preserve the bail-to-transparent discipline.
-
Lock it. Commit the agreed input.jl + expected.jl under
crates/fatou-formatter/tests/fixtures/formatter/<slug>/. The gate goes
green for that slug.
-
Guardrails:
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
Stability (idempotence + clean reparse) must hold.
-
Parser/lexer blocker => STOP and hand off. (See below.)
-
Update TODO.md (mark a formatter bullet) and RECAP.md. Commit
(Conventional Commits; subject <= 60 chars; feat(formatter) for a new
layout capability, test(formatter) for fixtures-only, for a
RECAP/handoff change). The pre-commit hook runs clippy + rustfmt — never
. Don't push unless asked.
The rule recipe
A construct touches just crates/fatou-formatter/src/formatter/rules.rs:
- Add a
match arm in lower_node for the node SyntaxKind, dispatching to a
lower_<construct> helper (split into a rules/<construct>.rs submodule
once the file grows — no mod.rs).
- Build the
Ir: Ir::text for tokens, Ir::concat to sequence,
Ir::Line/Ir::group/Ir::indent for breakable layout. Recurse into
operand/child nodes via lower_node so handled descendants keep normalizing.
- Bail to
lower_transparent on any shape you don't fully model
(interleaved comment/newline, error recovery, missing child). Never guess — a
verbatim passthrough is always idempotent and lossless.
Tenet 1 is the authority
Output is decided solely by the formatter's rules and the layout engine, never
by how the input was written. Semantically-equivalent inputs must format
identically: source line breaks, whitespace, operator spelling (in vs ∈,
a*b vs a * b), and numeric-literal form never influence the result. Fatou
fully reflows, laying out each construct from scratch under line_width,
breaking only where width or semantics require it. Push back against hard-coding
special cases for specific constructs. (See AGENTS.md.)
What this skill is NOT
- Not source-break mirroring. Author
expected.jl as the canonical
fully-reflowed form. Do not let the current code's output define
expected.jl. The width-driven reflow engine already exists and is what
decides every group's flat-vs-break (printer.rs); the old Runic-derived
source-break mirroring was retired rule by rule, and has_newline_token now
has a single live call site, in the comment-bearing matrix path — where
mirroring is arguably correct, since moving a trailing # note changes which
element it annotates. A new rule that reads whether the author broke a line
is a Tenet 1 violation (see RECAP for the audit).
- Not a pass-rate chase. Every handled construct stays idempotent and must
never mangle unhandled syntax — that's what the transparent fallback protects.
- Not semantic rewriting. Layout only; semantic rewrites (e.g. implicit
return insertion) are out of scope.
Parser/lexer blockers — stop and hand off
Formatter work routinely surfaces gaps that aren't yours to fix — a construct
that won't tokenize or parse cleanly, leaving you only the transparent bail (you
see ERROR nodes, not the real shape). rules.rs is the only growth surface
here. Stop, keep the broken shape out of the fixture (use a parser-safe
variant), and record the gap where the fixer will look:
- a "Queued next target" note at the top of
.agents/skills/parser-parity/RECAP.md, and/or a bullet under TODO.md's
Parser section;
- include the JuliaSyntax ground truth
(
julia --startup-file=no -e 'using JuliaSyntax; print(JuliaSyntax.parse(Expr, "CODE"))'),
what Fatou does instead, and the crux;
- cross-reference it from this skill's
RECAP.md and from the fixture that had
to route around it.
Key files
crates/fatou-formatter/src/formatter/rules.rs:
lower/lower_node/lower_transparent, the per-construct rules. The growth
surface.
crates/fatou-formatter/src/formatter/ir.rs: the Ir primitives
(Text/Concat/Line/SoftLine/
HardLine/BlankLine/Indent/Group/IfBreak, plus HugGroup for a
trailing-argument hug with an explode fallback and CondGroup for a choice
decided by a later re-indented line, as in f(args) where {T, S}).
crates/fatou-formatter/src/formatter/printer.rs: best-fit layout engine
(group flat-vs-break).
crates/fatou-formatter/src/formatter/core.rs: format/format_with_style
entry points.
crates/fatou-formatter/tests/formatter.rs: the gate + stability invariants.
Traps
expected.jl is hand-authored — never capture it from any formatter,
including Fatou's current output. Author it to the canonical full-reflow form.
format <file> rewrites in place. Pipe via stdin
(cargo run -q -- format < file) to inspect output without touching a
fixture.
- Always bail to transparent on an unmodeled shape. Stability (idempotence +
clean reparse) and losslessness of unhandled syntax depend on it.
- A parser/lexer gap is not yours to patch in
rules.rs — hand it off.
Session boundaries
A committed construct with RECAP.md updated is a clean stop — RECAP.md is
the handoff. One construct per context window is the intended cadence.
Report-back format
- Construct landed (e.g. "operator spacing").
- Gate: fixtures passing before -> after (regressions must be zero).
- Files changed.
- Any parser/lexer blocker surfaced and where it was handed off
(parser-parity RECAP /
TODO.md). "None" if clean.
- Ranked next target. If ending uncommitted or with regressions, say so and
list the red tests.