Normalize frontmatter field order in fix and create - #132
Merged
Conversation
Split out of _plans/032, which established the decisions this rests on. 032's surgical UpdateField is what removes ordering as a side effect of every write; this is the explicit replacement for the two commands that rewrite frontmatter wholesale anyway. Separate because it is a feature rather than a consequence of #130, it carries the only --json contract change across both plans, and bundling the two buried the part that fixes the bug.
Reorders a block into canonical field order, reporting whether anything moved. Nothing calls it yet. It is a no-op when the order already holds, which is what makes the reported boolean mean exactly "keys moved" -- and what keeps the blank-line handling predictable: blank lines are dropped only as a consequence of a real reorder, never as a side effect of some unrelated field being written. A canonical file keeps its blank lines; this normalizes ordering, it is not a formatter. Blank lines are dropped textually rather than structurally because a blank line is not a node: it lives in the preceding value's token origin, so reordering carries it to a position that means nothing. A text filter is only safe because reads refuse a multi-line scalar, so nothing this package emits spans more than one line and a blank line in the output is always a real one. isCanonical checks adjacent pairs, which equals global sortedness because the parser rejects duplicate keys. Comments travel with their key across a reorder, which is better than the old writer hoisting every comment to the top of the block -- a comment about page_id belongs next to page_id. Not free: a block-header comment written above a key other than title sinks with that key. Visible in the diff, and accepted.
032's surgical UpdateField no longer rewrites the whole block in canonical order as a side effect of writing one field. That is the right default for an edit -- these files live in git and a write should not churn lines nobody asked it to touch -- but stable ordering is still worth having, so the commands that already rewrite frontmatter wholesale do it explicitly. A file whose values all match its live page but whose fields are jumbled is changed, not consistent. Reporting it consistent would mean running fix, being told there is nothing to do, and still having a jumbled file. It also keeps --dry-run a faithful preview. Reported as a dedicated boolean rather than an entry in changes[]: changes[].field is an actual frontmatter key everywhere else, and a non-field there makes the slot polymorphic for any consumer grouping by it. The existing "(none)" sentinel lives in old, which is a display slot; field is an identity slot. Normalize runs last in the write path, so a key inserted above lands canonically rather than wherever the surgical insert put it. Computing reordered on pre-change content is stable for two reasons, not one: a surgical UpdateField never moves an existing key, and inserting before the first key that sorts after it cannot flip canonicity in either direction. The cost, stated plainly: fix now touches files it previously left alone, so the first run after this lands produces a diff across the tree.
The minimal-diff argument behind a surgical UpdateField does not apply here: persist rewrites all five fields by definition, so there is no untouched line left to protect. It also means the frontmatter markfluence authors is always canonical, rather than "canonical unless it came from a jumbled file and you have not run fix yet". No result field: create writes every field, so its output is always canonical and there is nothing to report.
README's fix section promised it "writes a file only when a field actually changed", which order normalization makes false. That sentence described behaviour rather than promising anything, and it is not in docs/guarantees.md where the load-bearing promises live.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #131, split out of
_plans/032into_plans/033. Makesfixandcreate --persistleave a file's frontmatter in canonical field order, reported as a newreorderedboolean.Why this is a separate change
#131 made
UpdateFieldsurgical — an existing key keeps its position and only its value node is replaced — where the old writer rewrote the whole block in canonical order on every single write.That is the right default for an edit: these files live in git, and a write should not churn lines nobody asked it to touch. But it means ordering is no longer normalized as a side effect, and stable ordering is worth having, so the two commands that already rewrite frontmatter wholesale now do it explicitly.
Kept out of #131 because it is a feature rather than a consequence of #130, and because it carries the only
--jsoncontract change across both branches — #131 left--jsonoutput byte-identical to main.Behaviour
A reorder-only file is
changed, notconsistent. A file whose values all match its live page but whose fields are jumbled is rewritten and reported. Otherwise you runfix, are told there is nothing to do, and still have a jumbled file. It also keeps--dry-runa faithful preview, which this codebase protects elsewhere.The cost, stated plainly:
fixnow touches files it previously left alone, so the first run after this lands produces a diff across the tree.README.md's "writes a file only when a field actually changed" becomes false and is rewritten — it described behaviour rather than promising anything, and it is not indocs/guarantees.mdwhere the load-bearing promises live.create --persistnormalizes too. The minimal-diff argument does not apply there: persist rewrites all five fields by definition, so there is no untouched line left to protect. It also means the frontmatter markfluence authors is always canonical, rather than "canonical unless it came from a jumbled file and you have not runfixyet".No flag. A tidy-ordering feature you have to opt into leaves the files untidy.
--jsonfixResultgainsreordered: boolean(propertiesandrequired, sinceadditionalProperties: falseneeds both). This is a dedicated field rather than a pseudo-entry inchanges[]:changes[].fieldis an actual frontmatter key name everywhere else, built from real fields, and putting a non-field there makes the slot polymorphic — a consumer doingchanges | map(.field)gets a phantom key it has to know to filter. The existing"(none)"sentinel lives inold, which is explicitly a display slot (oldDisplayin the struct);fieldis an identity slot.No equivalent on
createResult:createwrites every field, so its output is always canonical and there is nothing to report.Normalizeis a no-op when the order already holdsThis is what makes the boolean mean exactly "keys moved", and what keeps the blank-line handling predictable — blank lines are dropped only as a consequence of a real reorder, never as a side effect of some unrelated field being written. A canonical file keeps its blank lines: this normalizes ordering, it is not a formatter.
Blank lines are dropped textually rather than structurally, because a blank line is not a node — it lives in the preceding value's token origin, so reordering carries it to a position that means nothing. A text filter is only safe because #131 refuses a multi-line scalar, so nothing the package emits spans more than one line and a blank line in the output is always a real one.
isCanonicalchecks adjacent pairs, which equals global sortedness because the parser rejects duplicate keys.Comments travel with their key
Better than the old writer hoisting every comment to the top of the block: a comment about
page_idbelongs next topage_id. Not claimed to be free — a block-header comment written above a key other thantitlesinks with that key. Visible in the diff, and accepted.Ordering within
fixNormalizeruns last in the write path, so a key inserted above lands canonically rather than wherever the surgical insert put it.reorderedis computed on pre-change content, which is stable for two reasons rather than one: a surgicalUpdateFieldnever moves an existing key, and inserting before the first key that sorts after it cannot flip canonicity in either direction — an existing inversion survives the insert, and a canonical sequence stays canonical.Not in this PR
checkreporting a jumbled file (ordering is not a publishability defect, andcheckis about what would fail a publish),updatenormalizing (it never writes back to files, and that stays true), and normalizing anything else about the block — intra-line whitespace, blank lines in a canonical file, comment placement.fixorders fields; it is notgofmtfor frontmatter.Testing
make checkpasses and every commit is independently green. Pinned: a canonical block with a blank line is a no-op (the case that distinguishes "ordering" from "formatting"); a comment stays attached to its key across a reorder; a reorder-only file ischangedwithreordered: trueand emptychanges;--dry-runreports it without writing; a canonical consistent file still reportsconsistentand is not written;create --persistoutput is canonical from jumbled input; and schema conformance through the command's ownjsonResult().214 lines of Go, 99 of them tests.