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
+9 -1
View File
@@ -43,7 +43,7 @@ function detectorSection(raw) {
return raw && raw.detector && typeof raw.detector === 'object' && !Array.isArray(raw.detector) ? raw.detector : null;
}
const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem']);
const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem', 'advisoryRules']);
const DEFAULT_DETECTION_CONFIG = Object.freeze({
ignoreRules: [],
@@ -71,6 +71,11 @@ function cloneRawDetectionConfig() {
function applyDetectionConfigSource(config, raw) {
if (!raw || typeof raw !== 'object') return config;
// Advisory rules are opt-in for the design hook; the CLI carries the setting
// so config round-trips (e.g. `impeccable hooks ignore-value`) preserve it.
if (raw.advisoryRules === 'include' || raw.advisoryRules === 'exclude') {
config.advisoryRules = raw.advisoryRules;
}
if (raw.designSystem && typeof raw.designSystem === 'object' && !Array.isArray(raw.designSystem)) {
config.designSystem = {
...config.designSystem,
@@ -151,6 +156,9 @@ function normalizeDetectionConfigForWrite(config) {
out.ignoreFiles = uniqueStrings(config.ignoreFiles.filter(v => typeof v === 'string' && v.trim()).map(v => v.trim()));
}
out.ignoreValues = normalizeIgnoreValueEntries(config?.ignoreValues || []);
if (config?.advisoryRules === 'include' || config?.advisoryRules === 'exclude') {
out.advisoryRules = config.advisoryRules;
}
if (config?.designSystem && typeof config.designSystem === 'object' && !Array.isArray(config.designSystem)) {
out.designSystem = {
enabled: config.designSystem.enabled === false ? false : true,