| F22 | Rules refinement | 🚧 | v0.2.x → v0.3 | First slice shipped in v0.2.x — structure.excessive-commas now discounts commas inside (A, B, C, …) parenthesised token lists (3+ short comma-separated segments inside balanced parens, language-agnostic). Sibling helper parenthesised_list_comma_count in src/rules/enumeration.rs. Dogfood drops from 25 → 15 hits (10 FPs killed, ~40% reduction). Deferred to v0.3: relaxing MAX_SEGMENT_WORDS = 2 for 3–4-word Oxford items, non-Oxford / “plus”-closed lists, interleaved parentheticals inside Oxford runs. See research note in .personal/research/[F22](#f22).md. |
| F-example-fixtures-part2 | Example fixtures | 🚧 | v0.2.x | Desired-fixture-shapes coverage table + replacements for high-value local-only entries. Part 1 — coverage tables: ✅ Shipped (2026-04-23) — scripts/texts_coverage.py splits output by audience: the committed examples/texts.md shows public_ok counts only (no totals, no names that would leak local-only existence), spliced between <!-- coverage:begin/end --> markers; the gitignored examples/local/COVERAGE.md carries the full matrices plus the load-bearing local-only list. Wired as just texts-coverage / just texts-coverage-check. Part 2 — replacement hunting: 🟡 In progress. First addition (2026-04-25): a French government FALC source under Etalab Open Licence 2.0 — knock-on lifted aphasia × FR and gov_guide × FR out of 0 / N ⚠. Second addition (2026-04-27): three US-federal public-domain ADHD sources — NIMH ADHD topic page (mixed shape, ~780 words), CDC About ADHD (good, ~920 words), CDC Treatment of ADHD (good, ~1040 words). All three covered by the explicit reproduction notices in NIMH and CDC reuse policies (17 USC § 105 + agency policy pages). Knock-on: adhd × EN lifted from the load-bearing list; public-coverage gov_guide × EN and condition adhd × EN rise to non-zero counts. Remaining load-bearing slots: dyscalculia × EN (one BDA link_only) and aphasia × EN+FR (three plain-language standards as link_only). |
| F-docs-responsive | Docs — reading prefs | ☐ | v0.2.x | Responsive / mobile adaptation — right-rail page TOC and header controls collapse gracefully below 700 px; touch targets verified ≥ 44 × 44 px; sidebar drawer behaviour polished. |
| F-experimental-rule-status | Architecture | ☐ | v0.2.x | Experimental rule status — registry substrate for the v0.3 cohort. Routed 2026-05-02 (.personal/brainstorm/20260502-v03-breaking-change.md). Soft-breaking changes (new default-active rules) are the SemVer-major signal for linters; lucid-lint has 5 such rules queued for v0.3 (F46 / F49 / F51 / F53 / F57). Rather than smear 5 score regressions across v0.2.x patches or hold all 5 until a single v0.3 cut, this entry adds a rule lifecycle status (Stable / Experimental) and ships the cohort in v0.2.x as Experimental (off by default). Users — including this repo’s own dogfood loop on adjacent projects — opt in via a [experimental] config section (enabled = ["structure.italic-span-long", …] or enabled = "*") or --experimental <id> CLI flag. v0.3’s breaking change is then a single-line per rule (Status::Experimental → Status::Stable) plus a CHANGELOG cohort entry. Why this shape, not per-rule default = false knobs: Status is one concept that maps to a known industry pattern (clippy nursery, biome nursery, ESLint experimental rules, rust #[unstable]); per-rule booleans would add five toggles for the same concept and pre-figure no lifecycle. Minimal viable substrate (resist gold-plating): Status enum on the Rule trait (default Stable); default_rules() filters Experimental unless config opts in; [experimental] TOML section parsing; --experimental CLI flag (multi-occur + *); experimental tagging visible in --list-rules output; one snapshot test for the experimental-off vs experimental-on diff. No rule-group / preset / category-toggle machinery yet — the biome-style recommendedRules preset is filed as a v0.4 question. Estimated effort: half a day for the substrate, then one line per rule once F49 / F51 / F53 / F57 ship on top of it. F46 keeps its original FR-corpus slip-flag (independent of the experimental status). |
| F-project-scoring-rollup | Architecture | ☐ | v0.2.x | Document-level scoring shipped in v0.2 (multi-path runs are aggregated as one document). Project-level roll-up (per-file breakdown + project summary) still open. Section-level deferred → F-section-scoring. |
| F-redundant-intensifier-bullet-fix | Rules refinement | ☐ | v0.2.x | lexicon.redundant-intensifier parser miss inside bullet items / **strong** spans. Routed 2026-05-02 (.personal/brainstorm/20260502-async-book-pr-timing.md); blocks the async-book audit-and-PR play (tracked in .personal/promotion-channels.md). Surfaced while linting rust-lang/async-book/src/why_async.md: very inside - **OS threads** are very … (bullet + strong span) does not fire, while highly in a flat paragraph does. Same family of misses as F-tight-list-paragraphs — paragraph-level rules go silent when the surrounding event is Tag::Item with no enclosing Tag::Paragraph. F-tight-list-paragraphs is the right substrate to fix once for every paragraph-level rule; this entry is the verification slice that pins the regression for redundant-intensifier so the case cannot regress when F-tight-list-paragraphs lands. Definition of done: corpus fixture tests/corpus/en/redundant-intensifier-bullet.md + FR mirror, snapshot covering very / highly / really inside - **strong** ... and * **strong** ... shapes, comment in the test linking to F-tight-list-paragraphs so the slot stays after F129 lands, CHANGELOG entry. |
| F-release-policy-codify | Architecture | ☐ | v0.2.x | Codify release policy — rule lifecycle + patch cadence in RULES.md / CONTRIBUTING.md / CHANGELOG.md / README.md. Pairs with the v0.3 cut: the first Experimental → Stable flip lands under this written policy instead of ad-hoc debate. Lifecycle: patch may add Status::Experimental rules (opt-in via --experimental / F-experimental-rule-status, never fire on default); minor may flip Experimental → Stable gated on dogfood pass on examples/public/ + per-rule CHANGELOG entry (rule ID, severity, opt-out); major bump required to remove a default-fire rule or to raise default severity to a stricter level; adding new default-fire rules in a minor stays acceptable (Cargo precedent — analogous to new compiler warnings). Cadence: trigger-based — cut a patch when user-facing work hits main (bug fix, perf win, UX/TTY/render change, security fix); chores + new Experimental rules + docs ride along; quiet periods are normal. Branch: feat/release-policy-codify. Includes a “Rule lifecycle changes” template section in CHANGELOG.md, a perf-win threshold (initial proposal: ≥ 5 % wall-clock on the bench corpus), a “new dependency” classification (chore unless runtime behavior changes on default config), and a README note so quiet release periods don’t read as abandonment. Spawned F-rule-removal-policy (deprecation/removal mechanics, promote on first real candidate) and F-config-pinning (rule-set pin for strict-CI users, promote on first churn report). |
| F-repo-discoverability-polish | Adoption channels | ☐ | v0.2.x | Repo discoverability polish — README badges + GitHub social preview. Two adjacent first-impression surfaces, bundled because they share the audience (drive-by visitors on crates.io, link unfurls on Mastodon / LinkedIn / HN). Checklist: (1) README badge row at the top of README.md: crates.io version (img.shields.io/crates/v/lucid-lint), docs (mdBook URL), CI (ci.yml badge), license. Standard Rust crate signal — first thing crates.io users scan to gauge a project. (2) Social preview image uploaded under Settings → General → Social preview (1280×640, 40pt safe zone per GitHub template). Replaces the auto-generated avatar+name card that GitHub serves on Twitter / Mastodon / LinkedIn / Slack / HN unfurls. Two viable directions — pure brand card (wordmark + tagline), or terminal-screenshot card (real lucid-lint diagnostic on a real sentence, more “shows what it does”). Pick at design time; brand assets in .impeccable.md. Both items are non-breaking, no code touched — fits any v0.2.x patch slot or rides alongside an unrelated PR. |
| F-report-quick-wins | Reporting / DX | ☐ | v0.2.x | TTY report quick-wins block — actionable hint hooks under the diagnostic list. Routed 2026-05-03 from Block C of .personal/2026-05-03-today.md (originally surfaced in the 2026-05-02 deferred buffer). Dogfood loops on this repo and adjacent docs surfaced the gap: high-density runs already produce a complete summary (score line + per-category breakdown + diagnostic list), but the next action a user can take is buried inside the list. This entry adds a small “quick wins” block rendered after the diagnostic list, TTY only in v0.2.x, with two seed shapes both grounded in observed dogfood patterns: (1) Acronym whitelist hint — when ≥ 3 occurrences of lexicon.unexplained-abbreviation share one token, surface → add "X" to whitelist (N hits suppressed) (one line per acronym, top 3); (2) Single-rule hot-spot hint — when one rule fires ≥ 10 times in one file, surface → <rule-id> dominates this file — see <docs URL> (one line per rule per file). Why a single ROADMAP entry, not two: one shape (a quick-wins reporter), two seed heuristics that share the threshold-based fire rule, the TTY-only render path, and the test pattern; a third heuristic (e.g. category dominance) earns its own line later without new scaffolding. Threshold heuristics (sketch, finalised in PR): acronym hint requires ≥ 3 hits sharing a token; hot-spot hint requires ≥ 10 hits of the same rule in one file. Each hint is one line; the block caps at ≤ 5 lines so it never crowds the score banner. Output surfaces: TTY only in v0.2.x; JSON / SARIF stay structural (separate ROADMAP entry if a CI consumer asks; no current ask). Definition of done: new report::quick_wins module (or extension of the existing TTY renderer) with the two seed hints wired, snapshot tests pinning the fires-vs-silent path for each shape, threshold parameters live next to the hint definition (no central config knob until a second consumer asks), CHANGELOG [Unreleased] entry. Non-breaking: purely additive output below the diagnostic list, no JSON / SARIF schema change, no scoring change, no new CLI flag (existing --quiet already suppresses TTY decoration if a consumer wants the bare list). |
| F-roadmap-slug-ids | Architecture | ☐ | v0.2.x | ROADMAP feature IDs adopt F-<kebab-slug> form for all new entries. Routed 2026-05-02 (.personal/brainstorm/20260502-roadmap-id-attribution.md). Numeric F<n> IDs collided when two branches independently picked the same free number; reservation-on-main was ruled out because new features are routinely discovered mid-implementation inside an existing feature branch. Decision: new ROADMAP entries use a slug-as-ID form (F-inline-ast-substrate); legacy F1–F146 stay numeric (no migration — Devil’s-Advocate verified no programmatic parser in src/, tests/, scripts/, .github/, justfile depends on the format; mixed taxonomy is cosmetic). Slugs are coined locally with no coordination. The cross-branch race that survives (two offline branches independently coining the same slug) is detected at PR time and resolved by a one-line slug rename in ROADMAP + CHANGELOG — no branch rename, no rebase, no commit-history rewrite, because the new convention drops the F- prefix from branch names and commit subjects. Minimal viable substrate: (a) tests/roadmap_id_uniqueness.rs parses ROADMAP.md + CHANGELOG.md and asserts every F-<slug> appears uniquely as a definition site, no slug shadows the legacy F<number> namespace, and every referenced [F-foo](#f-foo) resolves; runs offline via cargo test, re-runs in CI as a backstop. (b) Explicit `` anchors on first definition (matches the existing convention for numeric IDs). (c) F- prefix becomes optional in branch names and commit subjects — branches use plain feature slugs (feat/<slug>), commits use scope syntax (feat(parser): <subject>). Surfaces touched: tests/roadmap_id_uniqueness.rs (new), AGENTS.md Conventions section, CHANGELOG.md [Unreleased], this entry itself (first dogfood). Reversibility: if the mixed taxonomy ever bites (it shouldn’t — no parser depends on it), a one-shot rename script could fold slug entries into a numeric scheme at any future v0.x cut. Estimated effort: ~1 h total — uniqueness test (30 min), AGENTS.md update (15 min), this ROADMAP wiring (15 min). |
| F-severity-floor-flag | Suppression / config | ☐ | v0.2.x | --severity-floor=warning CLI flag. Routed 2026-05-02 (.personal/brainstorm/20260502-async-book-pr-timing.md); supports the async-book audit-and-PR play (tracked in .personal/promotion-channels.md). Need: external audit PRs (async-book and adjacent) want a “narrow audit” mode that drops info diagnostics from output and from score impact, so the PR demonstrates value on the unambiguous wins (sentence-too-long, redundant-intensifier, unclear-antecedent, paragraph-too-long) without the contested ones (info-tier weasel words after F-weasel-words-severity-tiering lands). Shape: --severity-floor={info,warning,error} with default info (current behavior). Pairs with F-weasel-words-severity-tiering: once weasel-words emits info on quantifiers, an auditor running --severity-floor=warning ships a PR where reviewers see only the prose changes the tool is most confident about. Implementation is a post-engine filter (mirrors F19 [[ignore]] post-engine pre-scoring shape) so JSON / SARIF / TTY all see the same filtered view; scoring excludes filtered diagnostics so --min-score interacts correctly. Definition of done: CLI flag in src/cli.rs, filter in src/engine.rs post-rule pre-score, two snapshot tests (info-included default vs warning-floor), docs in docs/src/guide/configuration.md + FR mirror with a “running a narrow audit on someone else’s repo” worked example, CHANGELOG entry. |
| F-weasel-words-severity-tiering | Rules refinement | ☐ | v0.2.x | Severity tiering for lexicon.weasel-words. Routed 2026-05-02 (.personal/brainstorm/20260502-async-book-pr-timing.md); blocks the async-book audit-and-PR play (tracked in .personal/promotion-channels.md). The current rule fires uniform warning on every entry in the EN/FR weasel-list, conflating two distinct linguistic functions: quantifiers (some, many, often, most, several) which are legitimate technical hedging in reference docs, and hedges (a bit, just, quite, rather, pretty, kind of) which signal under-confident prose. Stripping all of them in a Rust async-reference produces prose reviewers reject as artisan (“over-edited” — surfaced in .personal/f113-async-book/READABILITY_REVIEW.md). Fix: split WEASEL_WORDS_EN / _FR into two sub-lists, emit Severity::Info on quantifier hits and Severity::Warning on hedge hits. Per-rule TOML override stays available for users who want stricter / looser bands. Surfaces the pattern other lexical rules can adopt later (no architectural lift; a per-match severity decision inside the rule body). Definition of done: split lists in src/language/{en,fr}/weasel.rs, severity routing in src/rules/lexicon/weasel_words.rs, snapshot regen for both languages, docs page (docs/src/rules/weasel-words.md + FR mirror) describing the two bands and the rationale, CHANGELOG ## [Unreleased] entry. Pairs with F-severity-floor-flag: once --severity-floor=warning exists, an external auditor running on a Rust-reference repo gets the “no contested edits” view in one flag. |
| F143 | Architecture | ☐ | v0.2.x | Inline AST layer over pulldown-cmark — substrate for inline-positional rules. Routed 2026-05-02 (.personal/brainstorm/20260502-parser-substrate-choice.md). The current Markdown parser at src/parser/markdown.rs flattens emphasis, strong, and link spans into the Paragraph.text string before rules see it — visible text preserved, structure lost. F49 (structure.italic-span-long, cohort lead) needs italic-span boundaries; future inline-positional rules (F-paragraph-landmark-density speculative, F-lexicon-acronym-distance conditional on F9) would hit the same wall. Decision: introduce a thin typed inline AST on top of pulldown-cmark, not swap the engine for comrak / markdown-rs. Reasons: pulldown stays the perf-leading parser; the AST is the domain model the rules walk (CUPID-aligned: composable, predictable, domain-based); the engine swap regresses bench by ≈ 2–3× and collides with the lightning-fast positioning pillar. Minimal viable substrate (YAGNI applied inside the layer): enum Inline { Text(String), Emphasis(Vec<Inline>) } plus a Paragraph.inline: Vec<Inline> field captured during the existing pulldown walk. Not modeled yet: Strong, Link, Code, footnotes, task-list markers, hard breaks inside emphasis. Each gets added when a second rule actually demands it; today only F49 does, and the steel-man check confirmed the cohort is non-uniform (F51 / F53 / F57 don’t need inline spans). Plain-text parser path: empty inline vec (no Markdown semantics). Estimated effort: half a day for the substrate, then F49 ships on top in a follow-up PR. Bench gate: PR-1 against the existing bench corpus; > 5 % regression triggers a profile pass before merge. Reversibility: the layer can be deleted and folded back into per-rule fields if a year passes with one consumer; the engine swap (comrak) was rejected partly because that reversal is much harder. |
| F35b | Docs — reading prefs | ☐ | v0.2.x | Drop role="radiogroup"/role="radio" on reading-demo chips (P2 from F-a11y-audit-sweep audit). Current markup declares radiogroup semantics but the JS only binds click — arrow-key traversal is missing, so the ARIA contract is broken. Simpler fix is to switch to plain buttons with aria-pressed (the chips are preset toggles, not radios) rather than add a keyboard handler. Promoted to 🔴 Next on 2026-04-24 (brainstorm-next-cycles). |
| F-a11y-audit-sweep | Docs — reading prefs | ☐ | v0.2.x | Accessibility audit sweep — full AAA pass on both themes (contrast, focus order, prefers-reduced-motion coverage, keyboard-only walk-through, skip-link), plus a published accessibility statement page. First audit pass ran 2026-04-22 (17/20, 0 P0, 2 P1, 3 P2); findings filed as F35a–F35d below. F-a11y-audit-sweep stays open until the statement page ships and P1s are cleared. |
| F-adversarial-review | Architecture | ☐ | v0.2.x | Adversarial PR review — bot second pair of eyes. While the repo stays solo-maintainer, every PR is self-authored and self-reviewed; OSSF Scorecard Branch-Protection plafonds at 4/10 (see F-repo-config-hardening). An adversarial bot reviewer on each PR adds real signal without faking human-review-as-gate. Two complementary tracks: (1) LLM review — Claude Code or Gemini Code Assist via GitHub integration, comment-only mode, included in existing subscriptions; catches semantic and context-aware issues. (2) Rule-engine review — Semgrep, custom CodeQL queries, cargo-deny, cargo-audit, danger.js, or similar, run as required PR checks; deterministic, complementary to LLM, catches structural issues (unsafe patterns, license drift, CHANGELOG gaps). Hard scope rule: review-only, no auto-approve — auto-approve crosses into Scorecard theatre and is explicitly out. Reassess if a co-maintainer joins or a release-managers group is added. Status 2026-05-07: Track 1 calibrating — Gemini Code Assist installed (review-only by App scope; auto-approve structurally impossible), 4-week window → 2026-06-04 with action-rate tripwires. Track 2 mostly shipped — cargo-deny + actionlint live, CodeQL pre-existing via F-repo-config-hardening, Semgrep parked as F-semgrep-rust pending first concrete missed-pattern signal. |
| F-code-block-without-lang | Docs plugin | ☐ | v0.2.x | Rule 8 dropped from v0.1, candidate for lucid-lint-docs plugin |
| F-config-whitelist-normalize | Performance / hygiene | ☐ | v0.2.x | Config whitelist normalization at load time. src/config.rs — normalize (trim, case-fold per rule needs) on load instead of per invocation; catches user typos early. Small win; fits a v0.3 config-plumbing pass rather than a 0.2.x patch. |
| F-docs-codegen | Docs — content | ☐ | v0.2.x | Code → docs codegen for data-heavy surfaces. Several docs surfaces are hand-maintained today but derivable from the rule registry and config types: per-rule pages (defaults, weight, condition tags, category, severity), docs/src/rules/index.md table, docs/src/guide/profiles.md threshold tables, docs/src/guide/conditions.md tag list, docs/src/guide/suppression.md directive list, JSON output schema page. Proposed shape: a lucid-lint manifest --format=json subcommand emits one document with everything pulled from default_rules, Category::for_rule, scoring::default_weight_for, the Condition enum, profile presets, and schemars::JsonSchema derives. A just docs-gen script renders marked regions in existing prose pages (<!-- BEGIN: lucid-gen rule-defaults id=structure.sentence-too-long lang=en --> … <!-- END: lucid-gen -->) so prose around the data stays hand-authored. CI runs just docs-gen and fails on non-empty git diff (same shape as F27 for the roadmap sync). Translation surface shrinks to prose only — labels (Default, Profile, Threshold) come from a small i18n.toml keyed by (lang, key), the data is identical across languages by construction. Block on F25 guide translations landing first so we don’t change the substrate mid-translation; open after Block C closes. |
| F-docs-final-polish | Docs — reading prefs | ☐ | v0.2.x | Final polish pass — optical alignment, spacing rhythm, edge-state copy, favicon PNG fallback, social-card refinement, re-running /critique to verify the score moves above 30/40. |
| F-docs-i18n-substrate | Docs — bilingual | ☐ | v0.2.x | Docs i18n substrate evaluation (Starlight vs Sphinx). mdBook is a twin-tree at the file level with a post-build hreflang patcher (F92); it cannot deliver page-keyed translations or identical section numbering across languages by construction. A real i18n model needs either route-keyed translations (Astro Starlight: defaultLocale + locales, language dropdown built in, Markdown sources kept) or message-catalogue translations (Sphinx + sphinx-intl / gettext PO files: FR is the same file with strings substituted, headings and numbering identical by construction; weblate-style flow). Don’t migrate now — F92 + F25 + the F92 staleness sub-task carry through v0.2.x. Migration triggers (any one): (a) a third language is requested (Spanish or German via the EU disability-federation play, F-falc-readiness-guide); (b) docs surface crosses ~50 pages; (c) contributors complain about FR/EN drift after the staleness gate is in place. Default pick on trigger: Starlight (lightest migration, keeps Markdown). Sphinx only if RGAA-mandated structural parity becomes a contractual requirement. Placeholder entry — no work scheduled. |
| F-doctest-entrypoints | Docs.rs polish | ☐ | v0.2.x | One runnable doctest per major entry point (Engine::with_profile, Engine::lint_str, Report field access, key Profile variants). /// blocks render as code samples on docs.rs and run under cargo test --doc, so they cannot rot. ~5 lines each. Lifts the API page from “list of names” to self-explanatory reference. Renumbered from F131. |
| F-docx-support | Formats | ☐ | v0.2.x | .docx support via Pandoc integration |
| F-excessive-nominalization-suffix-refine | Rules refinement | ☐ | v0.2.x | Refine lexicon.excessive-nominalization suffix list (drop or gate -al; many adjectives — crucial, horizontal, positional, attentional — are flagged despite not being abstract nouns) |
| F-fail-on-warning-bool | v0.2.0 | ☐ | v0.2.x | --fail-on-warning accepts optional boolean; hidden mirror --no-fail-on-warning. --min-score now testable in isolation on documents with warnings. |
| F-fix-mode | Quality | ☐ | v0.2.x | --fix mode for the mechanical subset of rules — promoted to 🟡 Later on 2026-04-24 (brainstorm-next-cycles, 0.3 Should). Narrow scope locked: lexicon.all-caps-shouting (lowercase the run), lexicon.redundant-intensifier (drop the intensifier), structure.mixed-numeric-format (normalise to the detected majority style), structure.line-length-wide (rewrap to max_chars). All other rules stay report-only — cognitive-load judgments need the author to choose the rewrite. Borderline structure.heading-jump stays out of the initial cut. Design: per-rule fixable: bool metadata on the Rule trait, --fix flag walks diagnostics in document order applying only those with concrete replacements, writes files in place (or emits a unified diff with --fix=print), exits with count of fixes applied. Conservative default: --fix only touches the explicitly-fixable set, never guesses. |
| F-html-support | Formats | ☐ | v0.2.x | Relevant for EAA compliance |
| F-low-diversity-stoplist | Rules refinement | ☐ | v0.2.x | Custom stoplist parameter for lexicon.low-lexical-diversity |
| F-missing-connectors | Rules refinement | ☐ | v0.2.x | missing-connectors rule (15b not shipped in v0.1) |
| F-pandoc-companion | Formats | ☐ | v0.2.x | Companion script pandoc → lucid-lint |
| F-per-family-subscores | Architecture | ☐ | v0.2.x | Per-family sub-scores |
| F-proptest-invariants | Quality | ☐ | v0.2.x | Property-based tests via proptest (dep already in [dev-dependencies], zero call sites today — paid for, unused). Four invariants in tests/properties.rs, deliberately small: (1) split_sentences never drops a non-whitespace character on round-trip, (2) re-linting an identical string yields identical diagnostics (engine idempotence), (3) for threshold-driven rules, public-profile diagnostics are a superset of dev-doc-profile diagnostics on the same input (profile monotonicity), (4) Engine::lint_str never panics on arbitrary valid UTF-8 ≤ 10KB. Goal: fortify tokenizer / engine seams, not rewrite the suite. |
| F-public-api-audit | Docs.rs polish | ☐ | v0.2.x | Public-API audit with cargo public-api: surface candidates that should carry #[doc(hidden)] (re-exports for macros, internal helpers leaked via pub) so the rustdoc index reflects the intended surface, not the current surface. Pair with a CI gate later if the surface becomes load-bearing for SemVer. Renumbered from F132. |
| F-reading-prefs-popover | Docs — reading prefs | ☐ | v0.2.x | Full reading-preferences popover UI — cog button in the header opens a popover with font radio (Atkinson / Standard / OpenDyslexic), line-spacing slider (1.4–2.0, 0.05 step) and text-size slider (90–130 %, 5 % step). v0.1 ships only the Introduction-page demonstrator; the CSS-variable plumbing (--reading-scale, --reading-line-height, [data-font]) is already in place, so this is UI work only. |
| F-rule-fixture-coverage-map | Example fixtures | ☐ | v0.2.x | Bidirectional rule ↔ fixture coverage map. Generate examples/COVERAGE.md from each content.md’s rules_relevant frontmatter, rendered as two views: rule → fixtures that exercise it (surfaces under-fixtured rules) and fixture → rules it covers (surfaces untagged or mis-tagged fixtures). Once stable, embed or link the canonical fixture per rule from docs/src/rules/<rule-id>.md. Optional follow-up: calibrated snapshot tests that lock expected lint output per canonical fixture. |
| F-rule-mention-coverage-test | Docs — content | ☐ | v0.2.x | Coverage test for F-rule-mention-linking rule-mention linking — assert each rule id mentioned in docs/src/**/*.md is linked on first-per-section occurrence. Follow-up from F-rule-mention-linking. |
| F-rule-mention-linking | Docs — content | ☐ | v0.2.x | Audit every rule mention across the docs and link it to its reference page (F28). Requires F28 to land first. References-page surface (rule IDs in → Relevant to: lines + rule → reference summary table) covered by F105b 2026-04-27; remaining surface is rule mentions in docs/src/guide/* prose pages, RULES.md, and the introduction. |
| F-score-letter-grade | Architecture | ☐ | v0.2.x | Letter-grade decoration (A–F) on the X/max score — promote when user feedback shows the numbers feel noisy or hard to compare across docs. |
| F-score-traffic-light | Architecture | ☐ | v0.2.x | Traffic-light (🔴🟡🟢) + pass/fail margin in the TTY output — promote when CI users ask for a stronger glance signal than the number alone. |
| F-section-scoring | Architecture | ☐ | v0.2.x | Section-level granularity for scoring (deferred from F-project-scoring-rollup) — per-heading sub-scores once document + project are proven in the wild. |
| F-suppression-disable-file | Suppression / config | ☐ | v0.2.x | File-level directive (disable-file) and multi-rule lists |
| F-suppression-reason-field | Suppression / config | ☐ | v0.2.x | reason="..." field, optional in v0.1, surfaced in reports and optionally required via config |
| F-terminal-demo-a11y | Docs — reading prefs | ☐ | v0.2.x | Terminal-demo accessibility — keep VHS, add motion + transcript fallbacks. Audited VHS (charmbracelet/vhs, active 2026-04-27, headless+CI-reproducible) vs. terminalizer (~16k stars, last commit 2024-08-29, effectively unmaintained). Verdict: keep VHS — .tape files are text-diffable, the build is reproducible, and the motion-handling problem is the same on both tools, so it is not a recorder choice but a wrapping problem. AAA gap to close: every embedded GIF on the docs site (today: docs/src/assets/tty/explain.gif plus future captures) must (1) honour prefers-reduced-motion — browsers do not pause animated GIFs automatically, so a static <picture> source-set with a still PNG fallback served when (prefers-reduced-motion: reduce) is the right shape; (2) carry the per-step transcript inside the page so non-sighted, screen-reader, and reduced-motion readers reach the same content as motion viewers — a stepwise prose block (e.g. <details><summary>Transcript</summary>…</details> with each tape command + its visible output as a list) sitting next to the GIF, plus an alt= summary on the image itself. The .tape source already encodes the steps deterministically — a small generator can emit the transcript from the same file the GIF is built from, keeping motion view and transcript view pair-locked. Phase: v0.3 marketing. |
| F-text-before-after-refine | Example fixtures | ☐ | v0.2.x | Refine texts_convert._split_before_after. The current heuristic looks for literal ## Before / ## After (EN/FR) headings; no upstream page in the current batch uses that shape, so every before_after source fell back to a single content.md with a warning. Replace with a per-source pair-extraction rule (plainlanguage.gov, EC How to write clearly, Canada.ca, OneStopEnglish, ASSET, Inclusion Europe) that emits before.md + after.md. |
| F-text-source-adapters | Example fixtures | ☐ | v0.2.x | Per-source adapters for git-cloned upstreams. The generic clean / convert path doesn’t know how to extract text from shallow-cloned repos (proselint checks, Vale style packs, write-good / alex / retext / textlint-rule fixtures, ASSET / OneStopEnglish / EASSE / CLEAR-corpus datasets). Each needs a small extractor that walks the repo and emits one or more .md files per rule / excerpt. |
| F-texts-yaml-url-maintenance | Example fixtures | ☐ | v0.2.x | Maintenance pass on examples/texts.yaml URLs. 12 sources failed on the first batch — 404s from moved landing pages (canada.ca × 2, BDA Dyslexia, Center for Plain Language, Newsela, HuggingFace wiki_auto), UA-/bot-blocking (Légifrance 403, Orthodidacte 403, ADHD Foundation 400), and a DNS error for the specific 18F post. Audit and update entries; for sources that genuinely require a browser-flavoured UA, add a per-source override in the fetcher. Fold in the opportunistic hygiene tasks from the 2026-04-23 brainstorm: (a) dedupe overlapping canada.ca / plainlanguage.gov entries, (b) add a licence-drift guard that flags when a source’s redistribution changes between fetches. |
| F29 | Docs — content | ☐ | v0.2.x | Rule ID harmonisation. F29-slim ✅ shipped 2026-04-22 in v0.2.0: the 25 rule IDs now use category.rule-name form (structure.excessive-commas, lexicon.weasel-words, readability.score, …) and rule source files moved into category subdirectories under src/rules/<cat>/. Category::for_rule derives the category from the id prefix rather than a hand-maintained match arm (F43-style drift now impossible by construction). Hard break — suppression directives, [rules.<id>] TOML keys, JSON/SARIF ruleId fields all use the new form; no alias layer. mdBook filenames and docs URLs still use the flat kebab slug; docs-tree rearchitecture into category subdirs is a separate slice. F29-full (parked 2026-04-24) would add a stable category-numbered code (STR-001, LEX-002, SYN-003) that survives renames — slim already makes drift impossible by construction, and numeric codes only earn their cost on a real rename. Revisit only when a rename actually happens. |
| F-comma-density-relative | Rules refinement | ☐ | v0.2.x | Comma density metric (relative) for structure.excessive-commas |
| F-docsrs-logo | Docs.rs polish | ☐ | v0.2.x | Logo + favicon on docs.rs via #![doc(html_logo_url = "…")] and #![doc(html_favicon_url = "…")] at crate root. Reuses an asset hosted under the repo’s raw URL. Tiny visual identity win on the docs.rs landing page. Renumbered from F130. |
| F-landing-page | Docs — content | ☐ | v0.2.x | Landing-page polish. docs/src/introduction.md already plays both roles today: lens-motif hero, before/after figure, “what makes it different”, quick-taste terminal capture, “where to next”. A real landing-page push only earns its cost when there’s a first consumer outside the maintainer (project gets adopted, traffic shows up). Until then, polishing is design work without a forcing function. Notes for when triggered: more positioning above the fold, demo grid for the rule families (one canonical example per category), CTA toward profiles + quick-start, lens-motif extension already validated for use across the page. |
| F-llm-fp-miner | Quality | ☐ | v0.2.x | LLM false-positive miner via Claude Code. Dev-only audit script (not a test, not a CI gate) that runs lucid-lint across the CC corpus, asks Claude to flag diagnostics that look wrong, writes a triage report to .personal/audits/. Reframed from the original “LLM-as-Judge harness” after Devil’s Advocate surfaced three blockers on the gating form: non-determinism across Claude model versions, ambiguity about whether a disagreement indicts the rule or the judge, cost / wall-clock at 600×N scale. The miner form sheds all three — human triages, Claude suggests. Respects prime directive #4 (deterministic core, no LLM) because it lives entirely outside the library crate and never blocks just check. Wait until v0.3 lucid-lint-nlp plugin work surfaces the need for correctness review at scale. |
| F-lucid-stance-unify | Quality | ☐ | v0.2.x | Unify rule-page example figures on the .lucid-stance component. Today the intro page uses a custom .lucid-stance figure (Before / After side-by-side, colour-matched ideas, diagnostic in the figcaption), while rule pages use plain H3 + blockquote + fenced text for the diagnostic (see docs/src/rules/sentence-too-long.md). The H3 form works and is cheap to roll out, but wide screens could show stronger Before↔After pairing with the side-by-side figure. Scope: extract .lucid-stance into a reusable component (mdBook include or raw HTML pattern), tune the styling for in-content width (rule pages sit inside the narrower content column, not the landing-page hero), one figure per language, drop the H3 subsections in favour of a data-lang attribute surfaced as a chip on the figure. Ship only after the H3-based rollout has landed across all example-bearing rule pages and the unified pairing is confirmed as the dominant reader complaint. |
| F-mixed-script-fixtures | Architecture | ☐ | v0.2.x | Mixed-script test fixtures. Pin behaviour on EN + CJK and LTR + RTL prose mixed within one paragraph. unicode_words() should handle the boundaries correctly (UAX-29), but no regression test exists. Filed as Speculative — no known bug, just a coverage gap. Open if a real-world bilingual corpus surfaces edge cases. |
| F-multi-book-mdbook | Docs — bilingual | ☐ | v0.2.x | Multi-book mdBook layout (one book per locale). The truest “parallel version” — / redirects to /en/, /fr/ is its own mdBook with its own theme inheritance. Benefit: each locale has its own table of contents, its own search index, its own navigation neighbour hints; no cross-locale bleed in any surface. Cost: biggest surgery — book.toml per locale, build orchestration, shared theme / asset de-duplication, sitemap updates, redirects. Revisit only if F-summary-per-locale isn’t enough. |
| F-reading-time-score | Architecture | ☐ | v0.2.x | Reading-time-seconds as an alternative score unit — ties score to concrete user outcome. Requires validated heuristic + companion metrics (comfort, fatigue, understandability) so the time unit doesn’t monopolize the read. |
| F-reference-auto-discovery | Example fixtures | ☐ | v0.2.x | Auto-discovery of new references with triage queue. Crawler (sitemaps, RSS, GitHub search, ACL Anthology API) surfaces candidate sources against a relevance filter derived from rules_relevant keywords; a lightweight triage file lists candidates with accept / ignore / defer. Mini-product — revisit post-v0.3 once the referential has stabilised. |
| F-score-evolution-dashboard | Quality | ☐ | v0.2.x | CI users explicitly ask for trend view (not delta — delta is F-diff-mode / --compare) |
| F-sentence-diversity-density | Rules refinement | ☐ | v0.2.x | Sentence-level low-lexical-diversity density |
| F-summary-per-locale | Docs — bilingual | ☐ | v0.2.x | Split SUMMARY.md per locale (EN + FR) via a small preprocessor. v0.2.1 ships the single-SUMMARY.md + CSS :has() locale-hiding approach (1.A); both language trees coexist in the built HTML and each viewer only sees theirs. A clean separation would maintain SUMMARY.en.md + SUMMARY.fr.md and stitch them at build. Benefit: smaller per-page sidebar payload; clearer authoring story; no :has() browser-support floor. Cost: build-time stitcher, tooling to keep the two files in pair-sync. File when the FR tree outgrows the hide-via-CSS approach. |
| F35d | Docs — reading prefs | ☐ | v0.2.x | Publish an accessibility statement page (docs/src/accessibility.md, FR counterpart at docs/src/fr/accessibility.md). EN page carries the stated bar (WCAG 2.2 AAA), first audit pass result (2026-04-22, 17/20), a “Known limitations” block listing F35a/b/c pending, report route, and audit cadence. FR stub mirrors the limitations block. Shipped 2026-04-22. |
| F78b | Scope control | ☐ | v0.2.x | .lucidignore file (gitignore-style, with negations and nested files). Different crate (ignore) and a larger test matrix than the glob-list MVP. Ship only if users ask — the exclude list in lucid-lint.toml covers the dominant use case. |
| F11 | Rules refinement | ✅ | v0.2.x | Shipped in v0.2 — --readability-formula {auto,flesch-kincaid,kandel-moles} CLI flag + FormulaChoice enum on readability_score::Config + Engine::with_readability_formula(choice). auto (default) keeps F-readability-formulas-extra per-language selection; flesch-kincaid / kandel-moles pin a formula for cross-document comparison. TOML config wiring is tracked separately as F77. |
| F126 | Architecture | ✅ | v0.2.x | Shipped — Markdown parser maps <br> to \n in paragraph.text. Pulldown-cmark emits <br> as Event::InlineHtml, not Event::HardBreak, so the v0.2.x author-break-aware fix for structure.line-length-wide silently dropped <br> despite advertising it as a measured hard break. Helper html_is_br_tag recognises <br>, <br/>, <br /> (any case, optional whitespace); HTML comments (suppression directives) flow through unchanged. Five new tests pin the contract: br_tag_inside_paragraph_is_a_hard_break and html_comment_directives_do_not_inject_newlines (parser); markdown_br_tag_is_checked, list_item_text_is_out_of_scope, table_cell_text_is_out_of_scope (rule). The two out-of-scope tests pin the parser-construction contract that list-item content and GFM table cells are not emitted as paragraphs today, so the rule is silent on over-length content inside them — a future parser change that starts emitting either as paragraphs would need to revisit this rule. |
| F26 | Docs — theming | ✅ | v0.2.x | MVP shipped in v0.2 via DOM-level trim in lucid-navigation.js — the picker now shows three honest items (Auto · Lucid light · Lucid dark); the stock Rust / Navy / Ayu <li>s are marked hidden so they’re inert for keyboard and screen-reader. CSS class mapping is unchanged (.light / .rust → lucid-light, .coal / .navy / .ayu → lucid-dark), so pre-existing localStorage selections still render correctly. Follow-up (optional): a full index.hbs override to drop the stock markup entirely rather than hide it; preferred once the mdBook upgrade cadence settles. |
| F43 | Docs — content | ✅ | v0.2.x | Shipped in v0.2 — RULES.md category drift fixed. Per-rule **Category** lines and the Categories table now match Category::for_rule: structure.excessive-commas and structure.deep-subordination are structure, rhythm.repetitive-connectors is rhythm, syntax.unclear-antecedent is syntax. The drift banners on the four per-rule mdBook pages are removed. |
| F47 | New rules | ✅ | v0.2.x | lexicon.consonant-cluster shipped in v0.2 — see docs/src/rules/consonant-cluster.md |
| F50 | New rules | ✅ | v0.2.x | structure.line-length-wide shipped in v0.2 — see docs/src/rules/line-length-wide.md |
| F52 | New rules | ✅ | v0.2.x | structure.mixed-numeric-format shipped in v0.2 — see docs/src/rules/mixed-numeric-format.md |
| F54 | New rules | ✅ | v0.2.x | syntax.dense-punctuation-burst shipped in v0.2 — see docs/src/rules/dense-punctuation-burst.md |
| F62 | New rules | ✅ | v0.2.x | lexicon.redundant-intensifier shipped in v0.2 — see docs/src/rules/redundant-intensifier.md |
| F73 | Docs — theming | ✅ | v0.2.x | Pre-deploy font-leak gate shipped in v0.2 — just docs-check-clean rebuilds the book, runs scripts/sanitize-stock-css.py, and greps the output for active font-family / --*-font / local() references to Open Sans or Source Code Pro. Not wired into just check (mdbook build is too slow for the dev loop); wire it into the docs-publish CI workflow before any release-candidate goes live. |
| F93 | Performance / hygiene | ✅ | v0.2.x | Parser hot-path allocations. src/parser/mod.rs:43 (Paragraph::new(trimmed.to_string(), …)) and src/parser/tokenizer.rs:~88/109 (current.trim().to_string() per sentence) allocate in hot loops. Confirm constructors accept impl Into<String>; pass the already-owned buffer where possible. Refuted by samply profile 2026-04-25: Paragraph::new does not appear in the profile; to_string() in tokenizer = 3 samples / 0.03%. Real hot spots are F102 (detect_language 7.5%) and F103 (per-rule split_sentences). |
| F-repo-config-hardening | Architecture | ✅ | v0.2.x | Shipped 2026-05-03 — full pass closed. What landed today: tag ruleset on v* pattern (block deletion + force-push, Active; matches all 7 release tags v0.1.0 → v0.2.4). Pre-existing (verified via API audit before clicking): .github/dependabot.yml (cargo + github-actions, weekly, grouped); Actions pinned-SHA required (sha_pinning_required: true); secret scanning + push protection both enabled; private vulnerability reporting enabled; CodeQL configured via Advanced setup workflow (.github/workflows/codeql.yml, weekly Rust scan, last run 0 results / 29 rules); Scorecard workflow also running. Retro on the routed-vs-actual gap: the entry as routed listed 6 items as if all were unconfigured; the API audit revealed 5 of 6 already shipped via earlier hardening passes that didn’t surface to the ROADMAP. Net work today: 1 click (the tag ruleset) + 1 retro audit. Lesson for future “GH Settings hardening” entries — verify state via gh api before drafting the checklist; the repo’s actual posture had drifted past the assumed baseline. Heads-up resolved 2026-05-03: the legacy main-protection branch ruleset transiently showed enforcement: disabled during the hardening session; re-verified via gh api repos/:owner/:repo/rulesets later the same day — both main-protection (branch, 5 rules → main) and v-tag-protection (tag, 2 rules → 7 v* tags) report enforcement: active. Branch protection is enforced on main + tags v*. Branch-Protection scorecard plafond (post-hoc note). OSSF Scorecard’s Branch-Protection check warns that main requires no approvers, no CODEOWNERS review, and no last-push approval. These three sub-controls assume ≥ 2 humans; a solo-maintainer repo cannot satisfy them without admin bypass (theatre) or auto-approve (contournement of the control’s intent). Decision 2026-05-03: accept the 4/10 ceiling as structural while solo. Revisit on co-maintainer onboarding, or pair with F-adversarial-review to add bot-driven review without faking human gating. Original checklist (preserved for retro reference): (1) Tag ruleset on v*. (2) Dependabot version updates (cargo + github-actions). (3) Actions: Require pinned-SHA. (4) Secret scanning + push protection. (5) CodeQL default setup. (6) Private vulnerability reporting. |
| F-roadmap-toml-source | Architecture | ✅ | v0.2.x | ROADMAP source-of-truth migrated to .roadmap/ (gitignored), one markdown file per feature with TOML front-matter; crates/roadmap-cli workspace member provides add / generate / validate. Generator runs locally on release prep; regenerated ROADMAP.md committed on the release-prep PR. Eliminates worktree+PR ceremony for 10–30 ROADMAP-only edits/week; design recorded in Design decisions — ROADMAP source-of-truth pipeline. A1 schema, A2 generate, A2.5 add, A2.6 validate, A3 backup, A6 release workflow, A8 ADR, A9 one-shot migration of ~160 features all shipped. Closed 2026-05-07 in PR #70. |
| F102 | Performance / hygiene | ✅ | v0.2.x | detect_language cost. Single function showed 7.5% inclusive in samply profile 2026-04-25. Rewrote as single-pass, alloc-light: scalar counters, to_lowercase() only for words containing an uppercase character, no intermediate vectors. Bench delta on engine_lint_str/en_long_devdoc vs stream2-noisy: −0.56 % (p = 0.00, ~20 µs) — smaller than profile suggested because most of the inclusive cost is unicode_words() itself, which the rewrite cannot touch. |
| F103 | Performance / hygiene | ✅ | v0.2.x | Per-rule split_sentences re-parse. 8 rules called split_sentences(¶graph.text, …) directly. Moved sentence splitting into Paragraph::new; rules now read ¶graph.sentences. Bench delta vs stream2-noisy: engine_lint_str/en_long_devdoc −11.58 % (~394 µs); parse_markdown/en_long +17.67 % (~38 µs, intentional — split cost moved into the parser phase, where it pays for itself across the eight consumers). Net user-facing win ~360 µs. New baseline saved as stream2-after-f103. |
| F104 | Docs — content | ✅ | v0.2.x | Shipped 2026-04-27 — SUMMARY.md reshaped into 5 collapsible sub-trees (Structure / Rhythm / Lexicon / Syntax / Readability) using mdBook draft chapters (- [Title]()) as non-clickable group headers; FR Version française block mirrors the same shape (Structure / Rythme / Lexique — Syntaxe and Lisibilité materialise as those FR translations land). markdownlint MD042 disabled globally to permit the empty-link draft-chapter syntax (matches the pre-existing MD025 carve-out for SUMMARY-required multiple H1s). Picked over (B) “one sub-page per category” — B doubles the page count without adding clarity the index table doesn’t already provide. |
| F105 | Docs — content | ✅ | v0.2.x | Shipped 2026-04-27 — docs/src/references.md (EN, under Project) and docs/src/fr/references.md (FR, under Version française) consolidate every cited source into one informative surface, preserving the full taxonomy of examples/REFERENCES.md (legend, per-domain sections, rule → reference summary table) and the scholarly-honesty note. examples/REFERENCES.md becomes a thin redirect to the docs sources — kept because external citations may already point there. Both rule indexes (EN + FR) cross-link to the new page next to the existing RULES.md pointer. Per-citation anchors deferred — readers scan the page or use browser search; if a need surfaces, file a follow-up. |
| F105b | Docs — content | ✅ | v0.2.x | Shipped 2026-04-27 — Per-citation anchors (<a id="author-year">) on every entry of references.md + fr/references.md, plus a ## References / ## Références section on every rule page (25 EN + 13 FR) listing the relevant citations as anchored links. The references page now links rule IDs in → Relevant to: lines and the rule → reference summary table to their per-rule mdBook pages — bidirectional rules ↔ references. Verified canonical URLs (DOI, publisher landing page, official archive — researched in 2026-04-27 lap, 26 of 34 academic citations carry one) added inline as raw HTML anchors with rel="nofollow noopener noreferrer" target="_blank": nofollow so the docs site does not vouch for outside content, noopener noreferrer for new-tab safety. Sources without a verifiable canonical URL stay text-only — no guessed links. Subsumes the F-rule-mention-linking rule-mention linking pass for the references-page surface; wider F-rule-mention-linking audit (rule mentions in docs/src/guide/* prose pages) stays open. |
| F107 | Docs — bilingual | ✅ | v0.2.x | Shipped 2026-04-27 — Two-part fix without aliasing the rule ID. (1) Page subtitle: every shipped FR rule page opens with a short italic gloss directly under the H1 (e.g. *Phrase trop longue.*); 13 pages received the subtitle, the remaining 12 land alongside their translation. (2) Index gloss: fr/rules/index.md “Catégories” block reshaped into 5 per-category sub-tables (Structure / Rythme / Lexique / Syntaxe / Lisibilité), each `Règle \ |
| F108 | Quality | ✅ | v0.2.x | low_lexical_diversity::ratio_at_anchor_min — assert reported ratio in tests. ✅ Shipped 2026-04-25. Added reported_ratio() helper (parses the documented message format) and three new test fixtures: reported_ratio_is_minimum_observed_in_cluster (50 W + 100 cache + 50 V → cluster-exit path with min ratio 0.01 deep mid-slide, not at anchor), flush_path_reports_final_ratio (cache-only doc → flush path), and exactly_window_size_tokens_runs_the_check (boundary on the early-return guard). Ratio assertion uses (ratio - 0.01).abs() < 1e-9 so floating-point shifts from arithmetic mutations are caught. Bonus refactor (typed-ratio field on Diagnostic) deferred — string parsing is fine for the test-only consumer. |
| F109 | Quality | ✅ | v0.2.x | low_lexical_diversity::check — borderline-cluster fixtures. ✅ Shipped 2026-04-25 alongside F108. Added cluster_starts_at_strict_inequality and ratio_exactly_at_threshold_does_not_trigger — the latter uses 49 W + 51 cache so the only full window has unique=50 → ratio exactly 0.50 = min_ratio. With strict < the rule must not trigger; a < → <= flip would emit a diagnostic and fail the test. Combined effect: the rule’s mutation score moved from 36 % (29 / 47 / 5) at F98 baseline to 89 % (68 / 8 / 5). The remaining 8 missed mutants are equivalent under the current rule logic — defensive guards (start_index + window > tokens.len() is unreachable in normal flow because anchor.index ≤ len − window), or initial values the slide loop unconditionally overwrites (let mut best = unique / window is replaced as soon as a lower ratio appears, which it always does in a real cluster). Closing those would require rule refactoring (e.g. starting best at f64::INFINITY to prove the initial computation is dead) — diminishing returns; deferred. |
| F110 | Architecture | ✅ | v0.2.x | Shipped 2026-04-28 — leading \u{FEFF} stripped once at the engine boundary (Engine::lint_with_source, via the normalize_input helper). Funnels every input path (string, stdin, file) through the same boundary so rules never see the BOM. Regression test in src/engine.rs::tests::bom_prefix_does_not_shift_diagnostics proves identical diagnostics + line/column locations with and without a leading BOM on a sentence-too-long fixture. |
| F111 | Architecture | ✅ | v0.2.x | Shipped 2026-04-28 — unicode-normalization = "0.1" added; Engine::lint_with_source NFC-normalizes input at the same boundary as F110, fast-pathing already-NFC text via is_nfc_quick. NFC café and NFD cafe + U+0301 now hash identically in every HashMap-using rule. Regression test in src/engine.rs::tests::nfd_input_yields_same_diagnostics_as_nfc exercises a 4-sentence FR fixture and asserts diagnostic count + per-diagnostic rule id and line match across NFC and NFD inputs. |
| F112 | Architecture | ✅ | v0.2.x | Shipped 2026-04-28 — src/engine.rs::tests::lone_cr_line_endings_are_normalized pins parity between LF and lone-CR three-paragraph fixtures (word count + diagnostic count). src/engine.rs::tests::zero_width_chars_inside_words_pin_behaviour pins observed behaviour for U+200B / 200C / 200D inside words: the engine round-trips without panicking and produces a valid Report; exact word count is intentionally not asserted because nfc() does not strip them and tokenisation is owned by unicode-segmentation. |
| F14 | Architecture | ✅ | v0.2.x | Hybrid scoring model shipped in v0.2 (global score + per-category sub-scores + diagnostics). X/max arbitrary-max at both levels, 5 fixed categories (Structure · Rhythm · Lexicon · Syntax · Readability), composition = weighted sum × density-normalization × per-category cap, weight field added to Diagnostic, --min-score=N CLI flag. See docs/src/guide/scoring.md. Letter-grade / traffic-light / reading-time decorations deferred (F-score-letter-grade–F-reading-time-score). |
| F18 | Suppression / config | ✅ | v0.2.x | Block form shipped in v0.2: <!-- lucid-lint-disable <rule-id> --> … <!-- lucid-lint-enable --> silences one rule across every line in the scope. enable with no argument closes every open scope; with a rule id, closes only that rule’s scope (so overlapping disables for different rules can nest). Unterminated disable extends to end-of-document. See RULES.md → Suppressing diagnostics. |
| F19 | Suppression / config | ✅ | v0.2.x | Shipped in v0.2 — top-level [[ignore]] array-of-tables in lucid-lint.toml, each entry with a required rule_id silences every diagnostic for that rule across Markdown, plain text, and stdin. Unknown ids tolerated. Applied post-engine, pre-scoring, so scoring / rendering / exit-code logic all see the filtered view. Scope broadened from the roadmap’s original “.txt and stdin” wording because a global filter is simpler and more useful; Markdown users can still prefer inline directives for local silencing. reason field tracked as F-suppression-reason-field. See docs/src/guide/configuration.md. |
| F23 | Rules refinement | ✅ | v0.2.x | Shipped in v0.2 — false-positive cleanup complete for v0.2. Hits inside inline code spans, straight "..." quotes, paired curly "..." quotes, and directional rather than / plutôt que pairings are now skipped. Single quotes / apostrophes are deliberately not recognised (possessives, contractions, FR elisions). The “concrete noun” semantic check ("many X" where X is a concrete noun) stays unshipped — needs POS data and belongs in the lucid-lint-nlp plugin (F-nlp-plugin) rather than the deterministic core. |
| F25 | Docs — bilingual | ✅ | v0.2.x | French mirror of the mdBook docs (/fr/ tree). First slice shipped 2026-04-22: translated introduction + rules-index, short FR accessibility and roadmap pages pointing at EN, SUMMARY sidebar entry. Second slice shipped post-0.2.1 (2026-04-23): fr/rules-index.md renamed to fr/rules/index.md for EN-parity, first FR per-rule page landed (structure.sentence-too-long), parallel-version sidebar and EN↔FR deep-link toggle (F-summary-per-locale plan slot A, F92). Third slice shipped 2026-04-24: four more FR per-rule pages landed (structure.excessive-commas, structure.long-enumeration, lexicon.weasel-words, lexicon.unexplained-abbreviation), locked template honoured, SUMMARY.md + fr/rules/index.md rewired to point at the local FR versions. Fourth slice shipped 2026-04-25: six more FR per-rule pages landed (structure.paragraph-too-long, structure.line-length-wide, structure.mixed-numeric-format, structure.deeply-nested-lists, structure.heading-jump, structure.deep-subordination), closing out the structure category (9 / 9 rules FR-complete). Fifth slice shipped 2026-04-27: two more FR per-rule pages landed (rhythm.consecutive-long-sentences, rhythm.repetitive-connectors), closing out the rhythm category (2 / 2 rules FR-complete). Both EN pages were brought up to canonical template first (Examples + See also added). Sixth slice shipped 2026-04-28: six more FR per-rule pages landed (lexicon.low-lexical-diversity, lexicon.excessive-nominalization, lexicon.jargon-undefined, lexicon.all-caps-shouting, lexicon.redundant-intensifier, lexicon.consonant-cluster), closing out the lexicon category (8 / 8 rules FR-complete). Three of five categories now at 100 % (structure + rhythm + lexicon). Seventh slice shipped 2026-04-30: six more FR per-rule pages landed (syntax.passive-voice, syntax.unclear-antecedent, syntax.dense-punctuation-burst, syntax.conditional-stacking, syntax.nested-negation, readability.score), closing out the syntax (5 / 5) and readability (1 / 1) categories — all 5 categories now 100 % FR-complete (25 / 25 per-rule pages). SUMMARY.md was missing FR Syntaxe + Lisibilité subsections entirely; added in the same commit. Also fixed an EN/FR logic bug in syntax.nested-negation example (After clause now something is possible / quelque chose est possible, matching the predicate-logic-faithful inversion of the Before clause). Eighth slice shipped 2026-05-01 (Block C slice A): first two FR guide pages landed (fr/guide/installation.md, fr/guide/quick-start.md); new Premiers pas draft-chapter group in SUMMARY.md; both pages stamped with the F92 sub-task en-source-sha HTML comment. Ninth slice shipped 2026-05-01 (Block C slice B): two more FR guide pages landed (fr/guide/profiles.md, fr/guide/suppression.md) — Block C now half-done (4 / 8). 4 EN-only guide pages remain (conditions, configuration, scoring, ci-integration); FR pair-completeness now 35 / 42 (untranslated EN: 7, down from 11 at start of day). Tenth slice shipped 2026-05-01 (Block C slice C — closing slice): four FR guide pages landed (fr/guide/conditions.md, fr/guide/configuration.md, fr/guide/scoring.md, fr/guide/ci-integration.md); SUMMARY.md Premiers pas group now lists all 8 children. Block C complete (8 / 8). All 8 EN guide pages now have FR mirrors; FR pair-completeness 39 / 42 — only the architecture overview, design-decisions, and contributing pages remain untranslated (these are next-tier surfaces, not part of the user-facing guide). Eleventh slice shipped 2026-05-01 (next-tier close): three FR pages landed (fr/architecture/overview.md, fr/architecture/design-decisions.md, fr/contributing.md); SUMMARY.md gains an Architecture draft-chapter group + Contribuer entry under Version française. F25 closes — pair-completeness 41 / 41 (only roadmap.md remains intentionally asymmetric). |
| F27 | Docs — content | ✅ | v0.2.x | Shipped in v0.2 — docs/src/roadmap.md is auto-generated from the root ROADMAP.md by scripts/sync-roadmap.py. just docs-build / just docs-serve run the sync first, so the mdBook site always ships the current roadmap. Relative links are rewritten (targets under docs/src/ become docs-relative; others become absolute GitHub URLs) so the docs_links_stay_inside_docs gate still passes. |
| F28 | Docs — content | ✅ | v0.2.x | Shipped in v0.2 — one page per rule under docs/src/rules/, wired into docs/src/SUMMARY.md, enforced by tests/rule_docs_coverage.rs. Each page carries category, severity, default weight, parameters per profile, EN/FR examples where applicable, and suppression guidance. |
| F31 | Rules refinement | ✅ | v0.2.x | Shipped in v0.2 — dev-doc baseline narrowed to the infrastructure stack (URL, HTML, CSS, JSON, XML, HTTP, HTTPS, UTF, IO, API, CLI, GUI, OS, CPU, RAM, SSD, USB, IDE, SDK, CI, CD). Accessibility standards, engineering-practice initialisms, and AI/language-tech terms moved to project config via new [rules.unexplained-abbreviation].whitelist in lucid-lint.toml (additive over baseline). Breaking change for downstream users, flagged in CHANGELOG with the recovery snippet. Dogfooded in this repo’s own lucid-lint.toml. |
| F32 | Architecture | ✅ | v0.2.x | Shipped in v0.2 — lucid-lint check --format=sarif emits a SARIF v2.1.0 log compatible with GitHub Code Scanning. One rule descriptor per observed rule id (category, default severity, default weight, helpUri to the per-rule mdBook page); per-result properties carry weight + section. Workflow snippet in docs/src/guide/ci-integration.md. |
| F35a | Docs — reading prefs | ✅ | v0.2.x | Shipped 2026-04-22 — theme/index.hbs is now forked from mdBook v0.5.2’s upstream template (minimal-diff approach, documented so future mdBook upgrades stay a mechanical re-sync). The skip link and EN / FR language switch are emitted as server-rendered HTML inside <body> and inside .right-buttons; both language variants are rendered and CSS in lucid-layout.css hides the wrong-locale copy based on html[lang] (which head.hbs sets synchronously before first paint on /fr/ pages). The previous skipLink() and langSwitch() IIFEs in lucid-navigation.js are gone; the only remaining JS on the skip-link path is a progressive-enhancement smooth-scroll handler. WCAG 2.4.1 Bypass Blocks now passes with JS disabled. Unblocks F26 (stock theme labels can be collapsed at the markup level). |
| F35c | Docs — reading prefs | ✅ | v0.2.x | Closed 2026-05-01 as audit false-positive. The 2026-04-22 audit reported that .lucid-stance__idea lost its colour tint under prefers-reduced-motion. Re-audit on 2026-05-01 against docs/theme/css/lucid-layout.css:567-622 and docs/theme/css/lucid-typography.css:424-431: no @media (prefers-reduced-motion: reduce) rule touches .lucid-stance__idea; the global reduced-motion reset zeroes animation-duration / transition-duration only and never overrides background-color. The only rule that strips the tint is @media (forced-colors: active) (line 620–622), which is intentional (Windows High Contrast users get the OS palette, position-based pairing carries the meaning). The original audit appears to have conflated forced-colors: active with prefers-reduced-motion: reduce. No code change needed; accessibility.md known-limitation bullet removed in the same commit. |
| F37 | Architecture | ✅ | v0.2.x | Rule-message clarity audit completed: all 17 rules reviewed against “what do I change?” bar. 15 rules already actionable; structure.heading-jump updated (first-heading-not-H1 and missing-H1 variants now include repair guidance). readability.score info variant left observational by design (fires only when always_report is set). |
| F42 | Docs — content | ✅ | v0.2.x | Shipped in v0.2 — rule documentation coverage gate. tests/rule_docs_coverage.rs cross-checks every shipped rule id against its mdBook page, Category::for_rule, scoring::WEIGHTED_RULE_IDS, and (on CI, gated by RULE_DOCS_GATE_GIT=1) the ## [Unreleased] section of CHANGELOG.md. Contract documented in CONTRIBUTING.md. |
| F48 | New rules | ✅ | v0.2.x | lexicon.all-caps-shouting shipped in v0.2 — see docs/src/rules/all-caps-shouting.md |
| F55 | New rules | ✅ | v0.2.x | syntax.nested-negation shipped in v0.2 — see docs/src/rules/nested-negation.md |
| F56 | New rules | ✅ | v0.2.x | syntax.conditional-stacking shipped in v0.2 — see docs/src/rules/conditional-stacking.md |
| F71 | Architecture | ✅ | v0.2.x | Shipped in v0.2 — ConditionTag enum (fixed 7-variant ontology: a11y-markup, dyslexia, dyscalculia, aphasia, adhd, non-native, general) plus Rule::condition_tags() trait method (default &[General]). All 17 v0.2 rules are general; future tagged rules (F48, F55, F56) opt in by overriding. See docs/src/guide/conditions.md. |
| F72 | Architecture | ✅ | v0.2.x | Shipped in v0.2 — [default] conditions = [...] config field and --conditions CLI flag (comma-separated). Filter semantics: rules tagged general always run; tagged-only rules run iff their tags intersect the active list. Profiles unchanged; FALC retains its regulatory meaning. See docs/src/guide/conditions.md. |
| F78 | Scope control | ✅ | v0.2.x | Shipped in v0.2 — exclude = [...] glob list in [default] of lucid-lint.toml and --exclude <GLOB> CLI flag (comma-delimited, repeatable). Patterns match against paths relative to the walked root; matching directories are pruned, not descended. Explicit file args bypass exclusion. Backed by globset. See docs/src/guide/configuration.md. .lucidignore (gitignore-style file) deferred to F78b if user demand surfaces. |
| F87 | Rules refinement | ✅ | v0.2.x | Shipped in 0.2.x — FR syntax.nested-negation now uses pair-based counting over ne / n' clitics and the second-position particles pas, rien, jamais, plus, personne, aucun, aucune, guère, nulle part. Each clitic contributes one negation and consumes its nearest particle within a 6-token window; unpaired particles in a ne-sentence contribute one more — so Nous ne disons pas que rien n'est jamais possible now counts as 3 (was 2). Guards: pas / plus never count when unpaired, de rien idiom is skipped, particles in ne-less sentences are skipped. Fixture at tests/corpus/fr/nested-negation.md anchors the behaviour. |
| F9 | Rules refinement | ✅ | v0.2.x | Shipped in v0.2 — definition-aware lexicon.unexplained-abbreviation is now two-pass. A pre-scan collects acronyms defined anywhere in the document in either canonical form (Expansion (ACRONYM) or ACRONYM (Expansion); expansion side ≥ 2 alphabetic words to reject (TBD)-shaped noise), and a single definition silences every occurrence of that token. Silencing precedence: defined-in-doc → user whitelist → baseline. See docs/src/rules/unexplained-abbreviation.md. |
| F92 | Docs — bilingual | ✅ | v0.2.x | Shipped post-0.2.1 (2026-04-23) — scripts/sync_lang_counterparts.py walks docs/book/**/*.html after mdbook build and rewrites both hreflang="en" and hreflang="fr" anchors so the lang-switch deep-links to the matching page (e.g. /fr/rules/sentence-too-long.html ↔ /rules/sentence-too-long.html). Wired into just docs-build, the Deploy-docs workflow, and a new just docs-lang-check CI gate that runs with --check and fails on orphaned FR pages (FR without EN counterpart). The invariant is asymmetric by design: EN is canonical, FR is a translation layer — untranslated EN pages are informational and tracked as F25, not gated. No front-matter flag yet; add a counterpart: none flag only when a truly asymmetric page appears. Sub-task — FR content-staleness gate (shipped 2026-05-01): filename parity is gated; content drift was not. Every FR page now carries an en-source-sha HTML-comment stamp on its first line (<!-- en-source-sha: 5e24f614… -->), recording the EN counterpart’s last commit SHA at translation time. mdBook passes HTML comments through unchanged so the stamp is invisible in the rendered page; YAML front-matter was tried first but mdBook renders --- as <hr> and the body as text. scripts/check_lang_staleness.py walks every FR page, compares the stored SHA to git log -n1 --pretty=%H -- <EN counterpart>, reports drift soft (PR ci.yml + main docs-deploy.yml) and fails on main with STRICT=1 once the existing stale backlog clears. Wired as just docs-lang-staleness. scripts/backfill_en_source_sha.py (one-shot) stamped the 29 already-translated FR pages with the EN SHA at their introduction commit. Reconcile shipped 2026-05-01 (commit 438fa48b, “F92 — reconcile stale FR backlog (13 → 0) + flip gate to strict”): of the 13 pages reported stale, 12 were cosmetic stamp drift only (the F105/F105b references-section sweep, the F35b/F35c a11y fix, and the line-length-wide author-break-aware fix all touched FR counterparts in the same commits — only the en-source-sha stamps lagged), 1 was substantive (fr/index.md had drifted on three sections — État du projet v0.2 numbers, Aperçu peak-end demo block, Pour aller plus loin guide-links update). Same commit flipped docs-deploy.yml from soft to --strict. PR-side ci.yml flipped to --strict on 2026-05-02 — both surfaces now strict-gated, sub-task fully closed. Optional further layers: an mdBook preprocessor banner above stale FR pages; a needs-fr-translation PR label automation for EN edits without FR counterparts. |
| F94 | Performance / hygiene | ✅ | v0.2.x | Tokenizer Vec<char> per sentence. src/parser/tokenizer.rs:~60 collects a full Vec<char> for lookahead. Swap to Peekable<CharIndices>. Refuted by samply profile 2026-04-25: Vec<char> drop = 3 samples / 0.03% on the engine path. Yesterday’s “low ceiling” note (~5%) was generous; real ceiling is ~0.1%. Skip. |
| F95 | Performance / hygiene | ✅ | v0.2.x | Shipped 2026-04-24 in commit 925ffb5. Two non-literal expects fixed: consecutive_long_sentences.rs (streak_start unwrap when streak_len > max) and all_caps_shouting.rs::flush_run (first()/last() on a Vec already verified len >= min_run). The originally flagged parser/tokenizer.rs:177 candidate is now an if let Some(...) pattern. Remaining expect("non-zero literal") sites are all NonZeroU32::new(LITERAL) — idiomatic compile-time invariants, explicitly out of audit scope. |
| F96 | Performance / hygiene | ✅ | v0.2.x | Shipped 2026-04-24 in commit 925ffb5. src/scoring.rs:199-209 now carries an explicit safety-contract comment naming the [0, cap] clamp dependency, plus a debug_assert!(normalized.is_finite() && (0.0..=cap).contains(&normalized)) that trips in debug builds if a future edit loosens the clamp. The #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] stays — it masks a lint, not a real bug — but the invariant is now load-bearing in tests. |
| F98 | Quality | ✅ | v0.2.x | Mutation testing via cargo-mutants. ✅ Baseline shipped 2026-04-25 — dev-tool installed, just mutants <file> recipe added (timeout 60 s, no-shuffle for reproducibility), four-file probe run: sentence_too_long.rs 6 caught / 0 missed / 4 unviable (100 %), scoring.rs 18 / 0 / 2 (100 %), engine.rs 5 / 0 / 12 (100 %), low_lexical_diversity.rs 29 / 47 / 5 (36 %). Canonical reference rule + cross-cutting layer score perfectly; the lexical-diversity rule has two clear test gaps surfaced as F108 + F109. Triage methodology: cluster missed mutants by site → one ROADMAP entry per root cause, not per mutant. |
| F-github-action | Adoption channels | 🚧 | v0.3 | GitHub Action in Marketplace (promoted to 🔴 Next, targeted at v0.3 from 2026-04-27 Block E recon — early-adoption feedback channel). Verified peer shape: both astral-sh/ruff-action and biomejs/setup-biome are thin composite actions (yaml-only) that download the prebuilt binary from upstream Releases, add it to PATH, optionally run it. Composite > Docker container for sub-second cold start; pure JS action avoided (no Node runtime needed). Proposed contract: uses: lucid-lint/lucid-lint-action@v1 with with: inputs version (default latest), paths, profile (falc / dev-doc / public), format (tty / json / sarif), min-score. v0.3 first cut emits ::warning file=…,line=…:: workflow commands for inline PR annotations; v0.4 swaps to SARIF upload via github/codeql-action/upload-sarif once the SARIF output stabilises, feeding GitHub Code Scanning natively. Risk: a composite action coupled to cargo-dist release-tarball naming — any rename breaks consumers, so pin the manifest contract. Internal scaffold landed 2026-04-28 — action.yml at the repo root implements the locked input contract (version, paths, profile, format, min-score, plus working-directory and passthrough args); a smoke workflow (.github/workflows/action-smoke.yml) exercises it on Linux / macOS / Windows runners against this repo’s own docs/src/. Not yet published, not yet v1-tagged, not yet listed in the Marketplace. Bake-in plan: dogfood the contract internally for 2–3 weeks, revise inputs that don’t survive contact with reality, then split out to a dedicated bastien-gallay/lucid-lint-action repo (the canonical ruff / biome pattern) and tag v1 alongside the v0.3 release. v0.3 first cut still emits ::warning::; SARIF upload deferred to v0.4 behind F32. |
| F-large-number-id-exclusion | Rules refinement | ☐ | v0.3 | readability.large-number-unanchored — exclude identifier-shaped digit sequences (phone numbers, long IDs). Pre-flip follow-up surfaced by the dogfood pass on examples/public/ (PR #77). After the dotted-reference fix, 26 hits remain, dominated by space-grouped phone numbers (020 7946 0457 → 2079460457, +39 1 33 45 70 90) and long ID-shaped runs (SIRET-length sequences) that the scanner collapses into spurious large quantities via the space-as-thousands-separator path. These are not magnitudes a reader needs anchored, so they inflate the false-positive rate. Constraint: the space separator cannot be dropped wholesale — FR writes 1 000 000 for a genuine million — so the fix needs a discriminator (e.g. leading 0 / +, irregular non-three grouping, or a digit-count ceiling) rather than disabling space-collapse. Gates the v0.3 Experimental → Stable flip of F53 (F53): the severity decision (Warning vs Suggestion) and the flip itself stay blocked until the public-corpus signal is clean. Boundary with the dotted-reference skip already shipped in PR #77 (irregular dot groups → reference): this entry covers the space-grouped / identifier class that path doesn’t reach. |
| F-npm-wrapper | Adoption channels | ☐ | v0.3 | npm wrapper with platform optionalDependencies — promoted to 🔴 Next, targeted at v0.3 (early-adoption feedback channel for the JS-toolchain audience: Prettier / ESLint / Husky / package.json scripts users). Canonical pattern verified on the npm registry: biome (@biomejs/biome 2.4.13) and dprint (0.54.0) both publish a thin root package whose optionalDependencies lists one sub-package per target; npm resolves only the matching platform; root bin shim execs the binary; dprint additionally runs a postinstall install.cjs as fallback. Proposed shape: root lucid-lint (~10 kB) + five platform-specific @lucid-lint/cli-{aarch64-apple-darwin, x86_64-apple-darwin, x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, x86_64-pc-windows-msvc}. Version stays in lockstep with the Rust crate; release workflow gains an npm publish --provenance step using OIDC (biome already does this). Risks: (1) 5+ packages per release multiply publish-failure surface — release workflow needs all-or-nothing semantics; (2) npm registry outages would block JS users — document fallback to direct binary download (F123). |
| F-tight-list-paragraphs | Architecture | ☐ | v0.3 | Markdown parser — emit paragraphs for tight list items (correctness fix). Discovered 2026-05-01 while verifying the F22 third-tranche dogfood metric: the same bullet content that triggers excessive-commas / dense-punctuation-burst / readability.score in a loose list (multiple items separated by blank lines) is silent in a tight list (single item, or items without separating blank lines). Root cause: pulldown-cmark only emits Tag::Paragraph events for items in loose lists; tight-list text events fire directly inside Tag::Item. The parser at src/parser/markdown.rs only buffers text inside heading or paragraph contexts, so tight-list content goes into the void and every paragraph-level rule (all 17 in v0.1) inherits the blindspot. Same pre-existing limitation flagged in F126 for structure.line-length-wide — F-tight-list-paragraphs resolves it once for every rule. Fix: synthesize a paragraph for each list-item span when no Tag::Paragraph event fires inside it. Expected dogfood impact: many CHANGELOG / release-note / README bullets become newly visible to rules — some genuine new diagnostics, some snapshot updates. |
| F-readability-formulas-extra | Rules refinement | ☐ | v0.3 | Must-ship slice shipped in v0.2 — readability.score auto-selects the formula by detected language: Flesch-Kincaid for EN (kept), Kandel & Moles (1958) for FR. Kandel-Moles ease scores are converted to a grade-equivalent so per-profile max_grade_level stays comparable across languages. Unknown language → Flesch-Kincaid. See docs/src/rules/readability-score.md. Still open: Gunning Fog / SMOG / Dale-Chall (EN), Scolarius / Flesch-Kandel (FR), --readability-verbose multi-formula reports, per-file override (covered by F11). |
| F-docsrs-metadata | Docs.rs polish | ☐ | v0.3 | [package.metadata.docs.rs] block in Cargo.toml. Pin the toolchain and feature set docs.rs builds with; add rustdoc-args = ["--cfg", "docsrs"] so any future feature-gated items can carry #[cfg_attr(docsrs, doc(cfg(feature = "x")))] and render the “available with feature X” badge. Cheap, lands the day a real feature flag is introduced. Renumbered from F-tight-list-paragraphs (collision with the parser tight-list fix that landed in parallel). |
| F46 | New rules (v0.3) | ✅ | v0.3 | FR corpus tuning; ships as info. Slip-flag (2026-04-24): if FR corpus tuning exceeds ~2 days, slides to 0.3.x. Ships as Experimental in v0.2.x via F-experimental-rule-status; flips to Stable at v0.3 cut. |
| F49 | New rules (v0.3) | ✅ | v0.3 | Cohort lead (2026-05-02) — first rule on the F-experimental-rule-status substrate, depends on F143 (inline AST layer). Ships as Experimental in v0.2.x; flips to Stable at v0.3 cut. |
| F51 | New rules (v0.3) | ✅ | v0.3 | Ships as Experimental in v0.2.x via F-experimental-rule-status; flips to Stable at v0.3 cut. |
| F53 | New rules (v0.3) | ✅ | v0.3 | Ships as Experimental in v0.2.x via F-experimental-rule-status; flips to Stable at v0.3 cut. Scoped 2026-05-04 (.personal/feature-torture/reports/F53.md): MVP fires when a numeric token (≥ 4 digits) or magnitude word (million / milliard / billion / trillion) appears with no same-sentence anchor — unit, percentage, ratio (X out of Y / X sur Y), or curated comparator phrase (≤ 30 entries per language under src/language/{en,fr}/). Excludes year-shaped numerals (1700–2100), ordinals, and figure / page / section refs. Profile thresholds: dev-doc = 100 000, public = 10 000, falc = 1 000. Severity at flip TBD (Warning cohort default vs Suggestion); pre-flip dogfood pass on examples/public/ gates the call. Boundary with F51 structure.number-run: F51 fires on numeric clusters; F53 fires on isolated unanchored numerals. |
| F57 | New rules (v0.3) | ✅ | v0.3 | Ships as Experimental in v0.2.x via F-experimental-rule-status; flips to Stable at v0.3 cut. Scoped 2026-05-04 (.personal/feature-torture/reports/F57.md): MVP fires when a sentence’s maximum balanced-bracket nesting depth across () and [] reaches the profile threshold. Profile thresholds: dev-doc = 4, public = 3, falc = 2. Em-dash pairs and comma-flanked appositives deferred (filed as F-syntax-appositive-depth if dogfood demands). Code spans / blocks already excluded by parser helper; unbalanced brackets fail open (mirrors parenthesised_list_comma_count in src/rules/enumeration.rs). Severity at flip TBD (Warning cohort default vs Suggestion); pre-flip dogfood pass on examples/public/ gates the threshold tuning. Boundary with F22 structure.excessive-commas: F22 already discounts flat (A, B, C) enumerations at depth 1 via parenthesised_list_comma_count; F57 fires only at depth ≥ 2, so the rules are mechanically orthogonal. |
| F-cargo-deny | Infrastructure | ☐ | v0.3+ | Supply-chain gate — cargo-deny (+ actionlint). Replace the cargo-audit step in .github/workflows/ci.yml with cargo-deny for unified CVE + license + crate-ban checks (e.g. ban lazy_static in favour of OnceLock). Pairs with actionlint in the same PR — both are CI-workflow edits, shared review surface. Adds deny.toml (advisories + MIT/Apache-2.0/BSD-3-Clause/Unicode-DFS-2016 license whitelist + bans). Split out of former F-security-hardening 2026-05-05 (see report). |
| F-gitleaks-precommit | Infrastructure | ☐ | v0.3+ | Secret-leak guard — gitleaks pre-commit hook. Add gitleaks to .pre-commit-config.yaml alongside ruff/mypy/typos (PR #58). Minimal .gitleaks.toml. Mirror as a non-blocking CI warning step for one cycle, then promote to required. Open question on first-run scope (history vs staged-only) and blocking-vs-warning posture for the first week. Split out of former F-security-hardening 2026-05-05. |
| F-infra-audit | Infrastructure | ☐ | v0.3+ | API-stability gate — cargo-semver-checks. Protect the [lib] API surface before major releases by failing CI on accidental breaking changes. Narrowed from the original actionlint + cargo-semver-checks pairing 2026-05-05 — actionlint moved into F-cargo-deny (shared ci.yml edit). |
| F-python-hygiene | Infrastructure | ☐ | v0.3+ | Python scripting hygiene — ruff + mypy. Enforce high quality on the scripts/ layer (lang-sync, text-conversion). ruff for linting/formatting (replaces black/isort/flake8); mypy for type checking (leverages existing type hints). Pre-commit + CI integration. |
| F-vale-style-pack | Adoption channels | ☐ | v0.3+ | Vale style pack — subset of rules → vale-cli/packages topic. Map only the rules that fit Vale’s existence / substitution / occurrence checks (target list: lexicon.weasel-words, lexicon.redundant-intensifier, lexicon.jargon-undefined, lexicon.unexplained-abbreviation, lexicon.all-caps-shouting — plus a couple thresholded structure rules if Vale’s conditional extends cleanly). The cognitive-load core (sentence-too-long thresholds, structure.deep-subordination, scoring engine, FALC profile) stays standalone-only. Pack is generated from the rule registry (~50 lines of Rust emitting Vale YAML) — zero hand-maintenance, regenerated per release. Each rule’s Vale link: field points to docs/src/rules/<id>.md so curiosity about gaps surfaces the standalone tool. Pack README opens with: “This is a subset of lucid-lint for Vale users. For sentence-shape, paragraph rhythm, scoring and the FALC profile, use lucid-lint standalone — see [link].” The Vale pack is intentionally a “trailer.” Risks (discovery dilution, identity blur, maintenance drag) all fall on the README + per-rule link surfaces; not cannibalisation — Vale users are a new audience, not poached existing users. |
| F-cargo-udeps | Infrastructure | ☐ | v0.3+ | Unused dependency audit — cargo-udeps. Identify and prune unused crates to minimize binary size and compile times. Runs weekly or manually (requires nightly). |
| F-diff-mode | Developer experience | ☐ | v0.3+ | Differential diagnostics — --compare=<ref> CLI mode. Runs against two revisions of the same text(s) and reports score-delta + diagnostic-delta. Pitch: CI/PR comment framing (“this PR adds 2 warnings, removes 5, net −3”), inverting alarm fatigue the way coverage tools do. CLI + JSON + SARIF-run-comparison. No dashboard (that is F-score-evolution-dashboard). |
| F-explain-fancy-rendering | Developer experience | ☐ | v0.3+ | Fancy terminal rendering for lucid-lint explain — pipe the bundled markdown through termimad (or a custom pulldown-cmark + owo-colors walker) so headings, tables, code fences, bullets, and inline code render with proper typography instead of raw markdown. Ship a toned Skin that matches the existing warning-yellow / info-cyan palette rather than termimad’s magenta defaults — the brand direction is calm, typographic, not “rich CLI”. Defer past v0.2 so the check output polish (F?) lands first. |
| F-falc-readiness-guide | Adoption channels | ☐ | v0.3+ | FALC-readiness guide page — new docs page docs/src/guide/falc-readiness.md (FR mirror at docs/src/fr/guide/falc-readiness.md) explaining how lucid-lint --profile=falc maps to the Inclusion Europe European Easy-to-Read standards. Cite the European Easy-to-Read logo program (logo use is free if conditions met: document follows the standards + at least one person with intellectual disability validated readability). Do not claim certification — claim readiness. The guide drives qualified traffic from disability-federation networks (UNAPEI, Inclusion Europe, etc.). |
| F-homebrew-tap | Adoption channels | ☐ | v0.3+ | Homebrew distribution (own tap → core). macOS-first audiences (writers, designers, docs teams) reach for brew install before cargo. Path is well-trodden: ship a tap on <org>/homebrew-tap immediately, graduate to homebrew-core once eligibility is met (current acceptable-formula policy needs a manual cross-check on homebrew/brew docs/Acceptable-Formulae.md — sandboxed during Block E recon; the old “75 stars” line was removed but maintainers still gate on adoption signal). Implementation: enable cargo-dist’s homebrew installer — it generates a Ruby formula referencing the same release tarballs we already build (aarch64-apple-darwin, x86_64-apple-darwin, plus Linux bottles) and opens a PR against our tap on each tag. Bottle building runs free on macos-latest runners. v0.4 launches the tap; homebrew-core submission deferred to v0.5+ behind real adoption signal. Risks: (1) tap fragmentation if we never graduate to core; (2) core review can take weeks. |
| F-interop-suppression | Ecosystem interop | ☐ | v0.3+ | Interop suppression mechanism. Rules declare overlapping external linter rules in their metadata (e.g. Rule::external_overlaps(), enum Linter::{Markdownlint, Vale, Proselint, Textlint}). Users opt in via [interop] suppress_when = ["markdownlint"] in lucid-lint.toml (CLI equivalent: --interop-suppress=markdownlint); opt-out is default, so coverage never silently drops. When active, affected rules are skipped at emission time with an info-level trace in --verbose. Ships CLI + LSP — the LSP path is the real motivator: two servers squiggling the same span with different severities and wording erodes trust in both. Only structure.deeply-nested-lists qualifies at time of writing (MD007); framework is designed to scale to future overlaps. Non-goal: detecting whether the external linter is actually installed or configured — the config field is the signal. |
| F-lexicon-acronym-distance | New rules (v0.3) | ☐ | v0.3+ | lexicon.acronym-distance-from-definition |
| F-lexicon-falc-idiom | New rules (v0.3) | ☐ | v0.3+ | Curated bilingual idiom lexicon |
| F-nlp-plugin | Plugins / NLP | ☐ | v0.3+ | lucid-lint-nlp plugin specification and scaffolding (Python subprocess or WASM-based). Replaces heuristic rules with POS- / dependency-tree- / anaphora-backed precise versions. Ship only when the first plugin rule is concretely scheduled — scaffolding-without-consumer is the red flag from AGENTS.md directive #1 (2026-04-24 brainstorm-next-cycles). |
| F-openssf-silver | Infrastructure | ☐ | v0.3+ | OpenSSF Best Practices — Silver/Gold prefill pass. Passing earned 2026-05-06 (project #12748). Re-evaluate Silver after v0.3 cuts: identify trivially-Met criteria (documentation_security, installation_common, external_dependencies, etc.) and flip them; the harder blockers are people-shaped (≥2 unassociated significant contributors for Silver, ≥2 active developers signing releases for Gold) and unlock with adoption, not paperwork. Security is a must but not a central project goal — keep this opportunistic, not a forcing function. |
| F-rhythm-topic-shift-cluster | New rules (v0.3) | ☐ | v0.3+ | May merge into F-missing-connectors after corpus review |
| F-syntax-address-inconsistency | New rules (v0.3) | ☐ | v0.3+ | FR primary (tu / vous); EN weaker (you / one) |
| F-syntax-complex-tense | New rules (v0.3) | ☐ | v0.3+ | FR morphology primary; EN lighter |
| F58 | New rules (v0.3) | ☐ | v0.3+ | FR corpus validation (dislocation FP risk) |
| F-lede-buried | Research track | ☐ | v0.3+ | structure.lede-buried — journalistic inverted-pyramid check. Strong candidate for a future lucid-lint-journalism plugin rather than core. |
| F-lychee-retry | Infrastructure | ☐ | v0.3+ | Lychee link-check — retry-on-flake. Add --max-retries 3 --retry-wait-time 10 to the lychee invocation in .github/workflows/ so transient cloud-runner network resets (e.g. Connection reset by peer (os error 104) on vale.sh, observed PR #65 merge 2026-05-06) don’t fail the run. Cosmetic redirect [WARN] lines stay non-blocking. Open only after a second flake — one occurrence is the noise floor. |
| F-mdbook-lint-coexistence | Adoption channels | ☐ | v0.3+ | mdbook-lint coexistence guide. Short page in our docs (and a one-liner cross-PR to mdbook-lint’s README) explaining “use both”: mdbook-lint = markdown structure, lucid-lint = prose / cognitive load. Different niches, complementary. Free, opportunistic. |
| F-paragraph-landmark-density | Research track | ☐ | v0.3+ | structure.paragraph-landmark-density — reprise-points for attention-fragile readers. Research needed to define “landmark” (bold / italic / headers / list-starts / code spans?). |
| F-pre-commit-hook-listing | Adoption channels | ☐ | v0.3+ | Pre-commit hook listing in pre-commit/pre-commit registry. Fires once --check mode is stable across our CLI surface (currently most surfaces use --format=json and exit codes; hook-friendly summary + fast-fail mode is the prerequisite). |
| F-rule-discovery-corpus | Research track | ☐ | v0.3+ | Rule-discovery corpus project — mine writer-heavy git histories for patterns that authors repeatedly rewrite. Source of evidence-grounded rule proposals. Intern / student project scale. |
| F-semgrep-rust | Infrastructure | ☐ | v0.3+ | Semgrep with the default Rust ruleset as a PR-required check. Routed 2026-05-07 from the F-adversarial-review track-2 (rule-engine reviewer set) decision. cargo-deny + actionlint shipped (covers supply-chain + workflow lint); CodeQL already runs weekly via F-repo-config-hardening. Semgrep would add structural pattern matching (unsafe patterns, anti-pattern detection) that overlaps with neither cargo-deny nor CodeQL. Unlock signal: first concrete adversarial-review finding (LLM or human) that points at a structural pattern Semgrep would have caught — e.g. a missed unwrap() on a fallible boundary, or an anti-pattern accumulating across rule modules. Until then, the cost (CI minutes + ruleset triage to silence noise) outweighs the marginal signal over CodeQL. Open question on first activation: pin the default Rust ruleset vs. authoring lucid-lint-specific custom rules; pick at unlock time, not now. |
| F-wasm-playground | Adoption channels | ☐ | v0.3+ | WASM playground for in-browser linting. Peer pattern (ruff play.ruff.rs, biome biomejs.dev/playground): single-page React/Preact + Vite app driving a Monaco editor, with a dedicated *_wasm Rust crate built via wasm-pack (ruff publishes ruff_wasm; biome publishes @biomejs/wasm-web from biome_wasm). Source layout: a playground/ workspace at repo root with wasm/ and web/ sub-trees. Hosting: Cloudflare Pages or GitHub Pages on a subdomain (e.g. play.lucid-lint.dev). Proposed shape for lucid-lint: crates/lucid-lint-wasm exposing lint(text, lang, profile) -> Diagnostic[] via wasm-bindgen; tiny Vite+Preact UI; estimated 300–600 kB gzipped given our deterministic core (no network, no LLM). Phase: v0.4+ — the surface needs its own brainstorm before scoping (UX shape, share-link encoding, persistence, mobile experience, contribution channel) and is best framed as a traction / acquisition lever once v0.3 distribution is in place. Risks: (1) bundle-size cliff if regex + unicode-segmentation push past 1 MB; (2) ongoing maintenance of a JS surface that can drift from CLI behaviour. |
| F-lexicon-vocabulary-rarity | New rules (v0.3) | ✅ | v0.3+ | Frequency lexicon per language (Lexique.org for FR, COCA / Google-Books for EN). Tiered weights: common / context-dependent / expert. LLM-built fallback only. |
| F-prose-audits | Infrastructure | ✅ | v0.3+ | Prose & Doc audits — typos + lychee. typos for fast source-code spell checking (aligned with lucid-lint mission); lychee for link integrity across docs/ and README.md. |
| F-rhythm-pronoun-density | New rules (v0.3) | ✅ | v0.3+ | rhythm.pronoun-density |
| F-syntax-impersonal-voice-heavy | New rules (v0.3) | ✅ | v0.3+ | syntax.impersonal-voice-heavy |
| F123 | Adoption channels | ✅ | v0.3+ | Shipped 2026-04-28 — curl-pipe-sh + PowerShell one-liners are surfaced in README.md and docs/src/guide/installation.md. The cargo-dist installer flip itself was a no-op — installers = ["shell", "powershell"] has been in Cargo.toml [workspace.metadata.dist] since the initial scaffold (d153ad8), so v0.1.1 / v0.2.0 / v0.2.1 / v0.2.2 have all been attaching lucid-lint-installer.sh and lucid-lint-installer.ps1 to their GitHub Releases. Yesterday’s Block E recon mis-filed F123 as a config flip; today’s reconnaissance confirmed the actual gap was discoverability. Documentation now covers both one-liners (Linux / macOS / WSL via `curl … \ |
| F77 | Ecosystem interop | ✅ | v0.3+ | Shipped in v0.2 — main.rs now auto-discovers lucid-lint.toml walking up from the CWD (stopping at the nearest .git boundary) and applies [default].profile, [default].conditions, [scoring] via ScoringFileConfig::into_scoring_config, and [rules.readability-score].formula. New --config <path> flag overrides discovery. Precedence: built-in profile defaults → TOML → CLI flags. Per-rule TOML overrides beyond readability.score extend rule-by-rule as each Config gains Deserialize. See docs/src/guide/configuration.md. |
| F-asciidoc-support | Misc | ☐ | v0.4 | F-asciidoc-support / F-html-support / F-docx-support / F-pandoc-companion — alternative formats (AsciiDoc / HTML / .docx / pandoc bridge) |
| F-llm-plugin | Misc | ☐ | v0.4 | ≥ 2 concrete LLM-as-Judge rules designed on paper; deterministic-core base stable enough that non-determinism is a clear opt-in |
| F-rhythm-forward-reference-heavy | Misc | ✅ | v0.4 | F-rhythm-forward-reference-heavy – F-syntax-address-inconsistency — remaining condition-tag rules |
| f-external-feedback-top3 | Misc | ☐ | Later | (stub) |
| f-figurative-language | Misc | ☐ | Later | (stub) |
| f130 | Misc | ☐ | Later | (stub) |
| f131 | Misc | ☐ | Later | (stub) |
| f132 | Misc | ☐ | Later | (stub) |
| F-config-pinning | Suppression / config | ☐ | Speculative | Rule-set pinning for strict-CI consumers. Spawned from F-release-policy-codify open question: under the lifecycle policy, minor releases may flip rules Experimental → Stable, which can change CI-failing diagnostics for users running --deny warnings-equivalent strict modes. A pin mechanism — e.g. lucid-lint.toml [ruleset] pin = "v0.3" — would freeze the active default rule set against a release marker, decoupling lint output stability from binary upgrade cadence. Promote when the first strict-CI user reports churn from a flip. Until then, lifecycle policy + per-rule opt-out via the [[ignore]] config block (F19, shipped v0.2.0) is the documented escape hatch. |
| F-rule-removal-policy | Architecture | ☐ | Speculative | Rule deprecation + removal mechanics. Companion to F-release-policy-codify: the lifecycle policy says removing a default-fire rule needs a major bump, but doesn’t yet specify the deprecation path (warn-on-config-load grace period, opt-out shim, CHANGELOG framing). Promote when the first real removal candidate surfaces — premature codification risks inventing ceremony before we know what shape removal takes in practice. Likely shape: one-minor deprecation window with [deprecated] config marker emitting a startup warning, then removal in the next major. Defer until first removal candidate. |