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