Skip to content

Edit-time docs style enforcement via Vale + PostToolUse hook#36

Merged
vraspar merged 13 commits into
mainfrom
vraspar/docs-writing-skill
May 19, 2026
Merged

Edit-time docs style enforcement via Vale + PostToolUse hook#36
vraspar merged 13 commits into
mainfrom
vraspar/docs-writing-skill

Conversation

@vraspar
Copy link
Copy Markdown
Contributor

@vraspar vraspar commented May 17, 2026

Summary

Vale + project-memory layer for x402r docs writing. The companion PostToolUse hook and writing-docs skill live in workspace .claude/ — already on BackTrackCo/x402r-workspace main (workspace hook commit d5acf69, skill rebuild ff1eb65).

  • Remove AllowsYouTo rule (stock proselint + write-good packs cover the same patterns)
  • Remove FirstPersonPlural rule (stock Microsoft.We rule covers the same we/our/let's surface — keeping both produces double warnings)
  • .vale.ini parses .mdx as Markdown so local Vale runs without the separate mdx2vast binary (CI's errata-ai/vale-action is unaffected — it installs its own MDX parser server-side)
  • Slim CLAUDE.md to project context + hard constraints + pointer to the workspace-level writing-docs skill

Why

Voice inconsistency and AI slop were surviving the PR-only Vale check. Pairing this PR with the workspace PostToolUse hook gives Claude immediate Vale feedback while editing — violations surface as additional context for the next turn, no waiting for CI. Custom rules removed because stock packs already cover the same surface.

Test plan

  • Install Vale locally: brew install vale
  • Run cd docs && vale sync to download Microsoft/proselint/write-good packs (one-time)
  • From workspace root, ask Claude to edit any .mdx file with we/our/let's and confirm Vale output appears in the next turn (requires workspace hook commit d5acf69 already on workspace main)
  • From workspace root, ask Claude to edit a non-.mdx file (e.g. docs.json) and confirm Vale doesn't run
  • Run vale path/to/any.mdx locally to confirm rule set: Slop, Enthusiasm, Microsoft.We all firing on relevant patterns; AllowsYouTo and FirstPersonPlural removed
  • Confirm existing CI workflow (errata-ai/vale-action) still runs on PR

🤖 Generated with Claude Code

vraspar and others added 7 commits May 17, 2026 15:36
Inconsistent voice and AI slop survived our PR-time-only Vale check.
This adds:

- PostToolUse hook in .claude/settings.json that runs Vale on .mdx
  edits, surfacing violations immediately instead of at PR review time
- New FirstPersonPlural Vale rule blocking "we"/"our"/"let's" — the
  hardest manual rule from prior writing feedback
- Remove AllowsYouTo rule; proselint + write-good cover the patterns
- Slim CLAUDE.md to project context + hard constraints + skill pointer

The full prose style guide moves to a workspace-level writing-docs
skill (separate change).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The PostToolUse hook would error on every .mdx edit because Vale
needs a separate mdx2vast binary to parse MDX, which isn't bundled
with the brew install. Treating .mdx as Markdown via [formats] lets
Vale lint prose with zero external dependencies.

CI is unaffected — errata-ai/vale-action installs its own MDX parser.

Verified locally: FirstPersonPlural, Slop, and Microsoft.We all fire
on a test .mdx fixture as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Same diagnosis as the Vale hook commit: voice and structure feedback
weren't landing consistently because nothing forced Claude to load
the existing prose guide before writing.

This wraps the guide as a Claude Code Skill with progressive
disclosure (short SKILL.md + on-demand references):

- SKILL.md: 10-step workflow (clarify, load refs, match brief, read
  source, read neighbors, outline, draft, quality gates, vale,
  preview). Auto-triggers on docs/**/*.mdx work.
- references/style-guide.md: 7 principles, anti-slop list, sentence
  rules, style exemplar
- references/page-briefs.md: per-page creative briefs for Getting
  Started, Guides, API Reference, Configuration
- references/quality-gates.md: 7-gate pre-merge checklist + Vale
  enforcement notes
- references/source-files.md: critical source files map + role
  availability table + protocol facts

The skill points at the PostToolUse hook added earlier on this
branch so Vale runs automatically after each .mdx edit and Claude
self-corrects before the user sees the draft.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The writing-docs skill now lives at the monorepo workspace .claude/
skills/ so it auto-loads from any cwd. Keeping a second copy here
just creates a sync burden. CLAUDE.md updated to point at "the
writing-docs skill" by name rather than by path.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Subdirectory .claude/settings.json files aren't documented to load
in Claude Code (only workspace root and ~/.claude/settings.json).
The Vale hook now lives at workspace .claude/settings.json with an
'if' filter scoping it to docs/**/*.mdx — same effect, actually
supported.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Custom Vale rule duplicated the Microsoft style pack's `Microsoft.We`
rule, which catches the same we/our/let's surface. Keeping both
produces double warnings on every match. Stock pack wins.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Vale's default spell-check flags every project-specific term as
unknown (config, Sepolia, Hono, middleware, USDC, x402r, etc).
Adding a Vocab file at styles/config/vocabularies/x402r/accept.txt
silences the false positives without disabling the rule entirely.

Also gitignore the Microsoft/proselint/write-good style packs —
those are fetched via `vale sync` per machine and shouldn't live in
the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@A1igator
Copy link
Copy Markdown
Contributor

Tried this out locally on this branch. Setup was clean: brew install vale + vale sync + vale index.mdx worked first try without mdx2vast, so the .vale.ini mdx-as-md change does what it claims.

One real finding: the vocab in styles/config/vocabularies/x402r/accept.txt is missing several common chain/term names that appear all over the docs. Vale.Spelling flags them as errors right now:

On index.mdx (supported-networks table, lines 91–100):

  • Ethereum, Arbitrum, Celo, Linea, Testnet

On roadmap.mdx:

  • pluggable, Arweave, combinators, ABIs, Subgraph / subgraph

These aren't slop, they're load-bearing nouns, so they shouldn't fire. Without adding them, the PostToolUse hook will dump a wall of false-positive spelling errors into Claude's context on every .mdx edit and drown out the actually-useful rules (Microsoft.Dashes, Microsoft.We, write-good.Passive, etc.).

Suggest extending accept.txt before this merges, otherwise the first few edits post-merge will probably need a follow-up PR anyway.

Default error severity drowns out style/voice rules whenever a page
contains chain names, identifiers, or contract names (Ethereum,
Arbitrum, pluggable, combinators, ABIs, etc). Per A1igator's PR
review, the noise floods the PostToolUse hook output and buries
actually-useful rules (Microsoft.Dashes, Microsoft.We, etc).

Follows GitLab's tiered Vale convention: error = render-breaking,
warning = style guide, suggestion = preference. Spelling false
positives don't break builds and don't make sense as the loudest
signal. Real-typo detection lives in editor review.

The existing vocab file stays — it now serves Vale.Terms (case
enforcement, e.g. flags 'hono' → 'Hono', 'sdk' → 'SDKs?') which is
genuinely useful and not what the downgrade addresses.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 18, 2026

Good catch. Verified the same flood on index.mdx and roadmap.mdx locally (78 distinct spelling false positives across docs/, with npm, pluggable, combinators, escrowed, Ethereum, etc. as the worst offenders).

Took a different path than vocab expansion in c9941c9: downgraded Vale.Spelling to suggestion.

Reasoning:

  • Vocab expansion treats the symptom — every new chain/contract/identifier means another vocab update. Maintenance burden grows with the docs.
  • The actual issue is severity: Vale.Spelling defaults to error, which dominates the PostToolUse hook output and buries the style/voice rules that matter (Microsoft.Dashes, Microsoft.We, write-good.*).
  • Per GitLab's tiered Vale convention: error = render-breaking, warning = style guide, suggestion = preference. Spelling false positives belong at suggestion.
  • Real-typo detection lives in editor review (your eyes during PR review) or a separate spell-checker in CI if we want it later.

Side benefit: the existing vocab file stops being a maintenance liability but stays useful — Vale.Terms now uses it for case enforcement (e.g. flags honoHono, sdkSDKs?). Caught two real cases on index.mdx and roadmap.mdx after the downgrade.

After-state on those pages: 14 real errors (em dashes, acronyms in headings) + 7 warnings (wordiness, etc.) + 0 spelling-noise. Clean signal.

Let me know if you'd rather we add the vocab anyway as belt-and-suspenders.

@A1igator
Copy link
Copy Markdown
Contributor

Solid reasoning on root-cause vs vocab-treadmill, and the Vale.Terms side-benefit is a real win.

One concern with the all-or-nothing downgrade: .vale.ini keeps MinAlertLevel = warning, and the CI workflow doesn't override it (vale_flags: "--glob='*.mdx'" only). So Vale.Spelling = suggestion doesn't just lower its priority, it filters spelling out of the hook output AND the CI check entirely. Real typos (recieve, transferd, unkown) now get caught by zero automated check, and "editor review" is the same fallback we had before this PR existed.

Three cheaper alternatives worth considering:

  1. Downgrade to warning instead of suggestion. Still appears below errors so it doesn't dominate the hook output, but stays visible. Simplest one-line change.
  2. Keep at suggestion + set MinAlertLevel = suggestion. Surfaces everything, sorted by severity. Better signal for editors who actually want to scan it.
  3. Seed a fixed vocab from x402rChains in x402r-sdk/packages/core/src/config/index.ts. That list is the actual source of truth for supported chains, so it's bounded by deploy decisions rather than every-new-identifier. Could even script it as a one-liner generator in CI to keep them in sync. Different shape of maintenance than the unbounded treadmill you're flagging.

Lean toward (1) personally, it's the smallest change and preserves the "spelling is real but lower priority" signal without the floor. But any of the three beats "no spell-check anywhere."

Two-part fix for the noise/detection tradeoff per A1igator's PR
review:

1. Vale.Spelling: suggestion -> warning. The earlier suggestion-tier
   downgrade combined with MinAlertLevel = warning silently dropped
   spelling from both hook output and CI, regressing typo detection
   to "editor review only". At warning level spelling still surfaces
   (catches recieve/transferd/unkown) but ranks below errors so it
   doesn't dominate.

2. Seed chain-name vocab from x402rChains in
   x402r-sdk/packages/core/src/config/index.ts. Bounded source of
   truth: only chains with deployed contracts get accepted (currently
   just Base; Sepolia was already in vocab). Regen one-liner lives in
   accept.txt so the next chain deploy triggers a clear vocab update.

Per-the-investigation finding: the noise A1igator was seeing on
docs/index.mdx (Ethereum, Arbitrum, Celo, Linea) isn't masked by
this vocab seed because those chains aren't in x402rChains -- the
docs/index.mdx Supported Networks table lists 11 networks while code
deploys to 2. Those warnings are now correctly tagged as warnings,
not errors, and serve as signal that the table is aspirational.
Separate concern from this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 18, 2026

You're right on both counts and I had it backwards. Pushed 2823c5f with both fixes:

1. Vale.Spelling = warning (not suggestion). Your catch on the MinAlertLevel interaction was exactly right — suggestion + warning-floor silently regressed spelling to zero detection. At warning now: still surfaces in hook output and CI, real typos still caught, but ranks below errors so it doesn't dominate.

2. Seeded chain vocab from x402rChains with the regen one-liner committed inline in accept.txt:

awk '/export const x402rChains/,/^} as const/' \
  ../x402r-sdk/packages/core/src/config/index.ts \
  | grep -oE "'[A-Z][a-zA-Z ]+'" | sort -u

Interesting finding while doing this: x402rChains currently has 2 chains (Base, Base Sepolia — only the deployed ones). The chain names you flagged on index.mdx (Ethereum, Arbitrum, Celo, Linea, Polygon, Optimism, Avalanche, Monad) aren't in code — the Supported Networks table at index.mdx:85-100 lists 11 networks as "Supported" while code deploys to 2. So seeding from x402rChains doesn't suppress those warnings.

That's actually the right outcome: those warnings are now correctly signal, not noise. The table is aspirational. Worth a separate doc-accuracy PR.

After-state on index.mdx + roadmap.mdx: 14 errors (em dashes, vocab case enforcement), 29 warnings (22 spelling + 7 wordiness/acronym), 0 suggestions. Spelling visible but no longer the loudest signal.

@A1igator
Copy link
Copy Markdown
Contributor

Ran Vale tree-wide on 2823c5f (after vale sync) to see what else surfaces beyond index.mdx + roadmap.mdx.

Totals

250 errors / 543 warnings / 1 suggestion across 66 MDX files.

Severity x rule:

Sev Count Rule
warning 263 Vale.Spelling
error 137 Microsoft.Dashes
warning 126 write-good.Passive
warning 93 write-good.TooWordy
error 47 Vale.Terms
error 29 Microsoft.Foreign
warning 18 Microsoft.Adverbs
warning 12 Microsoft.HeadingAcronyms
error 11 Microsoft.Auto
warning 9 Microsoft.HeadingPunctuation
error 9 proselint.Typography
error 8 write-good.ThereIs
warning 6 Microsoft.We
error 6 Microsoft.Quotes
warning 5 Microsoft.Ellipses
warning 4 x402r.Slop
warning 3 Microsoft.Terms
error 2 Microsoft.Plurals
warning 1 write-good.Weasel / write-good.Cliches / Microsoft.GeneralURL
error 1 proselint.Cliches

Real issue introduced by this PR: Vale.Terms false positives (47 errors)

Vocab entries in accept.txt are dual-use. Vale also treats them as Vale.Terms canonical-case rules at error severity, and several entries misfire:

Vocab entry Demand Reality
SDKs? "Use 'SDKs?' instead of 'sdk'" 24x. The regex ? leaks through as literal replacement text.
paymentInfo "Use 'paymentInfo' instead of 'PaymentInfo'" 10x. Flags the legitimate TS type name.
Base (chain) "Use 'Base' instead of 'base'" 8x. Fires on the base code identifier (chain key, imports).
PaymentInfoHash "Use 'PaymentInfoHash' instead of 'paymentInfoHash'" 2x. Type vs variable collision.
[Bb]igint "Use '[Bb]igint' instead of 'BigInt'" 1x. Would suggest the JS built-in be replaced with a regex string.
Hono "Use 'Hono' instead of 'hono'" 1x. Flags the npm package name in import paths.
async "Use 'async' instead of 'Async'" 1x.

Examples:

contracts/periphery/auth-capture-escrow.mdx:68:131:Vale.Terms:Use 'paymentInfo' instead of 'PaymentInfo'.
contracts/examples.mdx:8:313:Vale.Terms:Use 'SDKs?' instead of 'sdk'.
roadmap.mdx:62:68:Vale.Terms:Use 'Hono' instead of 'hono'.
contracts/periphery/refund-request.mdx:86:35:Vale.Terms:Use 'PaymentInfoHash' instead of 'paymentInfoHash'.

Fix options (any combination):

  1. Restrict Vale.Terms to suggestion or NO in .vale.ini so vocab is spelling-only.
  2. Strip regex chars from accept.txt: split SDKs? into SDK + SDKs, replace [Bb]igint with bigint + Bigint, etc.
  3. Drop entries whose case is genuinely ambiguous between proper noun and code identifier: Base, paymentInfo, PaymentInfoHash, async, Hono. Let them ride as ignored spelling instead of enforced canonical case.

Em dashes (137 errors)

CLAUDE.md hard-bans em dashes but they're spread across 30+ files. Worst offenders:

 20  x402-integration/escrow-scheme.mdx
 14  sdk/client/quickstart.mdx
 14  contracts/gas-costs.mdx
 10  sdk/arbiter/quickstart.mdx
  8  roadmap.mdx
  8  contracts/conditions/overview.mdx
  7  contracts/license.mdx
  7  contracts/fees.mdx
  7  contracts/audits.mdx
  6  contracts/recorders/overview.mdx
  5  contracts/conditions/escrow-period.mdx
  4  index.mdx
  3  sdk/arbiter/ai-integration.mdx
  3  contracts/conditions/freeze.mdx
  3  contracts/conditions/custom.mdx

This PR exposes them but doesn't fix them. Separate cleanup PR territory, but worth calling out as the scope outside index.mdx + roadmap.mdx.

Spelling warnings (263, 108 unique words)

Many are stable technical terms worth batch-adding to vocab:

  • Already-canonical primitives: reentrancy, multisig, timelock(s), timelocked, permissionless, mempool, combinator(s), pluggable, composable, composability, dApps, gwei, calldata, hardcoded, untrusted, trustlessness, subgraph, testnet(s), escrowed, Gasless, Micropayments, Swappable, deduplication, deserialize, Deregister
  • Proper nouns: Coinbase, Spearbit, Solady, Ownable, Uniswap, Aave, Mintlify, Anthropic, Windsurf, Arweave, Flashbots, Licensor, Abdoli, Vrajang, Parikh
  • Acronym plurals: LLMs, APIs, ABIs, CIDs, UIs, URIs, txns
  • Tooling: npm, npx, pnpm, mkdir, tsx, repo, dotenv, multicall
  • Type/identifier names appearing in prose (could be backticked instead, but currently bare): ICondition, IRecorder, arbiterAddress, arbiterCondition, authorizeCondition, authorizeRecorder, chargeCondition, chargeRecorder, releaseCondition, releaseRecorder, freezeAddress, freezeBy, freezeDuration, freezePayment, freezePolicy, isFrozen, unfreezePayment, escrowAddress, escrowPeriod, releaseBy, paymentExists, paymentId, requestRefund, collectorData, feeCalculator, feeRecipient, txHashes, existingCount, newCount, zeroHash, operatorConfig, deployedAt, capturable

Chain names Ethereum / Arbitrum / Celo / Linea / Polygon / Optimism / Avalanche / Monad continue to fire. Already documented as intentional signal pointing to the aspirational Supported Networks table in index.mdx.

Other smaller error rules

  • Microsoft.Foreign (29): every e.g., and i.e., is an error. Either soften to warning, or full-pass replace with "for example" / "that is".
  • Microsoft.Auto (11): auto-expires, auto-expiry hyphenation.
  • proselint.Typography (9): ... should be .
  • write-good.ThereIs (8): "There is/are" sentence openers.
  • Microsoft.Quotes (6): smart-quote style.
  • Microsoft.Plurals (2): (es) parenthetical plurals.
  • proselint.Cliches (1).

Recommendation

Before merging, address the Vale.Terms regression. Those 47 errors are net-new false positives that the seeded accept.txt introduces, including a few that point users at literal regex syntax (SDKs?, [Bb]igint). Everything else (em-dash sweep, vocab expansion for legit terms, deciding the fate of e.g. / i.e.) is fair game for follow-up PRs.

Pair with workspace commit c193202, which adds
--filter='.Name != "Vale.Spelling"' to the PostToolUse hook command.

Strict-in-CI / lenient-in-hook architecture per established practice
(GitLab Vale docs, Vaadin .vale-pr.ini, ESLint warnings-as-anti-
pattern). Spell-check at default error level in CI gates real typos
before merge. The hook filters Vale.Spelling out of Claude's
self-correction context so chain-name and identifier false positives
don't bury Microsoft.Dashes / write-good rules.

Why this beats the warning-tier compromise from 2823c5f:
- Hook noise drops to ZERO instead of "less but still there".
- CI signal stays at error tier where it belongs.
- Single .vale.ini, no two-config sync footgun.
- Native --filter, not a fragile grep post-process.

A1igator's review pushed this in the right direction. Earlier
compromise (Vale.Spelling = warning) reduced noise by one tier but
didn't fix the architectural issue — wrong place to apply the lever.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 18, 2026

Going to defer to you on whether to merge as-is or hold for further review, but here's the architectural cleanup your pushback drove.

Pair of commits — workspace c193202 + this PR f662841 — implements the strict-in-CI / lenient-in-hook pattern that GitLab, Vaadin, and the ESLint ecosystem all use. Your pushback on the suggestion-tier bug got me to research what production docs setups actually do; turns out the right lever isn't severity tuning OR vocab maintenance — it's channel scoping.

Workspace .claude/settings.json (BackTrackCo/x402r-workspace c193202):

-  vale "$f" 2>&1
+  vale --filter='.Name != "Vale.Spelling"' "$f" 2>&1

PostToolUse hook now drops Vale.Spelling alerts from the JSON payload it injects into Claude's context. Uses Vale's native AST-level --filter flag (https://vale.sh/docs/cli), not a fragile post-hoc grep.

This PR f662841: Vale.Spelling = warning reverted to default error. CI gates real typos. errata-ai/vale-action doesn't apply the filter, so PR review still sees the full spell-check.

Verified on index.mdx:

  • vale index.mdx (CI view): 13 errors / 3 warnings — spelling at error tier as it should be, including the aspirational chain names your first comment flagged.
  • vale --filter='.Name != \"Vale.Spelling\"' index.mdx (hook view): 4 errors / 3 warnings — zero spelling noise. Pure signal (Microsoft.Dashes, write-good.Passive, HeadingPunctuation).

Why this beats the warning-tier compromise:

  • Hook noise drops to zero instead of "less but still there"
  • CI signal stays where it belongs (error tier)
  • Single .vale.ini, no two-config sync footgun (Vaadin's pattern requires "KEEP IN SYNC" header comments)
  • Vocab maintenance becomes opportunistic — only when an actual CI failure needs silencing, not a treadmill

On the separate index.mdx finding: the Supported Networks table at index.mdx:85-100 lists 11 chains while x402rChains deploys to 2. CI now correctly errors on the unsupported chain names. Worth a separate doc-accuracy PR.

References: GitLab Vale tiered convention, Vaadin .vale-pr.ini, Vale CLI --filter.

Sorry for the back-and-forth — should've gone here in the first round instead of severity-tuning. Appreciate the push to actually think it through.

@A1igator
Copy link
Copy Markdown
Contributor

The strict-in-CI / lenient-in-hook split is the right call, and the --filter approach is cleaner than two .vale.ini files. No notes on that part.

But this fix is orthogonal to the Vale.Terms finding from my last comment, and that one's still live. --filter='.Name != "Vale.Spelling"' only drops Vale.Spelling. Vale.Terms still fires at error severity in both CI and the hook.

Re-ran on f662841:

Hook view (filter applied):
  137 error    Microsoft.Dashes
   47 error    Vale.Terms          ← unchanged
   29 error    Microsoft.Foreign
   11 error    Microsoft.Auto
    9 error    proselint.Typography
    8 error    write-good.ThereIs
    6 error    Microsoft.Quotes
    2 error    Microsoft.Plurals

The accept.txt entries that misfire are all still in place:

  • SDKs? → produces "Use 'SDKs?' instead of 'sdk'" (regex ? leaks through as literal replacement text, 24x)
  • [Bb]igint → would suggest the JS built-in BigInt be replaced with the regex string [Bb]igint
  • paymentInfo → flags the legitimate TS type PaymentInfo (10x)
  • Hono → flags the npm package name hono in imports
  • PaymentInfoHash → flags variable name paymentInfoHash (2x)
  • async → flags Async capitalization
  • Base (newly seeded in this PR) → flags the base code identifier / chain key (8x)

These will show up as errors in CI on any PR touching those identifiers, including PRs that are correct. Three options, any combination:

  1. Restrict Vale.Terms to suggestion or NO in .vale.ini so vocab stays spelling-only.
  2. Strip regex syntax from accept.txt: split SDKs? into SDK + SDKs, replace [Bb]igint with bigint + Bigint.
  3. Drop entries whose case is ambiguous between proper noun and code identifier (Base, paymentInfo, PaymentInfoHash, async, Hono). Let them ride as ignored spelling rather than enforced canonical case.

(1) is the smallest diff and matches the "vocab is spelling-only" mental model the rest of the setup now implies.

No back-and-forth needed, the architectural pivot was worth doing. Just wanted to flag that the Vale.Terms misfires would otherwise ship with this PR.

Per A1igator's tree-wide review on f662841: accept.txt entries are
dual-used by Vale as both spelling acceptance AND Vale.Terms
canonical-case enforcement, the latter at error severity. That fires
47 false positives across docs, including:

- SDKs?      -> 'Use SDKs? instead of sdk' (regex ? leaks literally, 24x)
- paymentInfo -> flags TS type PaymentInfo (10x)
- Base       -> flags base code identifier / chain key (8x, my seed)
- [Bb]igint  -> tells users to type the regex string
- Hono       -> flags hono npm package in import paths
- PaymentInfoHash -> flags variable paymentInfoHash (2x)
- async      -> flags Async capitalization

Vocab here was added for spelling acceptance only. Vale.Terms was an
accidental side-effect, never an intended feature. Disabled.

If we later want real terminology enforcement, the right path is a
dedicated styles/x402r/Terms.yml (or similar) with hand-curated
canonical forms, not the spelling accept-list.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 18, 2026

Good catch — and you were right that I'd previously framed those Vale.Terms "catches" as a side benefit when they were actually the bug. Pushed 378ea1d taking your option 1: Vale.Terms = NO in .vale.ini. Vocab stays spelling-only, terminology enforcement disabled entirely.

Verified tree-wide: vale . now reports 0 Vale.Terms errors (down from 47). What remains on the previously-failing pages is real (em dashes, wordiness, etc.) — none of which this PR is on the hook for.

Worth naming explicitly: my Base chain-seed in 2823c5f was net-negative — added 8 of those 47 false positives (base code identifier vs Base chain name collision). Now silent.

If we ever want real terminology enforcement (e.g. canonical x402r casing, PaymentOperator PascalCase), the right path is a hand-curated styles/x402r/Terms.yml with intentional canonical forms — not the spelling accept-list. Filed as a separate concern.

Other items from your tree-wide audit that this PR doesn't address (all genuinely scope-out and worth follow-up PRs):

  • 137 Microsoft.Dashes errors (em dashes across 30+ files)
  • 29 Microsoft.Foreign errors (e.g., / i.e.,)
  • Various proselint.Typography, Microsoft.Auto, write-good.ThereIs
  • Aspirational Supported Networks table at index.mdx:85-100

Should be clean now from this PR's perspective. Re-review when you have a minute.

@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 18, 2026

Did a config audit against Vale's official docs. One real bug, one mild tradeoff worth a comment.

Bug — Microsoft.SentenceLength and Microsoft.Passive are silently dead

MinAlertLevel = warning
...
Microsoft.SentenceLength = suggestion
Microsoft.Passive = suggestion

Per Vale's MinAlertLevel docs, MinAlertLevel = warning filters everything below warning out of output entirely. So setting these to suggestion doesn't deprioritize them — it filters them out completely. Both rules never fire.

This is the same bug class as the Vale.Spelling = suggestion regression caught earlier in this thread (round 2 — "suggestion + warning-floor silently regressed spelling to zero detection"). Got fixed for Spelling but the same pattern is in place for these two Microsoft rules. Verified locally — neither rule produces output on any .mdx file currently.

Fix options:

  1. Bump both to warning — visible but ranked below errors
  2. Drop MinAlertLevel to suggestion globally — full severity sort
  3. Filter them out of hook the same way Spelling is filtered (if the intent was 'silence them in hook but keep in CI')

If silencing was actually the intent, worth a comment saying so — current state reads as 'downgraded for noise control' but the practical effect is 'completely off.'

Tradeoff worth a comment — [formats] mdx = md is officially a downgrade

Vale's MDX docs call the mdx = md trick "merely an extension-level substitution... not a means of adding support for a new file type." JSX components (<CodeGroup>, <Note>, <Steps>), JSX expressions, and ESM imports/exports won't be skipped properly — they leak into prose linting.

The inline comment already acknowledges CI uses the proper mdx2vast parser server-side. So local hook view ≠ CI view, and local view is strictly less accurate. Not a bug given the deliberate tradeoff, but if the hook ever feels noisy on component prop strings or import paths, this is where it's coming from.

Sanity checks that came back clean

  • Section ordering (core → [formats][*.mdx]) matches Vale's documented structure
  • Vale.Terms = NO with vocab-as-spelling-only is explicitly documented as a supported pattern ("You can also disable Vale.Terms and just use Vale.Spelling...")
  • Custom rules in styles/x402r/ (Enthusiasm.yml, Slop.yml) use idiomatic extends: existence syntax

Per the config audit comment on f662841: Microsoft.SentenceLength and
Microsoft.Passive were set to `suggestion` while MinAlertLevel = warning,
meaning Vale filters them out of output entirely. Same bug class as the
Vale.Spelling regression caught earlier in this thread — `suggestion` looks
like "downgrade" but is actually "off" under our floor.

Original intent (per the rule comment header) was to silence these. Now
matches that intent explicitly:

- Microsoft.Contractions = NO   (already was, explicit)
- Microsoft.SentenceLength = NO (was: silently off via suggestion)
- Microsoft.Passive = NO        (was: silently off via suggestion)

Why each is off:
- Contractions: irrelevant for technical docs.
- SentenceLength: low-ROI, no actionable threshold.
- Passive: write-good.Passive already covers it with better signal.

Also documented the [formats] mdx = md tradeoff per Vale's MDX docs
(https://vale.sh/docs/formats/mdx): this is an extension substitution, not
real MDX support. JSX components, JSX expressions, and ESM imports can leak
into prose linting. Hook view (md mode) is strictly less accurate than CI
view (mdx2vast). Acceptable tradeoff: zero-dep local install, with CI as
the merge gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 19, 2026

Fair catch — and it's the same bug class as the Vale.Spelling regression earlier in this thread, which makes the miss embarrassing on my part. I never re-audited the existing Microsoft rules through that lens after learning it the first time.

Pushed 1969bad:

1. Silently-dead Microsoft rules. Changed both to explicit = NO matching the original "disable noisy" intent. Verified tree-wide: zero output from Microsoft.SentenceLength, Microsoft.Passive, or Microsoft.Contractions. Comment in .vale.ini now explains the bug pattern explicitly so the next person doesn't repeat it.

Picked = NO over option 1 (promote to warning) because the original comment header said "Disable overly noisy" — the intent was off, the syntax was just wrong. write-good.Passive is already enabled and covers passive voice, so no detection lost. SentenceLength has no actionable threshold and was net-noise.

2. MDX format tradeoff. Added the limitation note to the [formats] mdx = md comment block per Vale's MDX docs. Now explicit that this is extension substitution, not real MDX support — JSX components, expressions, and ESM imports can leak into prose linting. Hook view ≠ CI view. Documented as the deliberate tradeoff (zero-dep local install vs accuracy) with CI as the merge gate.

Thanks for the audit. The suggestion + warning-floor trap got me twice; explicit comment in the file now should prevent a third time.

@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 19, 2026

Pulled 1969bad and ran lint to verify. Three findings, including a correction to my prior comment.

1. Microsoft.Passive dedup claim is empirically true. Forced both rules on, compared spans:

Microsoft.Passive lines:      126
write-good.Passive lines:     126
Identical spans:              126
Microsoft-only spans:           0
write-good-only spans:          0

Exact match on every span — they're functionally identical on this corpus. If both were ever enabled, every passive sentence would double-fire. Dedup is correct.

2. Correction to my prior "silently dead" framing. I attributed the dead state of Microsoft.SentenceLength / Microsoft.Passive to MinAlertLevel = warning filtering them out. That's only half-true. Checked the rule files (styles/Microsoft/SentenceLength.yml, styles/Microsoft/Passive.yml) and both ship with level: suggestion as their rule-file default. So they've been silently off since day one regardless of .vale.ini. The old = suggestion line was redundant restating of the default, not a separate severity override. End state same (off), mechanism different from what I implied. 1969bad makes intent explicit but doesn't change behavior for these two.

3. Only Microsoft.Contractions = NO actually changes runtime behavior. Counterfactual hit counts if forced = error:

Rule Hits Behavior change from = NO
Microsoft.Contractions 37 Real suppression (would fire)
Microsoft.SentenceLength 11 No change (rule default already filters it)
Microsoft.Passive 126 No change (same reason)

So this commit is tidying the config to make actual state explicit + correctly noting Microsoft.Passive is redundant with write-good.Passive. Not a silent-regression fix — but a clean cleanup with one real empirical insight (the 126/126 dedup).

LGTM.

Previous comment block on 1969bad attributed the dead state of
Microsoft.SentenceLength and Microsoft.Passive to the same MinAlertLevel
trap as the Vale.Spelling regression. That's wrong — both rules ship with
`level: suggestion` in their YAML files (verified styles/Microsoft/*.yml),
so they were already filtered by the warning floor regardless of any
.vale.ini override. The old `= suggestion` lines were redundant
restatements of defaults, not severity downgrades.

Only Microsoft.Contractions (default `level: error`) had its behavior
actually changed by `= NO` — that line suppresses ~37 hits across docs.

Updated comment block to teach the actual mechanism per-rule. Also added
a "footgun to watch for" note: the trap is real (a default-error rule
downgraded to suggestion goes silent under warning floor), but only fires
when the rule's default is error.

Pure documentation fix. End-state unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vraspar
Copy link
Copy Markdown
Contributor Author

vraspar commented May 19, 2026

Pushed b6e5411 to fix the now-misleading comment in .vale.ini. Verified the per-rule defaults locally — your correction is right:

Rule Rule-file default = NO effect
Microsoft.Contractions error Real suppression (~37 hits)
Microsoft.SentenceLength suggestion Already filtered; intent-only
Microsoft.Passive suggestion Same — and duplicates write-good.Passive 126/126

Comment now teaches the per-rule mechanism and includes a "footgun to watch for" note: the suggestion + warning-floor trap fires only when a rule's default is error (the Vale.Spelling case), not on rules that already default to suggestion (these two). End-state unchanged from 1969bad; pure docs fix.

Thanks for the audit + the LGTM. Three iterations on this is more than I'd planned but the comment in the file is now accurate enough that the next person (or me, six months from now) won't repeat the mistake.

@vraspar vraspar merged commit 705141e into main May 19, 2026
2 checks passed
@vraspar vraspar deleted the vraspar/docs-writing-skill branch May 19, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants