badness 0.6.0

A language server, formatter, and linter for LaTeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# badness—Roadmap

A LaTeX formatter, linter, and language server on a lossless rowan CST,
following **rust-analyzer's** architecture. See `AGENTS.md` for load-bearing
design decisions and invariants.

Single-crate package (not a workspace). Parser and formatter are **intentionally interleaved**: the formatter is the primary tool for stress-testing the parser.

Status: `[ ]` todo · `[~]` in progress · `[x]` done

--------------------------------------------------------------------------------

## Parser

- [x] **Math operator atoms.** In math mode a `WORD` glued around `+ - * / = < >`
  is split into flat operator/operand atoms (byte-range `split_math_word`, trailing
  operand is the scriptable base). Needed a `SubTok` event. See `AGENTS.md`
  decisions #3/#4.

- [x] **Math environments parse in math mode.** An environment the built-in DB flags
  `math` (`equation`, `align`, `gather`, matrix, …) has its body wrapped in a `MATH`
  node (`math_environment_body`, gated by `is_math_environment`), so it enjoys the same
  math-aware layout as `\[…\]` instead of formatting as prose. No lexer changes. See
  `AGENTS.md` decision #1 ("Math environments").

## Formatter

- [x] **Math operator spacing.** A single space around each binary/relation atom
  (`a+2*1^5` -> `a + 2 * 1^5`, `x=-b` -> `x = -b`); unary signs and `^`/`_` scripts
  stay tight; command operators (`\cdot`, `\leq`) join via `math_atom_role`.
  Group bodies normalized (`x^{a+b}` -> `x^{a + b}`). Scientific notation (`1e-5`)
  is a known non-special-cased limitation.

- [x] **Math environment layout.** `lower_math_environment` routes a `math`-flagged
  environment's `MATH` body: a single formula (`equation`) through the relation-aware
  display breaker (`lower_display_math_body`); a grid (`align`/matrix, or a `gather`
  row stack) through `build_alignment_grid` in math mode (cells lower via
  `lower_math_seq`). A top-level `\\` is a hard break in a math body, so row stacks and
  grid-fallbacks keep each row on its own line. Long align rows stay flat (parity with
  the prior grid); per-cell relation breaking is a possible later refinement.

- [x] `Sentence`/`Semantic` (sembr) wrap modes. One sentence per line (width
  ignored); `Semantic` additionally preserves authored soft breaks. Boundary
  detection is a per-language abbreviation profile (`formatter::sentence`, ported
  from panache) driven by `[format] lang` + `[format.no-break-abbreviations]`.
  Babel/polyglossia language auto-detection is deferred (config-only for now).
- [ ] **Opaque-group layout non-determinism.** The content-kind taxonomy has
  landed: `ArgSpec` now carries a `ContentKind` enum (`Opaque`/`Prose`/
  `TokenList`) the formatter dispatches whitespace and break policy on
  (`DocumentBody` stays an environment-body concept via
  `EnvironmentSig::no_indent`; add it when a command-arg case appears). What
  remains is the non-determinism fix: `spans_multiple_lines` decides
  block-vs-inline from incidental source newlines, sidestepped for the
  `TokenList` kind but still governing every `Opaque` multi-line group. Give
  `Opaque` groups a deterministic layout policy that does not depend on
  incidental whitespace.
- [ ] **Long collapsed cite list overflow.** A `collapse` arg folds to one line
  even when the key list exceeds the width; it never breaks *at commas* (one
  key per line) as a fallback. Needs the token-list content kind to break on
  its own separators rather than the paragraph fill.
- [ ] Widen the prose-argument table (CWL ingest could feed it); consider gluing
  a prose arg onto its command line when a source break separates them.
