Fix#
Why katalyst fix rewrites
frontmatter the opinionated way it does. The parser and encoder live in
internal/codec/markdownbodytext; the transform that drives the canonical
form, and the backend write that persists it, live in internal/fix and
internal/storage/collection/filesystem respectively.
Terms#
| Term | Meaning |
|---|---|
| Fix | A command that rewrites existing content into Katalyst’s canonical form when a check can supply a safe transformation. |
| Canonical form | The deterministic output format fix writes: preserved frontmatter syntax, sorted top-level keys, native encoder style, preserved body bytes, and one trailing newline. |
| Report-only check | A check that can report violations but cannot safely rewrite content. |
| Check mode | The --check form of fix: print what would change, write nothing, and exit 1 if any item is non-canonical. |
Design rationale#
Fix is deliberately opinionated.
katalyst fix rewrites frontmatter in one canonical form in the file’s own
format: TOML stays TOML, JSON stays JSON, YAML stays YAML. fix never
converts between formats. Canonically, that means:
- the source format is preserved (same fence, same syntax),
- top-level keys sorted alphabetically,
- each format’s default block/indent style: yaml.v3 block style, the
go-tomldefault, two-space-indented JSON, - exactly one trailing newline,
- body bytes preserved verbatim.
Because the canonical scalar styling is each library’s default, a round-trip is
meaning-preserving rather than byte-identical: e.g. a double-quoted TOML
string re-emits single-quoted. Re-parsing the output always yields the same
Meta.
There are no style flags. gofmt, black, and rustfmt taught the same
lesson: a formatter’s value comes from there being one obvious answer.
Configurability just re-creates the bikeshed. Users who want a different style
simply don’t run fix. Because the body is preserved byte-for-byte, fix is
safe to run across an entire repo without touching prose.
Trade-off: comments inside the frontmatter block are not preserved. That is by design (frontmatter is structured data, not prose) and will be revisited only if it hurts in practice.
--check makes fix non-destructive: it writes nothing, prints the items that
would change, and exits 1. That is the CI form.
Fix never injects missing values.
An earlier idea had a mode that would add “sentinel” placeholder values for
missing required keys. It was dropped, and the safe-mutation story moved to a
later, opt-in command (working name patch).
Silently injecting placeholder values is hostile: it can mask real problems,
create merge conflicts, and produce documents that pass schema validation
while being semantically wrong. Katalyst would rather ship nothing than ship
that. A safer design, interactive or constrained to filling a schema’s declared
default:, deserves its own command and explicit per-field opt-in. Until then,
fix only ever normalizes what is already there; it never creates structure (a
frontmatter-less file is returned untouched).
Worked example#
fix rewrites frontmatter into a canonical form (here, sorting the keys) while leaving the markdown body byte-for-byte unchanged. It is idempotent and never injects missing keys.
Input#
notes/doc.md
---
zebra: 1
apple: 2
---
# Body
verbatim.katalyst/bases/my_directory.yaml
type: filesystem
root: .
collections:
notes:
path: notes
checks:
- kind: markdown_requires_h1Command#
$ katalyst fix notes/doc
<project>/notes/doc.md
Result#
notes/doc.md after katalyst fix notes/doc:
---
apple: 2
zebra: 1
---
# Body
verbatimLifecycle of fix#
For each item:
- Read bytes.
- Parse to
Document. - If no frontmatter, return verbatim,
fixnever invents structure. - Marshal
Metawith top-level keys sorted alphabetically, inDocument.Format’s native syntax and default style. - Re-assemble in the same format:
---\n<yaml>\n---\n<body>,+++\n<toml>\n+++\n<body>, or{...}\n<body>for JSON. Body bytes are preserved verbatim; one trailing newline is enforced on the file. - Compare against the original. If unchanged, do nothing. Otherwise atomically
rewrite (temp file + rename), or, with
--check, print the path and accumulate exit-1 status.
Invariants#
- Body bytes are sacred. No command except
fixmodifies them. Evenfixonly normalizes trailing whitespace and the leading separator; interior body bytes round-trip exactly. - Format is preserved.
fixre-emits each file in its own frontmatter syntax and never converts between YAML, TOML, and JSON. - No semantic values are invented.
fixonly normalizes existing frontmatter and configured text fixes; it does not create missing metadata.
See also#
- Markdown body text
for how markdown documents parse before
fixrewrites them. go doc ./internal/fixfor the code-level transform contract.