02 · Reviewable change
Will agent edits stay clean, or reformat my whole file?
A one-line change is a one-line diff, every time. Glyph has a single canonical layout, so an agent can't bury a real change under a wall of reformatting.
Why it matters
When an agent opens a pull request, the diff is the review. If a genuine one-line fix arrives as a 200-line reformat, nobody can see what actually changed, and that noise is exactly where a bug slips through. Diff stability means the diff shows the change and nothing but the change.
Two rules do most of the work. Here's each, shown as the diff you'd review.
Example 1: trailing commas, one item per line
Add a route. In TypeScript's common style the previous last line has to churn just to grow a comma. Two lines move for a one-item change:
const routes = [ "/home", - "/about" + "/about", + "/billing" ]
let routes = [
"/home",
"/about",
+ "/billing",
]
Trailing commas are required, and a list that doesn't fit on one line goes one element per line rather than being repacked, so inserting, removing, or reordering an entry never disturbs its neighbors. The same holds for record fields, parameters, match arms, and imports. The threshold has a cost: a rename that pushes a call past 100 columns expands the whole argument list to one per line.
Example 2: one canonical layout
There is exactly one correct formatting, produced by glyph fmt. Whitespace,
wrapping, and alignment aren't choices an agent can “tidy,” so a semantic
edit can't drag reflow along with it. Change one field's type, and that's the whole diff:
type User = {
id: string,
- age: string,
+ age: number,
email: string,
}
Because the layout is fixed, two different agents editing the same file produce
byte-identical formatting, no reflow churn, no “my
formatter vs. yours” noise, and a clean git blame that points at the
commit that actually changed a line, not the one that reformatted it.
One canonical layout, so does the formatter move my comments?
It used to, and that was a bug worth naming. Glyph has no block comments and no
doc-comment syntax, so a // line is the only way to explain a record field,
a union variant, or a match arm. The formatter flushed pending comments only
at declaration and statement boundaries, which meant a comment written inside
a construct drifted down to the next declaration and read as documentation for whatever
it landed on. Writing Minesweeper in Glyph is what surfaced it. These two declarations come
from the
real app (comments trimmed to fit), run through the old formatter and the new one.
type Visibility = | Hidden | Flagged | Revealed // Face down. Prints as `.`. // Face down and marked by the player. // Face up. `Cell.adjacent` is what gets printed. type Cell = { mine: bool, adjacent: int, visibility: Visibility } // Number of mined neighbours. Only meaningful // once mines are placed.
type Visibility = // Face down. Prints as `.`. | Hidden // Face down and marked by the player. | Flagged // Face up. `Cell.adjacent` is what gets printed. | Revealed type Cell = { mine: bool, // Number of mined neighbours. Only meaningful // once mines are placed. adjacent: int, visibility: Visibility, }
The rule now: a comment is re-emitted above the item it was written above, inside a
record body, a union variant list, an array or object literal, an argument or parameter
list, and a match. And a construct holding an interior comment always takes
the one-element-per-line form, at any size, so the comment has an item to sit above. So
type Shape = { w: int, h: int } still collapses to one line, and the moment
you document h the record stays expanded.
Why this counts as a correctness bug and not a cosmetic one: nothing failed. The build
exited 0, tsc passed, and the mangled output was itself a fixed point, so
glyph fmt --check in CI went green on a file whose comments now said
something false. That is the same class of failure Glyph exists to remove, just in the
formatter rather than the type system.
Can glyph fmt break a program that builds?
It could, in exactly one shape, and that is fixed. An arm meaning “the empty
record” is written true => ({}). Arm-body position is the only place
in the grammar where a leading { is ambiguous, and the parser settles it by
requiring key: or ... right after the brace. An empty object has
neither, so when the formatter dropped the parentheses and reprinted
true => {}, the arm came back as an empty block and the file
stopped building: E0223, the arm produces no value. You ran a formatter on
working code and got code that no longer compiled.
The printer now parenthesizes an arm body whose leftmost token is a { that
would reparse as a block, which is the empty object and anything built on it
(({}).size comes back as ({}.size)). An arm body that already
reads unambiguously, like false => { a: "x" }, stays bare, so there is no
new punctuation to read anywhere else. fmt is a fixed point on the result and
the emitted TypeScript is unchanged. A formatter your program has to survive is worse than
a missing feature, because it damages code that already worked.
Where it stands
Shipping today
Required trailing commas, one-item-per-line lists, and glyph fmt's single canonical layout, round-tripping and idempotent, with format-on-save in the editor. Comments are kept above the item they were written above, including inside a record, a literal, an argument list, and a match.
The edge, and what's next
A comment is always emitted on its own line, so one written at the end of a code line (w: int, // width in cells) moves to the line above the next item. It no longer crosses a declaration boundary, but it does move; keeping it trailing needs the printer to track which line a comment shares. The printer also has no rule for breaking a long method or + chain yet, so an overlong chain breaks the innermost argument list instead, which is the wrong place. We're also building a diff-size benchmark against the same edits in TypeScript, so this pillar is a number you can check.