- [x] **Brace-group body reflow (`ReflowKind::Statement`).** A multi-line brace
  body (a `\newcommand` definition body) now reflows as code-like *statements*: each
  source line stays its own logical line, but an over-long one wraps to the width
  (breaking before a trailing `{…}` atom) instead of forcing the printer to detonate
  the innermost nested prose group—the only soft break a rigid
  `lower_element_stream` body exposed. Continuation is **flush**, not hanging.
- [ ] **Hanging continuation indent for wrapped statements (B', deferred ---
  blocked on structure).** A wrapped brace-body line ideally hangs its continuation
  one step in (`\node[…] at (2,3)`/`····{…};`) to read as a continuation rather
  than a sibling. This **cannot be idempotent** under the generic CST: the wrap
  becomes a real source newline, and on re-parse the continuation is just a line at
  the body indent (no marker says "continuation"), so the next pass flushes it ---
  `fmt(fmt(x)) != fmt(x)`. Flush-B sidesteps this precisely because there is no
  indent delta. The real fix needs a node that *owns the whole statement*, so layout
  derives from structure (source newlines insignificant). For the motivating case
  (`\node[…] at (2,3) {…};`) that node is a **TikZ path statement**: `at` keyword,
  `(coord)`, `;` terminator, `{label}`—none of which are TeX-surface facts
  (`;`/`at`/`()` carry no special catcode in plain TeX), so grouping them is
  package-specific grammar, out of scope for the generic parser (decisions #1, #2;
  non-goals). Belongs in a future sanctioned **TikZ-aware mode** (its own grammar,
  corpus, and AGENTS.md amendment), not a formatter patch.
- [ ] Column-spec-aware L/C/R cell alignment and `\multicolumn` for the table
  environments (`tabular`/`array` are now grid-aligned, but every column is
  left-aligned regardless of its `{lcr}` spec). Also: `\cmidrule(lr){2-3}`
  paren trim specs (the parenthesized part isn't recognized as part of the
  rule line, so such a line is treated as a cell and the table falls back),
  and the same-line `\\ \hline` form (only own-line rule commands become
  passthrough lines today).

## Linter

Ships today: `duplicate-label`, `deprecated-command` (`\bf`→`\bfseries`),
`missing-nonbreaking-space` (tie before cite/ref), `obsolete-environment`
(`eqnarray`→`align`), `dollar-display-math` (`$$`→`\[…\]`), `mismatched-delimiter`
(`\left…\right` orientation), `undefined-ref`, `undefined-citation`.

A new rule is a unit struct implementing `Rule` (`src/linter/rules.rs`), added in
four spots there (`mod`, `pub use`, `all_rules()`, `ALL_RULE_IDS`, kept in lockstep
by `registry_and_id_list_agree`) plus a new `src/linter/rules/<name>.rs`. Node-shape
rules declare `interests(&[SyntaxKind])` + `check`; whole-file/semantic rules use
`check_file` (reading `ctx.model`, `ctx.resolution`, `ctx.citations`, signature DB).
An optional `Fix::safe`/`Fix::unsafe_` is picked up by `--fix`, LSP code actions, and
`select`/`ignore` for free. The candidates below are all **type-(B)** lints
(content/semantic/typographic) a deterministic formatter would never make — type-(A)
layout items (whitespace, indentation, brace-on-scripts) are the formatter's job and
excluded. Sources: ChkTeX (numbered warnings), lacheck, textidote.

- [~] Wire the remaining report-only fixes onto the autofix infra:
  `deprecated-command`'s `\bf → \bfseries` is **done** (a `Safe` control-word swap,
  consumed by `lint --fix` and the new LSP code actions); `obsolete-environment`'s
  `eqnarray → align` is still report-only.
- [~] `missing-nonbreaking-space` (a tie before a cite/ref command, broad curated
  family, `\nocite` excluded; `Unsafe` autofix) is **done**. *Follow-up:* the tie lint
  only covers a same-line `WORD WHITESPACE \cmd` shape; a *source line break* before
  the command (`Figure\n\ref{x}`) is also a breakable space but is left for a later
  pass (replacing the newline with `~` reflows the source and overlaps the formatter).

