WORK-657
Setting up your dashboard 0 entities found · 4/8 branches scanned
ID:WORK-657Status:done

refrakt format re-indents nested block tags, with a check mode and editor formatting

Once WORK-645 allows indentation, SPEC-170 D5 sets the rule for when to indent: a block tag's content is indented only when it contains another block tag. A rule nobody can apply mechanically drifts, and no existing tool applies this one:

Priority:highComplexity:moderateMilestone:v0.42.0Source:SPEC-170

Criteria completion

Criteria completion: 9 of 10 (90%) checked; tracking started on Oct 24, no incremental history yet0%25%50%75%100%Oct 24Oct 28

Tracking started Oct 24 — check back for trends.

Acceptance Criteria

  • refrakt format [paths] re-indents .md files by SPEC-170 D5: one tab per level of block-tag nesting, applied only inside a block tag that contains another block tag; inline tags do not count
  • It changes only the leading whitespace of lines inside block tags; every other byte is unchanged, asserted on fixtures that include lists, tables, emphasis and long tag openings
  • A fenced code block keeps its internal relative indentation and is shifted as a whole
  • It writes a file only when the AST parsed with the shared parse is unchanged; otherwise it reports the file and line and leaves the file alone
  • Formatting is idempotent: a second run changes nothing, asserted on every fixture
  • refrakt format --check exits non-zero and names each file that would change; CI runs it beside npm run format:check (.github/workflows/format.yml)
  • The language server provides document formatting with the same function, so editors can format on save
  • Every file under plan/ is unchanged by a run, asserted once in the resolution
  • The MCP server exposes it, or the resolution records why not
  • The composed-runes guide and the content-authoring docs state the rule and the command

Approach

  • Use the tokenizer to find each block tag's opening and closing lines. Work out each line's nesting depth, and rewrite the leading whitespace only. Do not reprint from the AST.
  • Lines inside fences, and continuation lines of list items, need care. The AST-equality guard is what makes a mistake there harmless: a wrong rewrite is refused, not written.
  • The CI check starts with the paths the rule is enforced on (site/content/, the composed fixtures, plan/), and widens as WORK-658 migrates them.

Blocked by

  • WORK-645: the shared parse with allowIndentation.

References

  • SPEC-170: D3 (round trips), D5 (the rule).

Resolution

Completed: 2026-10-25

Branch: claude/v0-42-0-planning-t31f5e

What was done

  • packages/runes/src/format-indent.ts: formatIndentation lays out each block tag's body from the AST's tag and fence line ranges.
    • Owned lines: their common leading whitespace becomes one tab per holding tag.
    • Child fences and multi-line openings move with their first line.
    • Relative indentation is kept. Blank lines are left alone, except inside fences.
    • guardRewrite accepts only a rewrite with the same AST, parsed as the pipeline parses (escapeFenceTags, plus each normalised for templates).
  • packages/cli/src/commands/format.ts + bin.ts: refrakt format [paths] [--check] [--format json], exported as @refrakt-md/cli/format.js.
  • packages/language-server/src/providers/formatting.ts: documentFormattingProvider with per-line indentation edits.
  • packages/mcp/src/tools/core.ts: refrakt.format.
  • CI: root scripts format:md / format:md:check (site/content and the composed fixtures). The check runs in validate.yml.
  • Tests: runes/test/format-indent.test.ts, cli/test/format-command.test.ts, language-server/test/formatting.test.ts, mcp/test/format-tool.test.ts.
  • Docs: docs/cli/format.md (new), docs/content.md, docs/mcp/tools.md, composed-runes.md, CLAUDE.md.
  • Repository-wide (1,545 tracked .md): no refusals, idempotent.

Notes

  • plan/ is not unchanged, and is not covered (criterion left open by decision). Five specs (SPEC-004, 006, 008, 050, 051) hold nested block tags such as {% sandbox %}. D5 would indent their whole bodies, and GitHub would render them as code. The owner chose to keep plan/ out of the run and the CI check, rather than refine D5 or accept that.
  • CI location: the check runs in validate.yml, not beside Biome in format.yml as the criterion says. The formatter is the CLI and needs the build, and format.yml is deliberately build-free. A comment there points to it.
  • The guard fired once for real (SPEC-006, a whitespace-only line inside a fence). That led to fence-interior whitespace lines moving with the fence.