Files
Charles Wiltgen c689651ff4 fix(scripts): docs-dashes flags multi-hyphen list separators (Axiom-b6p)
The 12i check matched a single wrong-separator char then whitespace, so a double hyphen ` -- ` escaped — the second hyphen sat where \s was expected. Same blind spot one level below the single-hyphen fix the check's comment brags about. Quantify the class (WRONG_SEP+) so runs of 2+ are caught; add double/triple-hyphen fixtures, keep the -5/-v regression guards. Closes Axiom-b6p.
2026-07-18 11:40:45 -07:00

89 lines
3.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Docs dash-convention detection (pre-deploy check 12i).
*
* Enforces `.claude/rules/documentation-style.md` §Dashes: a *list-led inline-heading
* separator* — a `**bold**` / `[link](url)` / `` `code` `` head at the start of a bullet
* or numbered list item, immediately followed by the separator — must use a spaced
* EN-dash " " (U+2013). Running prose keeps the spaced em-dash, so the anchored
* pattern only ever looks at the separator position and never touches prose.
*
* Extracted from pre-deploy.ts so the pattern is unit-testable. It previously lived
* inline and untested, and flagged ONLY the em-dash — so `- **Label** - desc` (ASCII
* hyphen) passed silently and six violations shipped on a docs page, past the very gate
* meant to catch them. See docs-dashes.test.ts.
*
* Scope: `docs/` only. For-LLM skill files are exempt by design (not human reading
* material) — they carry ~1,271 such separators and scanning them is meaningless.
*/
/**
* Every WRONG separator, enumerated.
*
* U+2013 EN DASH is the ONLY legal one here, so enumerate the whole complement rather
* than adding wrong dashes one at a time — that incremental habit is what let the ASCII
* hyphen through. Covers: ASCII hyphen, U+2010 HYPHEN, U+2011 NON-BREAKING HYPHEN,
* U+2012 FIGURE DASH, U+2014 EM DASH, U+2015 HORIZONTAL BAR, U+2212 MINUS SIGN.
*
* The hyphen is ESCAPED deliberately. A trailing `-` in a character class is literal, so
* an unescaped `[—-]` happens to work — but appending the Unicode minus (the obvious next
* edit) gives `[—-]`, a silent RANGE U+2014U+2212 matching ~510 code points. Escaping
* makes the class order- and insertion-proof.
*/
const WRONG_SEP = "[\\-\\u2010\\u2011\\u2012\\u2014\\u2015\\u2212]";
/**
* The trailing `\s` is LOAD-BEARING: it is what excludes negative numbers and CLI flags
* (`- **Delta** -5 degrees`, `- **Usage** -v for verbose`). Do not "tidy" it away, or
* every such line starts blocking commits.
*
* Matches inline links in the `[text](url)` form only — not reference links
* (`[text][ref]`). That is exhaustive for VitePress docs; widen the alternation if
* reference-link heads ever appear.
*/
export const dashSepPattern = new RegExp(
"^\\s*(?:[-*]|\\d+\\.)\\s+(?:\\*\\*[^*]+\\*\\*|\\[[^\\]]+\\]\\([^)]+\\)|`[^`]+`)\\s+" +
// `+` matches a RUN of wrong separators, so ` -- ` / ` --- ` are caught, not just a
// single wrong char — the single-char form let ` -- ` through the very check hardened
// for the single hyphen. The trailing `\s` still excludes `-5` / `-v`. (Axiom-b6p)
WRONG_SEP + "+\\s",
);
/** True when a single line is a list-led inline-heading separator using the wrong dash. */
export function isDashViolation(line: string): boolean {
return dashSepPattern.test(line);
}
/**
* Scan one file's contents, returning the 1-indexed line numbers that violate the rule.
* Skips fenced code blocks and YAML frontmatter — a bullet inside a ```markdown example
* is illustrative, not prose, and must not be flagged.
*/
export function findDashViolations(contents: string): number[] {
const lines = contents.split("\n");
const violations: number[] = [];
let inFence = false;
let inFrontmatter = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (i === 0 && line.trim() === "---") {
inFrontmatter = true;
continue;
}
if (inFrontmatter) {
if (line.trim() === "---") inFrontmatter = false;
continue;
}
const trimmed = line.trim();
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
inFence = !inFence;
continue;
}
if (inFence) continue;
if (isDashViolation(line)) violations.push(i + 1);
}
return violations;
}