--- numbering: title: true --- # Header Formatting `arch format` normalizes existing comment-header blocks through an immutable `HeaderSpec` and the same file-plan and atomic-application boundary used by `arch idx`. It is deliberately a formatter for headers that already match a configured shape. It does not decide that arbitrary comments should become headers. ## `1` Quick start Header formatting and document indexing have separate selectors. The built-in defaults choose `markdown` for Markdown and `source` for supported code files, while Typst, reStructuredText, and unknown families remain off. Headerization defaults to `flex`; set `header_mode: strict` when a repository intentionally wants profile-driven normalization. Override only the paths that need a custom profile or no header formatting: ```yaml header_mode: flex header_defaults: markdown: markdown source: source typst: off rst: off unknown: off headers: release-code: levels: - mark: '#' margins: [0, 0, 0, 0] width: content continuous: false index_policy: required formatters: [title] idx_spec: styles: [number] separator: '.' marker: '.' header_paths: - glob: 'src/release/**' profile: release-code - path: 'README.md' profile: off specs: formal-markdown: {styles: [number], separator: '.', wrapper: '`'} indexing: default: off paths: - glob: 'policies/**' spec: formal-markdown insertion: all ``` The `header_paths` rule above cannot enroll indexing, and the `indexing.paths` rule cannot select a header profile. This separation is intentional even when two policies happen to use similar names. Then choose the ordinary formatter workflow: ```console arch format --check src docs # exit 1 if any configured block would change; never write arch format --diff src docs # print unified patches; never write arch format src docs # apply content-hash-guarded plans atomically arch format --spec release src arch format --mode strict src # use the profile as an enforced template ``` The default `format` mode writes. `--check` and `--diff` return a finding status when a plan has edits. Standard input is supported using `-`. In write mode, it produces the formatted text on standard output and cannot be mixed with filesystem paths. `--stdin-filename` selects comment syntax and path policy for that input. ## `2` Flex and strict modes In default `flex` mode, an existing index is evidence for semantic hierarchy depth, while the rendered header is evidence for presentation. An indexed header maps its recognized presentation to a depth: matching unindexed headers inherit that depth, and a required sibling adopts the single presentation already established there when receiving its index. Headers that need no index edit remain byte-stable, including authored title case and geometry. Flex never borrows presentation from another file. An index-free file is not rewritten merely because it contains comments. Only blocks already recognized as normalized headers are eligible, such as `### BODY ###` or an indented `# Primary Methods` under the source profile. A profile's `forbidden` policy remains authoritative, so the built-in top source banners and Markdown H1 do not acquire indexes; this is structural policy rather than a hard-coded level-number exception. Malformed or ambiguous candidates remain unchanged. `strict` mode is the previous behavior. It renders every recognized block through the selected profile, including configured case transformations and ruler widths. Choose it in configuration with `header_mode: strict` or for one invocation with `arch format --mode strict`. ## `3` The model `HeaderProfile` is an ordered tuple of immutable `HeaderSpec` values. It selects the matching level by zero-based depth. If a document is deeper than the profile, the final configured level is reused. A `HeaderSpec` has the following stable configuration fields. | Field | Meaning | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `mark` | Punctuation used for rulers and framing. | | `margins` | `[left, top, right, bottom]`; one value applies to all sides and two values repeat. Horizontal `-1` means a dynamic ruler-side margin. | | `width` | `content`, `ruler` (100 columns), or a positive target width for ruler lines. | | `continuous` | Omit the normal space after the comment token. | | `idx_spec` | The document-index notation used if a header displays an index. | | `index_policy` | `optional`, `required`, or `forbidden`. | | `formatters` | Ordered title transformations: `capital`, `title`, `lower`, `upper`, `first`, `pascal`, `camel`, `kebab`, or `snake`. | | `tagged` / `color` | Persisted legacy-compatible display metadata. `idx_spec.wrapper` controls wrapped index and tag syntax. | The formatter recognizes whole configured blocks, not isolated ruler lines. The built-in Markdown profile preserves authored case, inline code, links, and URLs. H1 forbids an index, while H2+ accept dotted backtick-wrapped indexes such as `` `1.2` ``. The source profile remains independent and preserves the established code-header dialect. It understands the comment token for common hash (`#`), slash (`//`), dash (`--`), and semicolon (`;`) source-file families. Unsupported suffixes, binary inputs, undecodable input, symbolic links, and files that contain no configured header blocks are skipped rather than rewritten. A malformed index-like prefix is left unchanged instead of being guessed into a header. Each discovered block becomes an immutable `FilePlan`. The plan retains the source digest, original newline and BOM facts, diagnostic locations, and non-overlapping edits. Application rechecks the digest and writes through a same-directory temporary replacement, preserving the same safety properties as `arch idx` planning. Thus `--check`, `--diff`, JSON output, and a write all describe the same proposed change. ## `4` Required and forbidden indexes With `index_policy: required`, the planner derives structural indexes from the discovered header levels. With `forbidden`, renderers reject an explicit index and the planner never adds one. A leading index-like token is preserved as title content because its bytes are indistinguishable from a canonical numeric title. Formatting fails closed instead of deleting ambiguous text. With `optional`, flex keeps an existing index and presentation unchanged; strict canonicalizes the recognized block. The built-in Markdown profile uses `forbidden` at H1 and `optional` from H2 onward. Index enrollment and insertion remain the responsibility of the independent index planner. A required header whose derived address exceeds the configured `idx_spec.max_depth` yields `ARCH102` and is not changed. If one rendered presentation is indexed at multiple semantic depths, or multiple indexed presentations compete at the required header's semantic depth, flex yields `ARCH103` and leaves the file unchanged. `ARCH102` and `ARCH103` are errors: check, diff, and write invocations return exit status `1` even when fail-closed planning produces no edit. ## `5` Legacy `subl/headers` profiles The Sublime profile dialect remains available by marking the profile explicitly as legacy. The loader is immutable: it normalizes the input into modern `HeaderProfile` / `HeaderSpec` values and does not alter the mapping supplied by a caller. ```yaml headers: sublime: legacy: true levels: defaults: mark: '#' margins: [0] width: content formatters: [TITLE] idx_spec: styles: [NUMBER] dotted: true marked: '.' 0: idx_spec: forced: true 1: mark: '-' continuous: true margins: [2, 1, 2, 1] idx_spec: styles: [ALPHAU, NUMBER] dotted: false marked: ')' tagged: true ``` Legacy uppercase formatter and index-style names are accepted. The compatibility mappings are: - `dotted` maps to `separator`. - `marked` maps to `marker`. - `tagged` maps to a backtick index wrapper and preserves tag metadata. - `forced: true` maps to `index_policy: required`. - `forced: false` maps to `optional`. - A legacy `NONE` style maps to `forbidden`. Sparse integer levels inherit their preceding level, and deeper headers reuse the final level. New files saved by the editor use the modern `levels:` list syntax. ## `6` `arch spec`: keyboard-first profile editor `arch spec` builds or edits a profile in a terminal. It has no mouse requirement or mouse path. That support is intentionally deferred. Pass an input profile to edit it in place, or an output path to save a new profile. ```console arch spec --input headers.yaml arch spec --input old-sublime.yaml --output headers.yaml arch spec --snapshot # deterministic plain-text preview; safe off a terminal ``` | Key | Action | | -------------------------- | ------------------------------------ | | `j` / `k`, Tab / Shift-Tab | Select a field. | | `h` / `l` | Select a header level. | | `+` / `-` | Cycle the selected setting. | | `?` | Toggle the key reference. | | `w` | Save the current profile atomically. | | `q`, Esc, Ctrl-C | Exit and restore the terminal. | The editor exposes every supported `HeaderSpec` setting, including the complete index grammar: styles, separator, marker, maximum depth, numeric origin, and symbol alphabet. An immediately inverse `+` / `-` action restores the exact loaded value, including valid values outside the editor's presets. The editor uses a pure, immutable reducer for profile changes, a deterministic textual snapshot for tests and scripts, and a narrow PyRatatui rendering layer. It refuses interactive mode unless both standard input and standard output are terminals. Install the package's `terminal` extra when PyRatatui is not already available.