# Changelog

All notable changes to `jss-style-checker` land here. Version numbers follow
[semantic versioning](https://semver.org/). Breaking changes to the
JSON-output shape (see
`specs/001-linter-foundation/contracts/json-output.md`) require a major
version bump and an entry in this file — see the spec's Clarification Q2.

The **rule set** carries its own date, printed by `jss-lint --version` and
stamped into baseline files. A patch release may only make findings
disappear; a minor release may add rules or reword messages and
suggestions, which bumps that date and means baseline entries for the
reworded rules go stale — re-run `--update-baseline`. Channel-by-channel
version constraints and pinning advice: [`docs/versions.md`](docs/versions.md).

## [1.2.0] — 2026-09-09

### Added

- **Recall is reported everywhere, so a clean run stops overstating
  itself.** Precision has been public since 1.0; recall — of the style
  problems that exist, how many the tool finds — was measured but
  reached no user. Every run now ends with

      No findings does not mean compliant. Measured recall: 81% (1967 annotated instances, 17 papers).

  printed on stdout, including when there are no findings at all.
  Reviewer mode gains a per-category `Recall` column and a
  `Measured recall:` line; JSON gains `recall` on every category and a
  top-level `rule_set`; SARIF rule descriptors gain
  `properties.recall` and `properties.confidence`; `explain` prints a
  `Recall:` line for every rule; the catalogue page gains `Confidence`
  and `Recall` columns. Three states, never a fabricated number: an
  integer percentage at 10 or more annotated instances, `limited (n=K)`
  below that, and `unmeasured` where the corpus has no instances at all
  — so the `project` category reads `unmeasured`, not `100%`. The
  measurement is pinned per release in
  `specs/003-jss-rule-catalogue/recall.json`, which the README badge now
  reads too, and it is a lower bound (source-only linting). See
  [`docs/recall-and-coverage.md`](docs/recall-and-coverage.md).
- **The tool says what it does not check.** A new curated matrix,
  `specs/003-jss-rule-catalogue/guide-coverage.yaml`, maps all 149
  provisions of the four JSS authorities — `jss.cls`, `article.tex`, the
  style guide, the author instructions, each pinned to a dated edition —
  to the rules that enforce them: **76 checked, 4 partial, 5 not
  checked, 64 out of scope**. `jss-lint coverage` prints it
  (`--format terminal|markdown|json`); reviewer mode ends with a
  "Not checked by jss-lint" block listing the gaps; the author footer
  gives the ratio (`checks 80 of 85 guide directives`); JSON gains a
  top-level `coverage` object; and `explain` reports the reverse
  direction (`Covers: SG-027, SG-028`). Out-of-scope provisions —
  compilability, graphics legibility, replication scripts — are listed
  but excluded from the ratio: they were never checkable from source.
  It replaces a markdown review checklist that had quietly credited four
  rules retired months earlier; the new file is validated on every build,
  so a rule can no longer be added without claiming a provision, nor
  retired without re-judging the rows that credited it.
- **CI defends that number.** `eval-jss recall --gate --no-record` runs
  on every push with the aggregate floor raised from 0.70 to **0.78**
  (the decision spec 017 deferred), ratcheted to the shipped snapshot
  minus 0.03 at each release, with a test that fails if the floor drifts
  further behind than that.
- **Overleaf: drop the source zip on the browser app.** Menu → Download
  → Source gives a `.zip`; dropping it on
  [the web app](https://kollerma.github.io/jss-style-checker/) unpacks it
  **in the browser tab** — no upload, no new dependency, no WASM change
  — and checks every `.tex`/`.bib`/`.Rnw`/`.Rmd` inside with its project
  paths intact, so `\input` resolution and the report's file headings
  match the project. Figures and macOS resource forks are skipped; a
  browser without `DecompressionStream` is told to use the folder picker
  instead. [`docs/overleaf.md`](docs/overleaf.md) also covers the
  command-line route and checking a GitHub-synced project on every save.
- **Coloured terminal output.** Severities, PASS/FAIL/SKIPPED statuses,
  rule ids, and banners are coloured with the basic 16-colour palette
  when stdout is a terminal, and plain when it is piped. `--color
  auto|always|never`, the TOML `color` key, `NO_COLOR`, and
  `CLICOLOR_FORCE` behave as they do in ripgrep and cargo, identically
  in both CLIs. JSON, SARIF, and HTML are never coloured, colour is
  never the only carrier (every coloured token is still a word), and
  stripping the escape sequences yields exactly the previous plain
  output — colour cannot change layout.
- **`--fix` ends with a receipt.** Both CLIs now close a fix pass with
  one line — `Applied 3 fixes to 1 file (1 skipped: conflict 1).`, or
  the `Dry run: …` form — so a command that rewrites your manuscript
  says what it did and what it declined to do. Wording matches the R
  binding's `jssfix()`, which has printed it since 1.1.0. Still no git
  interaction of any kind (spec 008 stands); the README, `rust/README`,
  and the R vignette now state the expectation to commit or `--dry-run`
  first.
- **Baseline mode: adopt the tool on a manuscript that predates it.**
  `--baseline FILE` hides findings a baseline records — from every
  output format and from the exit code — so a run fails only on what is
  new; `--update-baseline` writes the file from the current run. Also
  settable as `baseline` in `.jss-lint.toml`, and as a `baseline:` input
  to the GitHub Action, where SARIF omission narrows the Security tab to
  new findings for free. An accepted finding is identified by
  `(rule_id, path, message, suggestion)` with a count — never by line
  number, which survives no edit: on a real four-round JSS submission a
  line-based key matched 1 % of findings across the first revision
  round, against 100 % for this one. The file is plain JSON, sorted, and
  timestamp-free, so `--update-baseline` is byte-identical whichever
  engine wrote it. Its documented limits, and the interaction with
  `--fix`, `--min-confidence`, and inline ignores, are in
  [`docs/baseline.md`](docs/baseline.md). JSON output gains an
  always-present `baseline` key (`null` when inactive).
- **Rule-set provenance.** `catalogue.yaml` now carries a dated
  `ruleset_version`, a `ruleset_fingerprint` over every active rule's
  contract fields *and* the message/suggestion wording, and the
  `guide_edition` the rules derive from. A rewording can no longer ship
  without a visible rule-set date change, which matters because baseline
  entries are keyed on that wording.
- **`--version` prints a four-line block** in both CLIs — tool, engine,
  rule set with its authority edition, and the effective journal — after
  `.jss-lint.toml` and `--journal` are resolved, so it reports the rule
  set the invocation would actually apply. The two engines differ only
  in the engine line.
- **Version functions in every binding**: `version()` in the WASM build,
  `jsslint.version()` / `jsslint.__version__` in the PyO3 wheel, and
  `jsslintr::jsslint_version()` in R, which additionally reports the
  CRAN package version (`1.2.0-N`) alongside the engine it wraps.
- **[`docs/versions.md`](docs/versions.md)**: the distribution → engine →
  rule-set mapping for all seven channels, each channel's version-string
  constraints, the compatibility policy, and what to pin.

### Fixed

- **`eval-jss recall --gate` refuses to judge an incomplete corpus.**
  A paper whose annotations exist but whose sources were never fetched
  was skipped with a warning, and its plants left both the numerator and
  the denominator — so the gate reported a confident number for a corpus
  nobody chose. It can move either way: the 1.2.0 CI run sank to 0.762,
  but dropping one paper measured 0.781, which would have *passed* the
  0.78 floor while measuring something else. The gate now fails on the
  cause and names the missing papers. Ungated runs are unchanged.

- **Bracketed text no longer disappears from terminal output.** `rich`
  parses `[word]` in a table cell as a console-markup tag and drops it,
  so any message or suggestion quoting LaTeX with an optional argument
  was rendered wrong: `\documentclass[shortnames]{jss}` came out as
  `\documentclass{jss}`, and `\citep[e.g.][]{key}` as `\citep[]{key}` —
  turning a correct suggestion into one that would introduce a
  different error if followed. The Rust port had reproduced the quirk
  deliberately to hold byte-parity, so both engines agreed and both were
  wrong. Cell text is now escaped on the Python side and emitted
  verbatim on the Rust side. Only terminal output was affected; JSON,
  SARIF, and HTML always carried the correct text.

- **The Rust engine now honours `% jss-lint: ignore`.** It never
  implemented the directives, so every surface built on it — the
  `jsslint` binary, the browser/WASM build and the hosted web app, the
  VS Code extension, the PyO3 wheel, and the R package — reported
  findings whose author had explicitly signed them off in the source,
  while the Python `jss-lint` hid them. A live engine-parity gap,
  covered from now on by `rust/jsslint-core/tests/suppress_parity.rs`
  over one fixture per directive behaviour.
- **Two long-standing bugs in the Python engine's directive handling.**
  A directive inside an `.Rmd` prose block only worked when the block
  started on line 1 (block-relative line numbers were compared against
  file-authoritative ones), and line counting used `str.splitlines()`,
  which also breaks on form feed, vertical tab, `\x1c`-`\x1f` and
  `\x85` — so a single form feed anywhere above a directive silently
  moved it to the wrong line.

### Changed

- **Ten rules now name what they found.** `JSS-CODE-001`, `JSS-CODE-003`,
  `JSS-OPER-003`, `JSS-XREF-002`, `JSS-XREF-004`, `JSS-TYPO-001`,
  `JSS-CAP-002`, `JSS-CITE-003`, `JSS-REFS-004`, and `JSS-REFS-007`
  quote a stable identifier in their suggestion — the BibTeX entry key,
  the equation label or the head of its body, the caption or section
  title, the offending code fragment, the referenced label, the cite
  keys, or the comment text. Detection is unchanged: no finding appears
  or disappears (verified finding-for-finding on 1 259 corpus files).
  Two findings of the same rule in one file are now usually
  distinguishable, which matters because the baseline mode landing in
  this release keys accepted findings on the suggestion text. On a real
  four-round JSS submission this raises the distinct-key count of one
  version from 49 to 79 while re-keying 4 of the 26 findings that
  survive a revision round. `JSS-WIDTH-001` deliberately keeps its
  generic suggestion (no stable identifier exists for an over-long
  line). This is a rule-set change: the rule-set date moves, and
  consumers keying on suggestion text see new values once.

## [1.1.0] — 2026-07-19

### Added

- **`.Rnw` (Sweave/knitr) and `.Rmd` (R Markdown) support in the Rust
  engine** — previously Python-only. Because every non-Python
  distribution shares `jsslint-core`, this lands at once in the
  `jsslint` CLI, the browser/npm WASM build (and the hosted web app),
  the `jsslint` PyO3 wheel, and the R package, with output
  byte-identical to the Python engine.
- **`--crossref` online DOI verification in the Rust CLI** — the port
  of the Python `--crossref`/`--crossref-mailto` flow (Crossref
  title/author/year matching; CRAN `@Manual` DOIs confirmed via
  `doi.org`; `--crossref --fix` writes the DOIs into the `.bib`). The
  network client lives in a dedicated `jsslint-crossref` crate that is
  never linked into the WASM/PyO3/R builds — those stay offline by
  construction, now guarded by a CI dependency-graph test.
- **`report --format html` and `--format pdf` in the Rust CLI.** HTML
  is byte-identical to the Python CLI's output. PDF is rendered by a
  pure-Rust layout (embedded fonts, works with no host fonts
  installed) and is deliberately *not* byte-identical to Python's
  WeasyPrint PDF — the one documented parity divergence.
- **`jsslint-wasm`: `fix()` and `analyze()` exports** alongside
  `render()` — in-memory auto-fixing, and structured violations with
  per-violation fix payloads (`render(json)` keeps `fix: null` for
  byte-parity with the Python JSON contract).
- **The VS Code extension runs the checker in-process via WASM.** One
  universal VSIX, nothing else to install — no Python interpreter, no
  binary. Live diagnostics while typing, per-violation quick fixes,
  and a "Fix all JSS style issues in this file" action. Replaces the
  Python-LSP design; the `jssStyleChecker.python.path` setting is gone
  (`severityOverrides`/`ignoreRules`/`codeWidth`/`runOn` remain).

### Changed

- **The release version is single-sourced.** Edit the root `VERSION`
  file and run `scripts/set_version.py`; a guard test fails CI naming
  any manifest that drifts from it. The R `DESCRIPTION` may carry a
  CRAN-resubmission suffix (`X.Y.Z-N`) on the same base version.
- **`\input`/`\include`/`\subfile`/`\bibliography` auto-resolution now
  ships (spec 013).** `jss-lint root.tex` / `jsslint root.tex` — a
  *single* file argument, not a directory or multiple explicit paths —
  now walks the reference graph and lints every reachable file as one
  project; diagnostics are attributed to the file that actually
  contains them, not the root. `--no-resolve` (previously a documented
  no-op) now genuinely disables this and lints only the file you pass.
  Two new tool-side rules surface graph problems:
  `JSS-PROJECT-001` (a cycle in the reference graph) and
  `JSS-PROJECT-002` (a `\input`/`\include`/`\subfile`/`\bibliography`
  target that doesn't resolve to an existing file); both participate
  in `--ignore-rules` like any other rule. A resolved file with a
  non-lintable suffix (e.g. a custom `.cls` loaded via `\input`, seen
  in real JSS vignettes) is silently excluded from linting rather than
  aborting the run. **JSON-output shape change**: when auto-resolve
  triggers, `Violation.file` becomes an absolute, canonicalized path
  (previously always the literal string you passed on the command
  line) — pass `--no-resolve` to keep the old single-file behaviour.

## [1.0.1] — 2026-07-18

The first published release, across four registries: crates.io
(`jsslint-core`, `jsslint-cli`), PyPI (`jss-style-checker` — the Python
package providing `jss-lint` — and `jsslint`, the PyO3 binding), and npm
(`jsslint-wasm`). The R package (`jsslintr`) gained the
`jsslint()`/`jssfix()`/`jss_files()` convenience API and a
getting-started vignette but was not yet on CRAN at release time
(it was subsequently accepted: `jsslintr` 1.1.0-1 published
2026-07-28, 1.1.0-2 on 2026-07-29).

Version 1.0.0 was burned: `jsslint-core` 1.0.0 reached crates.io
unbuildable (its build script reads rule-catalogue data that wasn't in
the crate tarball) and was yanked; 1.0.1 vendors the catalogue into the
crate, drift-guarded by a test. Everything below shipped as part of
1.0.1.

### Changed

- **Default `--fail-on` is now `warning`** (was `info`). Info-severity
  advisories (e.g. the missing-DOI rule JSS-REFS-003) are still
  reported but no longer flip CI red by default; pass
  `--fail-on info` for the old behaviour.
- **JSS-CAP-003 demoted to info severity.** The caption
  sentence-style heuristic sits at ~60% measured precision after five
  improvement attempts; it still runs and is shown by default, but no
  longer fails a run. Hide it entirely with `--min-confidence medium`.

- **Degraded-parse exit semantics.** Only error-severity
  `JSS-PARSE-000` findings force exit 2. Warning-severity parse
  findings mark a *recovered* parse (the file was fully linted, e.g.
  after an encoding fallback) and obey the normal `--fail-on`
  threshold like any other finding. Previously any `JSS-PARSE-000`
  finding exited 2.
- The LSP server lints the in-memory editor buffer directly instead
  of writing it to the file on disk. Unsaved edits no longer hit the
  filesystem, file encodings are preserved, and opening a file no
  longer bumps its mtime.

### Added

- **`JSS-XREF-007`** (info, auto-fixable) — cross-reference nouns are
  spelled out (`Figure`/`Section`/`Table`), not abbreviated
  (`Fig.`/`Sec.`/`Tab.`, plus plurals). The figure/section/table analogue
  of `JSS-XREF-002`'s `Eq.`→`Equation` rule. Fires only on an abbreviation
  immediately preceding a `\ref` macro (across an optional `~`), so the
  `\ref` disambiguates it (`sec.` = seconds stays silent); `\autoref` /
  `\cref` generate the noun themselves and are out of scope. Auto-fix
  rewrites to the spelled-out noun with a non-breaking space.
- **`JSS-XREF-005`** — the figure/table analogue of `JSS-XREF-004`: a
  captioned (numbered) `figure`/`table` (or starred variant) must carry a
  `\label{}` and be referenced from the text; an unlabelled or never-
  referenced (orphan) float is flagged at warning severity. Captionless
  (unnumbered) floats are out of scope. `JSS-XREF-001`'s catalogue
  wording was corrected to describe only what it enforces — the
  cross-reference *form* (use `\ref{}`, not a hardcoded "Figure 2") — now
  that the label/orphan concern lives in `JSS-XREF-005`.
- **`JSS-BIBTEX-005`** — flags a field key repeated within a single
  BibTeX entry (e.g. two `author =` lines, or duplicate
  `volume`/`pages`). BibTeX keeps only the first occurrence and silently
  drops the rest, so the rendered citation loses data. Previously such an
  entry tripped a catastrophic `JSS-PARSE-000` that failed the whole
  document; the parser now treats `bibtexparser`'s recoverable
  `DuplicateFieldKeyBlock` like its `DuplicateBlockKeyBlock` sibling and
  reports the dropped field(s) via this rule instead.
- **Directory arguments.** `jss-lint .` (or any directory path)
  recursively lints every supported file beneath it, in deterministic
  sorted order; an empty expansion exits 2 with a clear message.

- **VS Code settings are now honoured by the LSP server.**
  `jssStyleChecker.ignoreRules` (unioned with `ignore_rules` from
  `.jss-lint.toml`), `jssStyleChecker.severityOverrides` (per-rule,
  client wins over the file), `jssStyleChecker.codeWidth`, and
  `jssStyleChecker.runOn` (`"save"` lints on save only). Settings
  changes re-lint open documents immediately.
- `severity_overrides` config key (`[severity_overrides]` table in
  `.jss-lint.toml`): per-rule severity remap applied centrally in the
  engine so terminal/JSON/SARIF/LSP output and the exit-code policy
  all agree.

- Per-rule measured-precision **confidence tiers**. The catalogue now
  carries an optional `confidence` key (`high` default / `medium` /
  `low`) sourced from the eval-jss precision history; the four
  sub-90%-precision rules at iter-78 are tiered (`JSS-CAP-003` low;
  `JSS-CITE-002`, `JSS-CAP-002`, `JSS-MARKUP-001` medium). The tier
  surfaces in the terminal table (dim marker under the rule id), the
  JSON `confidence` field, and `jss-lint explain`. New
  `--min-confidence {low,medium,high}` flag (and `min_confidence`
  config key) skips rules below the floor, reporting them as skipped
  rules; default `low` runs everything.
- `--fail-on {error,warning,info}` flag (and `fail_on` config key):
  the minimum violation severity that exits 1. Default `info` keeps
  the historical behaviour (any violation fails); `--fail-on error`
  stops info/warning advisories (e.g. the missing-DOI rule) from
  flipping CI red while still reporting them. Parse errors always
  exit 2.
- Inline suppression: `% jss-lint: ignore [RULE-IDS]` on a finding's
  line (or on a comment-only line directly above it) silences matching
  findings in place, so one false positive no longer forces disabling a
  whole rule via `--ignore-rules`. Bare `ignore` suppresses every rule
  on the target line; trailing free text is treated as rationale; parse
  errors (`JSS-PARSE-000`) are never suppressed. Works in `.tex`,
  `.Rnw`/`.Rmd` LaTeX islands, and `.bib` files (a directive line above
  an entry covers findings reported on the entry's first line).
- `texlint.api.VERBATIM_ENVS` / `CODE_DISPLAY_ENVS` / `LISTING_ENVS`:
  shared contract for "this environment's body is not prose", consumed
  by both the parser's special-char neutraliser and the rule modules.
- `eval-jss`, a companion CLI for measuring per-rule precision of
  `jss-lint` against a real-world corpus. Implements Constitution §VI
  (≥90% precision per rule) as an enforceable gate and §XII (reproducible
  corpus) via a pinned `eval/corpus-manifest.csv` with SHA256 per
  paper. Full spec at `specs/002-eval-jss-harness/`.
- Subcommands: `init`, `scan`, `human-review`, `review` (AI-assisted),
  `report` (with `--csv` history and `--by-source` breakdown),
  `corpus fetch`, `corpus status`.
- Package layout: top-level `eval/`. Dependencies: stdlib + existing
  `click` + `rich` — no new runtime deps.
- AI review backend: pinned to `llama.cpp`'s `llama-server` hosting
  `unsloth/Qwen3-30B-A3B-GGUF:UD-Q4_K_XL` with greedy decoding (spec
  clarification session 2026-04-23).
- Phase A corpus: 3 placeholder vignettes under `examples/` exercising
  the `JSS-CITE-001` / `JSS-SRC-001` / clean code paths; the 10-paper
  real-CRAN corpus is planned follow-up work.

### Changed

- Internal: the fourteen per-module `_violation` and fifteen
  per-module `_rule` factory copies in `texlint.journals.jss.rules.*`
  are consolidated into shared catalogue-backed factories in
  `rules/_helpers.py` (`make_rule`, `tex_violation`, `entry_violation`,
  `make_violation`, `entry_line`). No behaviour change; net −280
  lines. New rules get severity/message/confidence wiring for free,
  and future cross-cutting changes (suppression, confidence) have one
  seam instead of fifteen.

### Fixed

- `JSS-STRUCT-005` no longer flags the literal word "and" inside
  `\author{}` when the block already separates authors with a
  `\and`/`\And`/`\AND` macro — in that case a literal "and" is part of an
  institution or name ("Computer and Information Science", "MIT and
  Harvard"), not a separator (recall-corpus opentsne false positives). A
  block with no macro separator still flags a literal "and" joining names.
- `JSS-OPER-002` no longer flags `\prime` used as **derivative** notation
  (`\h^\prime`, `\basisy^\prime`, `\bern{M}^\prime(\ry)`) as if it were a
  transpose. The `\prime`-macro branch fired unconditionally while the
  single-quote branch already exempted derivatives; it now matches that
  design — a `\prime` is treated as transpose only when it follows a
  closing bracket `)}]` (a grouped expression, e.g. `(6,7)^\prime`,
  `\mathbf{X}^\prime`) and is not immediately applied to an argument
  `(...)`. Real transposes still fire (recall-corpus mlt.docreg).
- `JSS-HOUSE-003` now handles a jss-loaded package loaded **with options**
  (`\usepackage[usenames,dvipsnames]{xcolor}`) differently from a bare
  redundant load. jss.cls loads these packages without options, so
  re-loading with options is an option clash, not a valid way to get them
  — the rule now advises moving them to `\PassOptionsToPackage{...}{pkg}`
  before `\documentclass` and **withholds the delete-the-line autofix**
  (deleting would silently drop the options and can break compilation).
  A bare `\usepackage{pkg}` (or empty `[]`) keeps the safe auto-delete
  (recall-corpus romc).
- `JSS-XREF-001` no longer flags a "Figure/Table N" that sits inside a
  citation locator (`\citet[Table 2.5]{X}` / `\cite[Figure 3]{X}`): the
  optional argument points at a float in the *cited* work, not this
  manuscript (recall-corpus HardyWeinberg false positives). Matches with a
  cite-macro ancestor are skipped.
- `JSS-CITE-003` no longer flags a lone `Author~(\citeyear{X})`: that's the
  legitimate narrative-citation idiom (author names in prose, year in
  parens), not a bracket-in-bracket. `\citeyear` was dropped from the
  trigger set; the hand-rolled `(\citeauthor{X} \citeyear{X})`
  reconstruction of `\citep` is still caught via the `\citeauthor` branch
  (recall-corpus HardyWeinberg false positives).
- `JSS-CAP-004` now also flags a `\Keywords{}` list whose **first** keyword
  starts with a lowercase letter — JSS keywords are sentence case, so the
  list's first word is capitalised (`ternary plot, …` → `Ternary plot, …`;
  recall-corpus HardyWeinberg). Previously the rule only caught the
  opposite direction (a non-first word in title case). A first keyword
  wrapped in markup (`\pkg{}`, `\proglang{}`, `\code{}`) or a known
  package/language name keeps its own lowercase case and is exempt.
  `\Plainkeywords{}` is deliberately *not* checked — it is PDF metadata the
  reader never sees.
- `JSS-XREF-004` exempts equations carrying `\tag{}` / `\tag*{}`: a `\tag`
  replaces the automatic number with a custom label (e.g.
  `\tag{\texttt{approx()}}`), so the equation isn't a standard
  auto-numbered cross-ref target — the same reasoning as the existing
  `\nonumber` exemption (recall-corpus trueskill false positives).
- `JSS-OPER-003` no longer flags the blank line between a display equation
  and a following sectioning command (`\section` / `\subsection` / …): a
  blank line before a heading is required structure and can't be
  `%`-suppressed like a prose paragraph break (recall-corpus trueskill).
- `JSS-OPER-003` now also checks `\[ … \]` / `$$ … $$` display math for
  blank lines before/after. These parse as a display-math node (not an
  environment), so the rule skipped them entirely (recall-corpus deSolve
  false negatives).
- `JSS-XREF-004` now checks each label in a multi-line equation
  environment (`align` / `eqnarray` / `gather`) independently: those envs
  number every line, so an orphan numbered line is a defect even when a
  sibling line *is* referenced. The old per-environment "any label
  referenced" test missed these (recall-corpus romc `eq:1D_example`).
  Envs containing `\nonumber`/`\notag` fall back to the conservative
  per-env check to avoid flagging a label on an unnumbered line.
- `JSS-MARKUP-001` no longer flags the emphasised first letter of a word
  as a language name: `\emph{C}ombination`, `\textbf{S}helter` (the
  acronym typesetting device, e.g. CUB/CUSH) read the `\emph{C}` as the C
  language. It's skipped when a single-letter language token sits alone in
  an emphasis macro whose closing brace is glued to a lowercase letter; a
  standalone `\emph{C}` (space/punctuation after) still fires
  (recall-corpus CUB false positives).
- `JSS-CODE-003` now flags missing spaces around R's multi-character
  assignment operators `<-`, `->`, and `<<-` (e.g. `x<-coef(y)` →
  `x <- coef(y)`). The missing-space matcher only recognised single-char
  operators, so the `<` broke the ident-operator-ident pattern and glued
  assignments slipped through entirely (recall-corpus CUB false
  negatives). Comparison operators (`==`, `<=`, …) remain a follow-up.
- Markup / prose rules no longer fire inside `lstlisting`, `alltt`,
  `tabbing`, and `verbatim*` bodies. The parser's neutraliser and the
  rules' non-prose check had drifted into two different environment
  lists; both now consume the shared `texlint.api.VERBATIM_ENVS`
  contract. Before the fix, `jss-lint --fix` would even rewrite code
  inside an `lstlisting` (e.g. `library(zoo)` → `library(\pkg{zoo})`).
- `JSS-CAP-001` now learns the paper's own package name from the
  document's `\pkg{...}` usage instead of a filesystem-path heuristic
  that only matched the eval corpus's `cran_<name>/vignettes/` layout;
  titles following the JSS convention (`\title{flexsurv: A Platform
  for ...}`) are no longer flagged on real submissions.
- One crashing rule no longer aborts the whole run with no output: the
  engine isolates per-rule exceptions, reports the rule as skipped
  (`internal error: ...`, visible via `--verbose`), and keeps the
  remaining rules' findings.
- `eval/review.py` now builds a `±3`-line source snippet per violation and
  passes it as `paper_context` to the `ReviewClient`, rather than sending
  an empty string. Observed effect: AI precision on the canonical JSS
  template's `JSS-SRC-001` violations improved from 86% to 57%, moving
  measurably toward the human ground-truth of 11%. Residual gap is the
  AI blind spot documented in `eval/review-skip-list.toml` and
  `specs/002-eval-jss-harness/spec.md`'s Assumptions section.
- `eval-jss review` now fail-fast-exits 2 with a diagnostic message on
  first-call network errors, per `contracts/review-client.md`. Earlier
  behaviour silently degraded every row to `uncertain`, which made a
  mis-pointed `--base-url` (or a down server) invisible to the operator.

### Packaging note

`eval-jss` is registered as a console script. The `eval` Python module
is included in the wheel; this differs from the plan's original
"`eval/` deliberately outside the wheel" design decision. Reason:
Hatchling's editable install only exposes `wheel.packages`, so
excluding `eval/` from the wheel produced a broken `eval-jss` binary.
Small wheel bloat accepted in exchange for a working end-user install.

## [0.1.0] — 2026-04-22

First foundation release. Framework + smoke-test rule set.

### Added

- Public data model in `texlint.api`: `Violation`, `Rule`, `RuleCategory`,
  `CategorySummary`, `ComplianceReport`, `ToolConfig`, `ParsedTexFile`,
  `ParsedBibFile`, `ParsedDocument`, `JournalRuleModule` (ABC), `FixSuggestion`
  (reserved, unused until Step 4), `Severity`, `CategoryStatus`,
  `JournalNotFoundError`, `InvalidJournalError`.
- Core parser `texlint.core.parser` with non-raising `.tex` / `.bib` parsing;
  non-UTF-8 and LaTeX / BibTeX failures surface as `JSS-PARSE-000`
  violations on the returned object.
- Rule engine `texlint.core.engine` with `importlib.metadata`-based journal
  loading and compliance-percentage derivation (excludes `SKIPPED` and the
  synthetic `parse` category).
- Config loader `texlint.config` merging built-in defaults, `.jss-lint.toml`,
  and CLI flags in that precedence.
- CLI entry point `jss-lint` (via `click`) with `--journal`, `--mode`,
  `--output`, `--ignore-rules`, `--verbose`; exit codes `0` / `1` / `2`.
- Output renderers: terminal (`rich`, author + reviewer modes), JSON
  (byte-deterministic), HTML (Jinja2 with packaged templates).
- Journal plugin: `jss` registered via the `texlint.journals` entry-point
  group. Smoke rules (each with 100% branch coverage per Constitution §IX):
  - `JSS-CITE-001` — `\emph{bibkey}` used for citation markup.
  - `JSS-BIB-001` — bibliography entry missing a `year` field.
  - `JSS-SRC-001` — source line exceeds `code_width` (default 80).
- Test fixtures under `tests/fixtures/compliant/` and
  `tests/fixtures/violations/`, plus a second-journal
  `tests/fixtures/stub_journal/` package proving zero-core-edit extensibility
  (Constitution §IV).

### JSON output contract

Top-level keys in the `--output json` payload — `tool_version`,
`journal_id`, `compliance_percentage`, `categories`, `violations` — are
**additive-only** within a single major version. Adding fields is a minor
version bump; removing or renaming fields requires a major version bump and
an entry in this file.

### Deferred

- `.Rnw` / `.Rmd` dispatch — Step 3.
- `--fix` / `--dry-run` and the `FixSuggestion` payload fields — Step 4.
- Full 53-rule JSS catalogue — Step 2.
- `eval-jss` precision-evaluation CLI and the ≥90% precision gate
  (Constitution §VI) — Step 5.
