Files
Clay f666087e52 feat: optional --style house-style layer (config-driven, points at Vale) (#99)
* feat: optional --style house-style layer (config-driven, points at Vale)

Implements the config-driven design recorded in #88, which #76 and the fork
review converged on. No bundled guides: the layer applies register/voice
directives and removes AI tells on top of whatever mechanics you enforce.

--style ./house.json applies a user-supplied JSON config (register +
mechanics) and scripts/check-style.js verifies the CHECKABLE mechanics
deterministically: quotes and latinAbbrev gate the exit code (0 clean /
1 hard / 2 tool error), headings, emDash and spellNumbersUpTo are advisory,
serialComma is never checked. The docs scope the verb accordingly rather
than claiming "mechanics are verified" flatly. examples/ ships two generic,
guide-neutral starters plus the schema, and links #88 for the licensing
rule.

A bare --style "APA" is a best-effort fallback, not a feature: SKILL.md
instructs the model to print a no-compliance-claim status line and not to
reproduce the guide's text. Those are instructions rather than checked
rules, and the docs say so instead of asserting the behaviour as fact.

--style is listed in the SKILL.md invocation line and has an explicit
composition rule with --voice/--context, since the file specifies
composition for every other axis.

README: the "House style is a different job" section said "There's no
--style input", which this makes false. The first paragraph now describes
the config input and reconciles with the heading (enforcing a PUBLISHED
guide is the different job). The Vale paragraph and the heading anchor are
unchanged.

cursor-rules: the ported rule would have told a Cursor user to run
scripts/check-style.js and read examples/, neither of which ships in the
.mdc. Two anchored spans in sync-cursor-rules.sh rewrite those the way
span 3 already handles detector/validate.js, so the port stays
self-contained. Worth noting the drift guard only covers edits to anchored
spans, not newly added SKILL.md sections that introduce repo references.

The checker went through three adversarial review rounds, and each round
found the previous round's fix had introduced a new way to hard-fail a
correct document or to miss a real one. The tests pin all of them: markdown
link titles, reference definitions (including escaped quotes in a title),
HTML attribute values, nested and tilde fences, BOM'd frontmatter, URLs
containing parentheses, parentheticals that wrap or span a code block, and
a stray "(" that must not suppress later findings, and prose containing a
comparison ("n<N ... >") that must not be mistaken for an HTML tag. Masking runs refdef ->
links -> tags, in that order, because masking a link destination first
makes an ordinary "[a](url): text" line look like a reference definition.
Link masking walks to the matching paren and scans linearly; the regex form
was quadratic (22.7s on a hostile line, now 4ms). resolveConfig decides
path-vs-name by shape so a cwd file can't shadow a bundled example, the CLI
parses by position, a config with "mechanics": null or [] exits 2 rather
than a false-green 0, and advisory findings print instead of hiding behind
--json.

Version 3.22.3 -> 3.23.0 in SKILL.md frontmatter, package.json and
plugin.json; CHANGELOG entry under a dated heading; plugin and cursor
copies regenerated.

No detector changes: catalog counts unchanged (61 / 112). 32 tests in
scripts/check-style.test.js under npm test; detector-test.yml path filters
extended to scripts/** and examples/**.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: mask indented code, tighten frontmatter open, announce the resolved mode

Review fixes applied on top of the submitted branch:

- Indented code blocks (4+ spaces / tab after a blank line, outside a list)
  are masked, so pasted terminal output can't hard-fail a correct document.
  Lazy continuation and list-item content stay checked; a code block nested
  in a list item needs a fence. Was the largest undocumented false positive.
- A document opening with a thematic break is no longer read as frontmatter
  (which silently swallowed everything to the next ---).
- The bare-name traversal regex was dead code its test never reached; a bare
  name is a single path segment by construction, so the guard is now a
  comment stating that argument, and a test pins dot-bearing names.
- Config runs open by naming the resolved config, mirroring the fallback's
  status line, so which of the two modes ran is always explicit in output.
- Known-limitations lists gain the double-backtick span and next-line
  reference-definition title gaps; examples/README.md joins the self-scan
  budgets; CHANGELOG updated; cursor-rule anchors follow the SKILL.md edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com>
2026-08-03 17:48:46 -07:00
..

House-style config examples

--style adds a house style on top of the de-AI pass. It is not a guide registry: it applies register/voice directives and removes AI tells, on top of whatever mechanics you enforce. The preferred way in is a config file (--style ./house.json, or a bare name matching examples/<name>.json): it is applied, and the checkable subset of its mechanics is verified deterministically (see the table below for which rules gate the exit code and which are advisory). The files here are examples of that format; copy one and edit it.

Where encoded guides live

For a real published guide, don't reach for a bare name or expect a bundled config: see the README's House style is a different job section, which points at Vale (where licensed, attributed guide packages live) and records the licensing decision in #88. In short: Vale enforces a guide's mechanics; this layer adds register/voice and removes AI tells; the config format below is for a quick custom house style.

This repo bundles no style guides. The example files are generic and guide-neutral (no guide names or aliases), so nothing here claims to implement a guide or tracks its edition.

A bare name resolves by filename only: --config technical loads technical.json. Because the shipped examples carry no guide names, --style chicago resolves to no config and falls back to applying the guide from the model's own knowledge as best-effort, labeled such as Applying Chicago from general knowledge (not verified; no compliance claim).. SKILL.md instructs the model to print that status line and not to reproduce the guide's text; both are instructions rather than checked rules, so treat that path as unverified. For enforcement, use Vale or write a config. The checker covers only the config path, so pointing it at an unresolvable name exits 2 (a tool error).

Schema

A config is JSON with two parts:

{
  "name": "My house style",
  "genre": "technical documentation",
  "register": [
    "Second person, active voice, present tense.",
    "No hype."
  ],
  "mechanics": {
    "quotes": "straight",
    "headings": "sentence",
    "emDash": "sparing",
    "latinAbbrev": "parentheses",
    "serialComma": true,
    "spellNumbersUpTo": 9
  }
}
  • register (list of strings) — voice/register directives the model applies as guidance. These are judgment calls, not machine-checked.
  • genre (string, optional) — what the config is written for. Don't apply a config to a genre it wasn't written for.
  • mechanics (object) — output rules, of which the checkable subset is verified by node scripts/check-style.js <file> --config <config.json>:
key values how it's checked
quotes straight | curly hard — flags the wrong mark form in prose
latinAbbrev never | parentheses | any hard — never flags any e.g./i.e.; parentheses flags them outside parentheses; any is unchecked
headings sentence | title advisory — proper nouns make sentence vs. title case ambiguous, so it can't be verified deterministically
emDash sparing | deliberate advisory — sparing flags a rate over ~1 per 1,000 words; deliberate is unchecked
spellNumbersUpTo number advisory — flags numerals at or below the threshold in prose
serialComma true | false model-applied only; not machine-checked

Unrecognized keys or values are reported as warnings (a config the tool couldn't fully apply) rather than silently ignored; omitted keys do nothing.

Before checking, the checker skips YAML frontmatter (only when it closes), code (fenced and inline), and markdown link destinations, link titles, and reference-definition tails, so identifiers, examples, and link syntax don't false-positive. It also masks HTML tags, whose attribute values are straight-quoted. Link titles matter here because they are delimited with straight quotes as syntax, which quotes: curly would otherwise read as a violation. Some limits worth knowing: an unclosed or multi-line HTML tag still registers, as do quotes inside an HTML comment; and the latinAbbrev parenthesis carve-out tracks depth across wrapped lines but resets at a paragraph break, so an unclosed ( disables that rule for the rest of its paragraph. Indented code blocks are masked, with a list exception: 4-space content inside a list item is the item's own prose (use a fence there). A double-backtick code span whose body contains a backtick leaks to the quote checks; a reference definition with its title on the next line is read as prose; and a document opening with a thematic break is prose, not frontmatter.