Make em-dash-overuse an advisory rule with browser parity

Em-dashes are used legitimately by humans, so em-dash-overuse fired far too
often. Reclassify it as the first advisory-tier rule: detected, but never a
failure.

Engine
- Add `advisory: true` to the rule metadata schema (em-dash-overuse is the
  first). findings.mjs stamps `advisory: true` on advisory findings so every
  consumer can partition without a registry lookup. Rule count stays 58.
- Raise the firing threshold from a flat 5 dashes to two gates: an absolute
  floor of 8 and a density of about one dash per 500 characters of body text.
  A long article that uses a few em-dashes no longer trips; a short,
  dash-per-clause page still does. Entity decoding (mdash, numeric, hex) is
  unchanged. Thresholds live in shared/constants.mjs so every engine agrees.

Browser parity
- The browser bundle carried a registry entry but no logic, so the overlay and
  extension could never flag it. Add checkEmDashOveruse / checkEmDashOveruseDOM
  in rules/checks.mjs (reads rendered text, no entity decoding needed), wire it
  into the injected page-level pass, and carry the advisory flag through
  serializeFindings so the overlay/extension can render it with the mildest
  affordance.

CLI
- Advisory findings print under a separate dimmed "Advisory" section, are
  excluded from the failure count, and never change the exit code (an
  advisory-only scan exits 0). JSON keeps them with `"advisory": true`.
  `--no-advisory` suppresses them entirely.

Hook
- Advisory rules are skipped by default in both the per-edit and Stop deep-pass
  hooks, so the hook never nags about them. Opt in with
  `.impeccable/config.json` -> `detector.advisoryRules: "include"`.

Tests
- Fixture + threshold + browser-adapter coverage; advisory-skip default and
  opt-in for the hook; formatFindings partitioning. The em-dash-overuse stand
  for a deferred copy rule in the tier tests is swapped to marketing-buzzword.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-22 12:23:06 -07:00
co-authored by Claude Fable 5
parent e409bec7b5
commit 270f4d20aa
12 changed files with 411 additions and 57 deletions
+31
View File
@@ -1,5 +1,7 @@
import {
BORDER_SAFE_TAGS,
EM_DASH_CHARS_PER_DASH,
EM_DASH_FLOOR,
GENERIC_FONTS,
KNOWN_SERIF_FONTS,
OVERUSED_FONTS,
@@ -2594,6 +2596,33 @@ function checkNumberedSectionLabelsDOM() {
return checkNumberedSectionLabels({ candidates });
}
// Em-dash overuse (ADVISORY) — pure logic shared by the browser DOM check.
// Mirrors the regex/static-HTML analyzer in engines/regex/detect-text.mjs:
// two gates (absolute floor + density) so a long article using a few dashes is
// left alone while a short, dash-per-clause page is flagged. Operates on
// already-rendered text, so no HTML-entity decoding is needed (the browser has
// resolved `&mdash;` to the literal glyph). Exported for jsdom unit tests.
function checkEmDashOveruse(text) {
const body = typeof text === 'string' ? text.replace(/\s+/g, ' ') : '';
let count = 0;
const re = /[—]|--(?=\S)/g;
while (re.exec(body) !== null) count++;
if (count < EM_DASH_FLOOR) return [];
if (body.length > count * EM_DASH_CHARS_PER_DASH) return [];
return [{ id: 'em-dash-overuse', snippet: `${count} em-dashes in body text` }];
}
function checkEmDashOveruseDOM() {
const body = document.body;
if (!body) return [];
// innerText reflects rendered, visible text; fall back to textContent for
// engines (jsdom) that don't compute innerText.
const text = typeof body.innerText === 'string' && body.innerText
? body.innerText
: (body.textContent || '');
return checkEmDashOveruse(text);
}
function checkElementMotionDOM(el) {
const tag = el.tagName.toLowerCase();
if (SAFE_TAGS.has(tag)) return [];
@@ -5121,6 +5150,8 @@ export {
checkNumberedSectionLabels,
checkNumberedSectionLabelsFromDoc,
checkNumberedSectionLabelsDOM,
checkEmDashOveruse,
checkEmDashOveruseDOM,
isRepeatedTextContainer,
collectRepeatedContainerTextFindings,
checkRepeatedContainerTextFromDoc,