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:

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:

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.

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.

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.