### Tier 3 — structural / semantic / project-layer (curated subset)

Whole-file or cross-file (`check_file`), using the semantic model, signature DB, and
project resolution. Pure prose-opinion textidote rules (title capitalization, caption
period, section length) are skipped as grammar-tool territory.

- [ ] `missing-required-argument`—command invoked with fewer `{…}` groups than its
  signature-DB arity (ChkTeX 14, done precisely via the tree + DB, not line
  heuristics). Report-only.
- [ ] `verbatim-trailing-text`—text after `\end{verbatim}` on the same line, silently
  dropped (ChkTeX 31). Report-only.
- [x] `unbalanced-left-right``\left` with no matching `\right`. **Already covered by
  the parser:** `left_right` recovery emits an `unclosed \left` parse diagnostic on
  every unbalanced-`\left` path (EOF, `}`, `$`, `\]`, `\end`, paragraph break), so a
  dedicated lint rule would only duplicate the existing `parse` finding on the same
  span. No separate rule added.
- [ ] `unreferenced-label`—a `\label` never targeted by any `\ref`-family command,
  project-aware behind the same closed+rooted namespace gate as `undefined-ref`
  (**supersedes the old `unused-label` deferral**, whose open-namespace false-positive
  risk is exactly what that gate handles). Report-only.
- [x] `sectioning-level-jump`—a heading that skips a level (`\section`  `\subsubsection`), read from the semantic sectioning tree (textidote sh:secskip).
  Report-only.
- [x] `hard-coded-reference`—literal "Figure 3"/"Section 2"/"Table 1" in prose
  instead of `\ref`/`\cref` (textidote sh:hcfig/hctab/hcsec). Report-only, heuristic.

## Semantic layer & signatures

- [ ] How much of `\newcommand`/`xparse` to model for the signature DB. *(open
  decision)*

## Language server

### Feature status vs texlab

A deliberate diff against **texlab** (5.25.1) as the mature reference LSP. The
comparison is asymmetric, and the framing matters when triaging the items below.

