# 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. ## [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_/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.