badness 0.12.0

A language server, formatter, and linter for LaTeX
# Recorded intentional parse-concordance deviations vs texlab.
#
# Keys are corpus file names (e.g. `math.tex`); values are a one-line reason.
# Used by the soft `tests/parse_compat.rs` gauge to bucket a divergence as a
# deliberate, documented choice rather than an open triage item.
#
# badness models generic TeX surface syntax; texlab resolves semantics. Divergence
# is expected — record here only deviations we have consciously decided to keep.
# See AGENTS.md ("measure against, never match").

[deviations]
edge.tex = "Greedy `{…}` args attach to control *words*, not bare control-symbol tokens (`\\^{}`, `\\~{}`), so accent groups stay siblings; `\\def`'s signature is not special-cased, so its argument attachment differs from texlab."
environments.tex = "texlab opens a semantic scope for `\\item` (nesting following content) and glues subscripts into the command name (`\\varepsilon_0`); badness keeps `\\item` a generic command and `_0` a separate SUBSCRIPT. `\\` is a LINE_BREAK node here vs texlab's generic command + greedy optional arg."
math.tex = "texlab glues the subscript into the command name (`\\int_0`); badness keeps `\\int` a command and `_0` a separate SUBSCRIPT (badness is the more faithful surface reading). `\\` is a LINE_BREAK node vs texlab's generic command + optional arg."
sectioning.tex = "texlab opens a semantic scope for sectioning commands (`\\section`, `\\subsection`, `\\paragraph`), nesting following content inside; badness keeps them flat generic COMMANDs (meaning never leaks into the parser, AGENTS.md tenet 2). Same scoping family as `\\item`."
citations.tex = "Citation/ref commands and their opt+required args are concordant; the only divergence is texlab nesting the trailing `\\label`/`\\ref` inside the preceding `\\section` scope, which badness keeps flat (section scoping, same as sectioning.tex)."
comments_trivia.tex = "Comment/trivia attachment produces no skeleton divergence (trivia is projected away); the only divergence is texlab nesting post-`\\section` content (itemize, math) inside the section scope, which badness keeps flat (section scoping)."
nested_envs.tex = "texlab nests the inner `itemize` and its math inside the enclosing `\\item` scope; badness keeps the nested environment a flat sibling after `\\item` (item scoping, same as environments.tex)."
optional_args.tex = "Nested optional args, key-value options, and bracketed optionals (`\\sqrt[\\frac12]{x}`, `\\item[{[b] label}]`) are concordant; the only divergence is texlab nesting a trailing `\\newcommand` inside the preceding `\\item` scope (item scoping)."
accents.tex = "Control-symbol accents (`\\\"{o}`, `\\'{e}`) keep their `{…}` group a sibling because greedy attachment fires on control *words*, not control symbols; texlab nests the group as the accent's argument. Same deviation as edge.tex."
newcommand.tex = "`\\newcommand`/`\\renewcommand`/`\\DeclareMathOperator`/`\\newenvironment` are concordant — including the split `\\begin`/`\\end` definition bodies (issue #45), which both parsers read as plain commands. Two divergences: `\\def\\foo{bar}`, whose signature badness does not special-case (`\\foo` and `{bar}` stay flat siblings vs texlab's signature-aware nesting, same as edge.tex), and `\\NewDocumentEnvironment`, where texlab detaches the final end-code group while badness's greedy attachment (decision #8) keeps it — the shape the xparse scanner reads."
env_demoted_end.tex = "The `\\end` side of the environment group-boundary gate (smoke-test issue #71). amsldoc.tex uses the `\\lowercase` trick to smuggle a literal `}` into text, so the `\\begin{error}` sits inside a group its `\\end` is outside of and the gate demotes it to a plain command. Its partner is then an orphan *the gate made*: left as a stray `\\end` it unwinds every enclosing environment on the way to the root, un-closing the whole `document`, so it is demoted in step. texlab pairs the `error` environment across the group and reads the surviving `\\end{document}` as the document closer. badness's reading keeps the parse clean and diagnostic-free."
env_group_boundary.tex = "The environment group-boundary gate (smoke-test issue #71, same family as the issue #45/#55 definition-body rule and the `$`/`\\[` shape gates): an environment cannot outlive the brace group its `\\begin` opened in, because braces are catcode structure while `\\begin`/`\\end` are only macros. So a `\\begin` whose `\\end` is unreachable before the enclosing `}` — array.sty's sibling `\\newcolumntype` groups, rotex.tex's sibling `\\BeginExample`/`\\EndExample` macros, `\\begin{split}` as prose in a `\\PackageError` message — parses as a plain command, as does an `\\end` reached inside a group (`\\StopEventually{\\end{document}}`). texlab pairs these into `(env)` nodes that swallow the closing braces and cascade into unmatched-brace noise. badness's reading keeps the parse clean."
display_math.tex = "Bundles three recorded deviations: subscript gluing (`\\int_0`), `\\`-line-break optional-arg artifacts (texlab manufactures empty `(opt)` nodes badness does not), and `\\(…\\)` which badness models as INLINE_MATH while texlab leaves the delimiters as bare commands (badness is the more faithful surface reading)."
math_operators.tex = "`\\left…\\right` is a `LEFT_RIGHT` node isolating the delimiter token (`\\left[` lexes `[` as the delimiter, not an optional-arg open); texlab instead parses delimiter content into a `(opt)`/bracket group. badness is the more faithful surface reading (AGENTS.md decision #1)."
math_unclosed_left.tex = "The `\\left` shape gate (smoke-test issue #77, same family as the `$`/`\\[` gates): a `\\left` whose `\\right` is unreachable (a bare `)` closer, or `\\left\\bra …` with no `\\right`) is demoted to a plain command with no diagnostic, so the formatter still accepts the file — a likely typo is linter territory, not a parse error. texlab reads every `\\left` as a delimiter opener and errors on the imbalance. badness's reading keeps the parse clean; the well-formed nested-env pair in the same file still pairs as a `LEFT_RIGHT`."
conditionals.tex = "badness does not evaluate `\\iffalse…\\fi` (no TeX execution, a non-goal), so it reads the body as generic commands and greedily attaches `{Hidden}` to `\\section`; texlab's conditional handling drops that group. badness is the faithful surface reading."
tables.tex = "Not a parser divergence: badness's CST keeps the `\\begin{tabular}{lcr}` column-spec as a GROUP child of BEGIN, but the skeleton projector drops BEGIN wholesale, so the group is hidden while texlab surfaces it. A gauge-projection artifact — see docs/parse-compat-triage.md."
verbatim_env.tex = "badness protects `verbatim`/`lstlisting` bodies as a single opaque VERBATIM_BODY (built-in signature DB); texlab under its default config parses *into* the body (`{braces}`→group, `\\commands`→command). badness is the more faithful reading; texlab is the one diverging."
verbatim_cmd.tex = "The verbatim argument now attaches as a *child* of the command (matching texlab's structure); the residual divergence is deliberate: badness keeps `\\url`/`\\lstinline` bodies opaque (VERB→`(verbatim)`) where texlab parses them as a `(group)`, and `\\verb`/`\\verb*` is one opaque VERB token vs texlab's command + verbatim split."
verbatim_braced_only.tex = "The braced-only capture cases (`$\\code:A+B$`, TikZ `\\path (0,0)`, issue #53) are concordant — both parsers read plain commands. The residual divergence is the same deliberate family as verbatim_cmd.tex: badness keeps the *braced* `\\code{…}`/`\\url{…}`/`\\lstinline|…|` bodies opaque (VERB→`(verbatim)`) where texlab parses into them (`(group)`, and math inside the jss `\\code{$ …}` body)."
definition_body_hooks.tex = "Hook and command-definition bodies (issue #55, same family as the issue #45 deviation in newcommand.tex): badness reads `\\begin`/`\\end` inside `\\AtBeginDocument`/`\\AtEndDocument`/`\\newcommand` bodies as plain commands (they balance across groups, not within one); texlab pairs the hook's `\\begin{stretchpage}` into an `(env)` and drops the split `\\end`/`\\begin` of the `\\newcommand` body. badness is the faithful surface reading."
math_probability_brackets.tex = "The math bracket gate (issue #55) leaves `\\P[` an ordinary atom because the lone `]` is claimed by the command-abutting `\\gamma[`; texlab attaches both brackets as nested optional args sharing that `]`. badness's reading keeps the parse clean (no unclosed-`[` diagnostic) and matches how the text renders."
doc_shortverb.tex = "doc's `\\MakeShortVerb{\\|}` short verbs (issue #57): badness captures `|}|`, `|$|`, and `|\\begin{document}|` as opaque VERB spans under the toggle-tracking lexer mode (AGENTS.md decision #1); texlab has no short-verb model and reads the delimiters as live syntax (unmatched braces, math, a nested `document`). badness is the faithful doc-package reading; the file is also on the parse_oracle exception list."
dollar_data.tex = "The `$` shape gate (smoke-test issue #60): a `$`/`$$` with no reachable closer before the enclosing group's `}`, an unowed `\\end`, or a paragraph break is macro-code data (a tabular preamble's `>{$}`, an expl3 token list's `{ $ }`), kept as a plain token — no math node, no diagnostic. texlab opens math optimistically and lets it swallow the rest of the group. badness's reading keeps the parse clean and mirrors the issue #43/#55 bracket gate."
bracket_data.tex = "The text-mode `[` shape gate (smoke-test issue #60, same family as the `$` gate): a `[` whose `]` is unreachable before an unbalanced `}`, a `\\begin`/`\\end` outside a definition body, or a paragraph break is macro-code data (`\\@ifnextchar [\\@xmpar\\@ympar`), kept as a plain token — no OPTIONAL, no diagnostic. texlab attaches the bracket optimistically and lets it swallow the rest of the group; it also nests `\\def` signatures, which badness keeps flat (edge.tex family). badness's reading keeps the parse clean."
braceless_end.tex = "The `\\begin`/`\\end` shape gate (smoke-test issue #60, same family as the `$` and `[` gates): a `\\begin`/`\\end` with no name-shaped `{…}` after it (`\\let\\end\\@@end`, `\\long\\def\\@gobble@nv#1\\end#2{…}`, `\\end{#1}`, `\\end{\\reserved@a}`) is the bare TeX primitive or macro data, kept as a plain command — no environment, no diagnostic. texlab reads every `\\end` as an environment closer and manufactures `(env)` nodes and errors from the macro code. badness's reading keeps the parse clean."
def_display_math.tex = "The `\\[`/`\\(` shape gate and `\\def`-family name isolation (smoke-test issue #65, same family as the `$` gate): the control symbol after `\\def` is the sequence being defined and its body is a macro-code body, so the trivlist split across `\\def\\[`/`\\def\\]` bodies and the data `$`s stay plain; a `\\[` with no reachable closer (`\\expandafter\\@tempa\\[\\@nil`) is likewise a plain token. texlab pairs the trivlist into an `(env)` and opens `(math)` at the body dollars, letting them swallow the macro code. badness's reading keeps the parse clean."
char_constant.tex = "TeX char-constant backtick notation (smoke-test issues #60, #71): after a numeric-context primitive (`\\char`/`\\catcode` code-tables, `\\number` and friends), a backtick makes the next character data, so `` \\char`$ ``/`` \\char`} `` lex as one plain WORD and the escaped `` \\number`\\[ ``/`` \\number`\\] `` never open display math — the char-code table's `\\relax[ … ]` pairs across rows. texlab has no char-constant model: it opens math at the `$`/`\\[` and reads the `}` as unmatched. badness's reading keeps the parse clean; the file is also on the parse_oracle exception list."
expl3_region_env_tokens.tex = "The expl3-region `\\begin`/`\\end` gate (smoke-test issue #60, same family as the definition-body rule): an expl3 region is code, and token lists pass `\\begin`/`\\end` around as data (l3prefixes.tex builds a longtable across two `\\tl_set`/`\\tl_put_right` bodies), so in-region they parse as plain commands — no pairing, no diagnostics. texlab pairs the `\\begin { longtable }` into an `(env)` that swallows the rest of the region. badness's reading keeps the parse clean."
math_nested_env_paragraph.tex = "The math shape gates' paragraph-break anchor is level-scoped (smoke-test issue #70): a blank line inside a nested environment or `{…}` group belongs to that body, not to the math, so `\\[ … \\begin{tikzpicture}` + blank line + `… \\]` still pairs. Both parsers agree on that pairing; the residual divergence bundles three already-recorded families — `\\def\\arraystretch{2.2}` signature nesting (edge.tex), TikZ/array option brackets texlab attaches as `(opt)` where the math bracket gate keeps them plain atoms (math_probability_brackets.tex), and `\\(…\\)` as INLINE_MATH vs texlab's bare delimiter commands (display_math.tex)."