- **Badness leads:** range formatting, on-type formatting, code-action quick-fixes
  (texlab's `codeAction` handler returns an empty array), a native linter (20
  LaTeX + 10 bib rules vs texlab's 4 built-ins plus a `chktex` shell-out),
  comment-run folding, the deterministic rule-based formatter, and a first-class
  bib formatter with entry sorting. texlab also ships **no** semantic tokens,
  signature help, selection ranges, or code lens—so those are not texlab gaps.
- **Badness trails (tracked below):** completion breadth (`### Completion`),
  macro/include navigation and matching-pair highlight (`### Navigation &
  structure`), label preview and macro references/rename
  (`### Labels & references`), package hover, and the change-environment refactor
  (`### Code actions`).
- **Deliberately not matched:** the typeset-adjacent features under
  `## Editor integration` (build, clean-aux, chktex passthrough, CSL rendering,
  kpsewhich DB).

### Configuration & sync

- [x] config over LSP—the LSP now discovers `badness.toml` per document
  (`GlobalState::resolve_settings`, cached by anchor dir, cleared on
  `didChangeConfiguration`). A discovered config wins outright
  (file-wins); editor settings are the fallback. Both `[format]` (`line-width`,
  `indent-width`, `wrap`) and `[lint]` (`select`/`ignore`, applied via
  `RuleSelection` in the analyze/diagnostic/code-action paths) are honored. Two
  follow-ups remain:
  - Deliberately *not* done: plumbing `wrap` (or other knobs) through
    `EditorSettings` itself. A discovered config's `wrap` flows via `FormatConfig`,
    so no new editor knob was needed; `EditorSettings` stays `line_width`/`indent_width`.
- [ ] `workspace/diagnostic` (the workspace-wide pull)—deferred: it is a
  streaming/long-poll protocol (held-open request, per-uri result ids, partial
  results) that fits the one-shot id-bound read-job model poorly. Advertise
  `workspace_diagnostics: true` and add it once that plumbing exists; editors
  drive interactive diagnostics through `textDocument/diagnostic` meanwhile.
- [x] `workspace/didChangeWatchedFiles` + dynamic `client/registerCapability`
  for `**/*.{tex,bib}` and `badness.toml` so on-disk edits to non-open
  includes/`.bib` files (the project graph's leaves) and the config reanalyze.
  Watchers are registered post-handshake (`register_file_watchers`) when the client
  advertises `didChangeWatchedFiles.dynamicRegistration`; no `notify`-crate fallback
  otherwise. A `.tex`/`.bib` change re-reads + re-upserts the non-open file
  (`Worker::apply_watched_change`, scoped to seeded project dirs) and re-lints via
  `RelintAll`; a `badness.toml` change clears the config cache and re-lints
  (`relint_all_open`). Open buffers are skipped (the editor overlay is authoritative).

### Formatting

- [x] Range formatting (`textDocument/rangeFormatting`)—expand the selection to
  whole top-level blocks (children of `ROOT`), lower only those (a byte-range
  emission filter in `format_node_range_with_signatures`, so the formatter stays
  the sole layout authority and out-of-range blocks are never laid out), then
  diff the fragment against the original slice into minimal edits. LaTeX only;
  bib is a no-op for now.
- [x] On-type formatting (`textDocument/onTypeFormatting`): typing `}` re-indents
  the containing top-level block, but only when the `}` structurally closes a
  multi-line group or an `\end{…}` (a `closes_multiline_construct` guard);
  inline `\textbf{x}` and `\begin{…}` opens are skipped. Reuses the range path
  (`range_edits_for_root` with an empty selection at the cursor). Trigger `}`;
  client opt-in (e.g. `editor.formatOnType`).

### Navigation & structure

- [ ] Selection ranges (`textDocument/selectionRange`)—expand-selection from
  the CST's node hierarchy (group → argument → command → environment). Subsumes
  texlab's `findEnvironments` command: the enclosing-environment stack falls out
  of the CST-hierarchy expansion.
- [x] **Document links (`textDocument/documentLink`).** Clickable include edges:
  `\input`/`\include`/`\subfile`/`\import`/`\subimport`,
  `\usepackage`/`\RequirePackage`/`\documentclass`/`\LoadClass` (→ resolved
  `.sty`/`.cls`/`.dtx`), `\bibliography`/`\addbibresource`, and
  `\includegraphics` (extension guessed against the graphics image types). A
  self-contained single-file, positional walk (`src/lsp/document_link.rs`)
  bypasses the range-free project graph: it takes each command's precise
  argument span via `ast::nth_group_inner` (per comma-separated name) and is
  **disk-aware**—a link is emitted only when the resolved target exists on disk
  (first existing candidate wins for the graphics guess; a `.dtx` fallback
  covers literate `.sty`/`.cls` sources). The local-only package resolver has no
  kpsewhich/TEXMF search, so a system `\usepackage{amsmath}` (no local file)
  correctly yields no link while a project-local `mypkg.sty` does.
  `\graphicspath` is unsupported (graphics resolve against `base_dir` only).
  texlab covers the include edges only (`crates/links`).
- [ ] **Go-to-definition for includes and user macros.** Extend `CursorTarget`
  (`src/lsp.rs`, today Label/Citation only): an include argument jumps to the
  target file (reuse include resolution); a user command `\mycmd` jumps to its
  `\newcommand`/xparse definition (needs the definition *span* recorded in the
  signature DB, `semantic::define`—provenance is already tracked). Supersedes the
  deferred macro-body goto note. texlab: `crates/definition`
  (command/include/label/citation/string_ref).
- [x] **Matching `\begin`/`\end` document highlight.** Highlight the paired
  begin/end of the environment under the cursor (highlight was label-key only
  before); the parser already pairs them structurally. texlab: `crates/highlights`
  (label-only) plus its `findEnvironments` command.
- [x] Workspace symbols (`workspace/symbol`)—the per-file outline (sections,
  labels, floats, theorems, macros, environments) aggregated across every tracked
  project file, filtered by the query string. LaTeX members only.

### Labels & references

- [ ] **Label hover.** Hover a `\label`/`\ref` to render a preview
  (kind + nearest heading/caption); *Caveat:* the resolved *number*
  needs `.aux` reading or counter tracking (which badness does not do), so a first
  cut shows kind + context without the number—texlab reads `.aux`
  (`crates/hover/label.rs`).
- [ ] **References + rename for user macros and environment names.** Extend
  references/rename (label/citation keys only today) to command names (cross-file,
  via the signature-DB provenance in `semantic::define`) and to environment-name
  pairs. texlab: `crates/references` + `crates/rename`
  (command/entry/label/string_def).

### Completion

Badness offers command, environment, label, cite-key, bib field/type, and file
completion (`src/completion.rs`, `src/bib/completion.rs`). texlab's completion
breadth is its biggest lead (`crates/completion/providers/`); the specialized
sources below are missing.

- [x] **Package/class name completion** for `\usepackage{}` / `\documentclass{}`
  (`package_arg` in `src/completion.rs`, `PackageName` context). Sourced from a
  baked `.sty`/`.cls` name list (`data/{package,class}_names.txt`, generated by
  `scripts/gen_package_names.py` from the pinned TeX Live tlpdb; names only, ranked
  namesake/common-first) merged with locally discovered `.sty`/`.cls` files in the
  LSP layer (`package_completion_items`). The kpsewhich/distro DB stays out of
  scope (same texmf/CTAN boundary as CWL ingest and the include graph); a runtime
  texmf-discovery tier is designed but deferred (opt-in; would cross that boundary).
- [ ] **Glossary/acronym key completion** (`\gls`/`\acrshort`/… ←
  `\newglossaryentry`/`\newacronym`)—extend `semantic::define` to scan these
  definers, mirroring the existing label/theorem discovery.
- [ ] **Color name + model, TikZ/PGF library completion**—small static datasets
  for `\color`/`\textcolor`/`\definecolor` and
  `\usetikzlibrary`/`\usepgflibrary`.
- [ ] **Argument-value enum completion** for fixed enumerated argument choices—
  needs the signature DB to carry per-argument value enums.
- [ ] **Package hover** (`\usepackage{amsmath}` → description). Depends on a
  package-metadata dataset badness does not yet ship (texlab ships CTAN
  descriptions in its component DB, `crates/hover/package.rs`). Pairs with the
  label-hover item above.
- [ ] *(Design decision)* **Package-scoped command completion.** texlab suggests
  only commands provided by the loaded packages (a package→command component
  model). Badness's signature DB is flat (curated + CWL + scanned); scoping
  completion to `\usepackage`-loaded packages needs package→command attribution.
  Open question, not a mechanical add.

### IntelliSense (signature DB)

- [ ] Signature help (`textDocument/signatureHelp`)—show the active argument
  while typing a command's `{…}`/`[…]` arguments. (Not a texlab gap—texlab has no
  signature help—but a natural fit for the signature DB.)

### Code actions

- [ ] **Change-environment refactor.** A code action rewriting the
  `\begin{a}`/`\end{a}` name pair around the cursor to a new environment. A
  correctness-only textual edit (never invokes the formatter—tenet #1); reuse the
  paired begin/end spans the parser already builds. texlab exposes this as the
  `texlab.changeEnvironment` execute-command.
- **Note (not a rule):** texlab's `MismatchedEnvironment` diagnostic
  (`\begin{a}…\end{b}`) is already covered by the parser's `unclosed environment …
  (found \end{…})` finding (`src/parser/grammar.rs`), surfaced via the synthetic
  `parse` lint—so, as with `unbalanced-left-right`, no separate rule is added.

### Infrastructure

- [ ] Client capability negotiation—gate advertised providers and
  UTF-8/UTF-16 position encoding on what `initialize` reports.
- [ ] README editor-wiring docs (Neovim/VS Code `initializationOptions`,
  `badness lsp` invocation).

## Package & class infrastructure (`.sty`/`.cls`/`.dtx`/`.ins`)

The document-level tools are mature; the next frontier is the **package
ecosystem**—class and package sources, and the literate `.dtx` format they
ship in. This is a large, multi-area subproject (parser + formatter + semantic).
It stays inside the AGENTS.md non-goals: bounded, statically-recognizable
patterns only, signatures *extracted, never executed*, no docstrip run, no TeX
engine. Local project files only—a `texmf`/CTAN/`kpsewhich` search is out of
scope (the same boundary the include graph and CWL ingest keep).

### Parsing

- [x] **expl3 full catcode model.** `~` is a literal space (catcode 10) and spaces/tabs
  are ignored (catcode 9) inside expl3 regions, so the formatter owns in-region layout
  regardless of `WrapMode`: one statement per source line, brace-group code blocks indent,
  inter-token whitespace collapses to a single space, and `~` is a breakable space. The
  formatter recomputes region membership read-only (`formatter::core::expl3_regions`, sharing
  the lexer's `expl_toggle` set); the CST/lexer are untouched, so losslessness holds and the
  layout is idempotent by construction (catcode-9 whitespace is insignificant, so it supersedes
  the flush-B deferral for expl3 — see "Hanging continuation indent" above). See the expl3
  code-formatting note in `AGENTS.md` decision #1.
- [ ] **expl3 implicit detection in toggle-less `.dtx` (deferred).** Real expl3
  package sources (e.g. `ltx-talk-structure.dtx`) carry no in-file `\ExplSyntaxOn`/
  `\ProvidesExpl*`; expl3 is declared in the parent `.dtx`/build, and `@@` is a
  docstrip module prefix (`%<@@=mod>`). Treat `macrocode` bodies as expl3 when the
  file carries a static expl3 signal (a `%<@@=mod>` guard or `\ProvidesExpl*`
  anywhere). Needs a file-level scan plus the `macrocode` save/restore interaction
  (mirror `at_letter`).

### Formatting

### Semantic and integration

- [x] **Signature extraction from package sources.** The `semantic/define.rs`
  scanner already runs across loaded `.sty`/`.cls` (via `scope_signatures` and its
  db-less CLI mirror `collect_package_signatures`), and already recognizes
  `\DeclareRobustCommand` (`DefKind::Command`) and `\DeclareDocumentCommand` + the
  `\New/Renew/Provide...DocumentCommand/Environment` family (`DefKind::Xparse*`).
  Added: package resolution now falls back to a package's `.dtx` literate source
  when no generated `.sty`/`.cls` is a member (preferring the generated file when
  both exist), in both `PackageGraph::build` and the CLI `collect_loaded`
  (`project::package::dtx_source_of`). The `.dtx` is scanned whole-tree by the
  existing per-file `document_signatures` (its `macrocode` bodies already lex as
  real code). Broadening the definer set beyond the above (`\DeclareMathOperator`,
  etoolbox `\newrobustcmd`/`\csdef`, ...) is deferred.
- [ ] **Package metadata & options (recognize, never execute).**
  `\ProvidesPackage`/`\ProvidesClass` (name/date/version),
  `\NeedsTeXFormat`, `\DeclareOption`/`\ProcessOptions`/`\ExecuteOptions` ---
  surfaced as signatures/metadata for hover/diagnostics, never run.
- [ ] **Package-aware diagnostics.** Once the load graph exists: unknown-option,
  duplicate `\RequirePackage`, missing `\ProvidesPackage`, and resolving
  user-macro definitions to their defining package for hover/go-to-definition.

## Performance & hardening

- [ ] Fuzzing (losslessness must hold on arbitrary input).
- [~] Large-doc benchmarks (`hyperfine`, criterion); flamegraph hot paths.
  Formatter speed bench vs `tex-fmt`/`latexindent` landed (`benches/compare_format.sh`,
  `task bench`, writes `benches/benchmark_results.json`, which feeds the docs
  benchmark page `docs/src/reference/benchmarks.md`). In-process parse/format micro-bench +
  flamegraph hot paths landed (`benches/formatting.rs`, `task bench:micro`/`bench:profile`;
  see the profiling item below). Still pending: bib + lint benchmarks.
- [ ] Intra-file incremental reparse (reuse green subtrees on contained edits).
- [ ] `wasm32` build for a web playground.

## Editor integration

texlab bundles PDF-workflow features. Only position mapping (no typesetting by
badness) is admissible; the rest are explicit non-goals recorded here so they are
not re-proposed.

- [ ] **Forward/inverse SyncTeX search (no typesetting).**
  `textDocument/forwardSearch` (a custom LSP method) locates a configured PDF and
  drives an external viewer; inverse search receives a viewer position over IPC
  and answers with `window/showDocument`. Badness never typesets—it only maps
  source↔PDF positions via SyncTeX and shells the viewer. texlab:
  `crates/commands/fwd_search` + the `ipc` crate.
- **Non-goals (do not re-propose):** build/latexmk orchestration
  (`textDocument/build`), clean-aux/artifacts, `chktex` passthrough (the native
  linter supersedes it), citeproc/CSL bibliography rendering (badness shows a
  lightweight entry summary; full CSL is out of scope), and the kpsewhich/distro
  package DB (same texmf/CTAN boundary as CWL ingest and the include graph). These
  sit inside the AGENTS.md "We never typeset" non-goal.

## BibTeX/BibLaTeX

- [ ] Cross-file `undefined-string`: a `@string` defined in one `.bib` and used
  in another resolves only once a project-level `@string` union exists (today
  single-file-sound, same caveat as `unused-string`).
- [ ] `unused-entry`: a `.bib` entry never targeted by any `\cite`-family
  command, project-aware behind the same closed+rooted namespace gate as
  `unreferenced-label`/`undefined-ref` (the bib linter has `unused-string` but no
  `unused-entry`). Report-only. texlab: `UnusedEntry`.
- [x] Bib-aware LSP completion: `@string` macro names in value position, field
  names per entry type (type-scoped, hiding fields already present), and entry
  types after `@` (`src/bib/completion.rs`); plus `\cite` key completion on the
  `.tex` side, resolved cross-file via `ResolvedCitations` (`src/lsp.rs`
  `cite_completion_items`).
- [ ] Bib document-symbol outline completeness: `src/bib/outline.rs` surfaces
  regular entries only; consider `@string`/`@preamble`/`@comment` blocks (and a
  richer `SymbolKind`/detail).
- [ ] `title-capitalization` refinement: the acronym heuristic flags mid-word
  capitals, so CamelCase names (`McDonald`, `DeForest`) are false positives ---
  a curated name-particle allowlist or a smarter word model would tighten it.
- [ ] Shared component-finder: `ResolvedCitations` duplicates the union-find +
  component assignment from `ResolvedLabels` (`project/citations.rs`); factor one
  helper when a third consumer appears.

--------------------------------------------------------------------------------

## Open decisions to revisit

- [ ] How much of `\newcommand`/`xparse` to model. *(Semantics)*
- [ ] Formatter opinionatedness: configurable vs. fixed. *(Formatter)*
- [ ] `.dtx` two-layer model: a preprocessor that splits doc/code layers, or a
  single lexer mode with margin-aware tokens? *(Package infrastructure)*