Reorganize contributor docs

This commit is contained in:
Paul Bakaus
2026-06-09 15:18:53 -07:00
parent 5fbe37c97c
commit 324cec73a4
10 changed files with 13 additions and 265 deletions
+4 -5
View File
@@ -45,15 +45,15 @@ Edit any of these directly and the dev server hot-reloads. No rebuild needed for
- **`--color-ash`** (55%) is for secondary labels, captions, relationship meta lines.
- **Never use pure black or pure white.** Use the tinted tokens.
## Prose: read STYLE.md before writing user-facing copy
## Prose: read docs/STYLE.md before writing user-facing copy
Editorial brief is at `STYLE.md` (root). Read it before editing the homepage, sub-pages, command editorials, tutorials, or READMEs. The site has been called out for AI prose; the rules there exist to keep that from creeping back.
Editorial brief is at `docs/STYLE.md`. Read it before editing the homepage, sub-pages, command editorials, tutorials, or READMEs. The site has been called out for AI prose; the rules there exist to keep that from creeping back.
The build's `validateProse` step (in `scripts/build.js`) enforces a denylist: em dashes (`—` and HTML entities), the `--` em-dash substitute, `load-bearing`, `highest-leverage`, `biggest unlock`, `seamless`, `robust`, `delve`, `elevate`, `empower`, `underscore`, `pivotal`, `tapestry`, `data-driven`, `reflex defaults`, `collapses into monoculture`, `in today's`, `gone are the days`, `whether you're`, `let's dive in`, `in summary`, `in conclusion`, `moreover`, `furthermore`. Each rule prints a rationale and a suggested replacement when it fires. **Do not silently work around the regex.** If a banned word has earned a real meaning here, raise it as a STYLE.md amendment.
The build's `validateProse` step (in `scripts/build.js`) enforces a denylist: em dashes (`—` and HTML entities), the `--` em-dash substitute, `load-bearing`, `highest-leverage`, `biggest unlock`, `seamless`, `robust`, `delve`, `elevate`, `empower`, `underscore`, `pivotal`, `tapestry`, `data-driven`, `reflex defaults`, `collapses into monoculture`, `in today's`, `gone are the days`, `whether you're`, `let's dive in`, `in summary`, `in conclusion`, `moreover`, `furthermore`. Each rule prints a rationale and a suggested replacement when it fires. **Do not silently work around the regex.** If a banned word has earned a real meaning here, raise it as a `docs/STYLE.md` amendment.
The validator scans `site/pages/`, `site/content/`, `site/components/`, `site/layouts/`, `README.md`, `README.npm.md`. It deliberately skips `skill/` because LLM-facing reference instructions sometimes need technical phrasings the marketing copy can't.
The deeper structural issues (negation pivot, triadic auto-pilot, uniform paragraph rhythm, hollow confidence) require human judgment. STYLE.md lists them. Use them on every editorial pass.
The deeper structural issues (negation pivot, triadic auto-pilot, uniform paragraph rhythm, hollow confidence) require human judgment. `docs/STYLE.md` lists them. Use them on every editorial pass.
## Editorial content lives under `site/content/`
@@ -275,7 +275,6 @@ The build system counts commands from the router table automatically. Update the
- `site/pages/index.astro` — meta descriptions, hero box, section lead
- `/cheatsheet` redirects to `/docs` (no standalone page)
- `README.md` — intro, command count, commands table
- `NOTICE.md` — command count
- `AGENTS.md` — intro command count
- `.claude-plugin/plugin.json` — description
- `.claude-plugin/marketplace.json` — metadata description + plugin description
-44
View File
@@ -1,44 +0,0 @@
# EMPIRICAL_VALIDATION.md
The rules in `skill/SKILL.src.md` and `skill/reference/` are hypotheses. This doc names the ones with empirical evidence.
Source: a private eval harness at `~/code/impeccable-evals/` ablates each rule across three providers (Claude Haiku 4.5, Gemini 3.5 Flash, GPT-5.4 mini) on four brand niches (luxury hotel, italian restaurant, observability dashboard, vintage moto forum) at n=10 samples each. A rule "validates" when removing it shifts a deterministic detector ≥ 20pp in the predicted direction. v2.1 sweep completed 2026-06-09 anchored to commit `54c3a502` (skill-v3.5.0 + the five prose fixes that landed alongside this doc). 544 cells total.
Treat any rule **not** named here as "intended but not yet measured" — many are correctly doing their job in territory the current detectors don't cover (DOM-render-dependent signals, niches we don't test, model-resistant patterns). The audit overlay in the eval dashboard categorizes every miss so a "failed-validation" verdict isn't read as "this rule is dead."
## Cross-provider winners (validated on ≥ 2 providers)
These are the trustworthy core. The ablation moved the signal in the predicted direction by a wide margin on at least two providers.
| rule | providers | strongest delta |
|---|---|---|
| `skill-typo-text-wrap-balance` | anthropic, google, openai | Gemini +100pp, OpenAI +92pp on `text_wrap_balance` |
| `brand-typo-reflex-reject-fonts` | google, openai | OpenAI +60pp; Gemini ablate samples switched fully to Inter |
| `brand-imagery-required` | anthropic, google | Anthropic +70pp, Gemini +60pp on `image_present` |
| `brand-imagery-one-decisive-photo` | anthropic, google | Removing the rule drops `single_hero_image` substantially on both |
| `brand-permission-first-load-motion` | anthropic, google | `first_load_motion_present` moves on both |
| `skill-color-strategy-commitment` | anthropic, google | Removing the rule drops `mentions_color_strategy` on both |
| `skill-color-anti-cream` | anthropic, google | Removing it raises `cream_sand_palette` on both |
Single-provider validations exist for ~20 more rules; see the eval dashboard at `localhost:8723/dashboard/biases/rules/` (filter "v2 any").
## What we learned about the rules themselves
- **Literal bad examples in rule prose primed the model to reproduce them.** Confirmed against OpenAI samples that emitted "fake theater", "vendor theater", and "heatmap theater" as verbatim copies of an example phrase in the skill. The five prose fixes landed alongside this doc remove that pattern from `skill-ban-codex-x-theater`, `brand-imagery-required`, `skill-typo-no-all-caps-body`, `brand-color-no-converge`, and `skill-copy-no-aphoristic-cadence`. **Going forward: describe the failure shape, never enumerate examples of it.**
- **Many "failed-validation" rules are doing their job; the detector just can't see the effect.** `det_low_contrast` saturates at 80% baseline; render-dependent detectors under-fire on one-shot output. A failed verdict on a render-dependent rule is not evidence the rule is dead.
- **Detector vocabulary anchoring is a real bias.** Detectors that grep for the rule's own example words measure echo, not effect. The current `mentions_color_strategy` / `mentions_register` detectors should be treated with caution.
## What got deleted in this pass
- `skill-typo-no-all-caps-body` — duplicate of `brand-ban-all-caps-body` (same signal, brand version is more specific).
- `skill-typo-codex-hero-ceiling-repeat` — duplicate of `skill-typo-hero-ceiling` (the codex-block restatement didn't reinforce; the universal rule covers it).
- `skill-typo-scale-ratio` — duplicate of `brand-typo-modular-scale` (same signal, brand version carries the implementation detail).
- `skill-typo-font-count` — no measurable signal; models don't reach for ≥4 families in any niche we test. The rule remains a defensible safety rail but isn't earning its place in skill prose.
## Cost discipline note
The v2.0 → v2.1 cycle cost ~$1,800 across two providers' worth of API plus operational waste. Ablation is expensive; new measurement should be triggered by a specific decision the existing matrix can't yet inform — not by a desire for more data.
## Methodology
For the harness setup, the rule-by-rule audit reports, and the cost trail: `~/code/impeccable-evals/notes/ablation-v2-plan.md` and `~/code/impeccable-evals/output/ablations/_audit_consolidated.json`.
-25
View File
@@ -1,25 +0,0 @@
# Notice
Impeccable
Copyright 2025-2026 Paul Bakaus
## Anthropic frontend-design Skill
The `impeccable` skill in this project builds on Anthropic's original frontend-design skill.
**Original work:** https://github.com/anthropics/skills/tree/main/skills/frontend-design
**Original license:** Apache License 2.0
**Copyright:** 2025 Anthropic, PBC
This project extends the original with:
- 7 domain-specific reference files (typography, color-and-contrast, spatial-design, motion-design, interaction-design, responsive-design, ux-writing)
- 23 commands
- Expanded patterns and anti-patterns
## Typecraft Guide Skill
The `typography.md` reference in this project incorporates a set of tactical additions merged in from ehmo's `typecraft-guide-skill` at the author's request: dark-mode weight/tracking compensation, `font-display: optional` vs `swap`, preload-critical-weight-only guidance, variable fonts for 3+ weights, `clamp()` max-to-min ratio bound, responsive measure/container coupling, `text-wrap: balance` / `pretty`, `font-optical-sizing: auto`, ALL-CAPS tracking quantification, and the paragraph-rhythm rule (space OR indent, never both).
**Original work:** https://github.com/ehmo/typecraft-guide-skill
**Original license:** see upstream repo
**Author:** ehmo
+1 -3
View File
@@ -286,14 +286,12 @@ Join the community and ecosystem conversations:
## Contributing
See [DEVELOP.md](DEVELOP.md) for contributor guidelines and build instructions.
See [DEVELOP.md](docs/DEVELOP.md) for contributor guidelines and build instructions.
## License
Apache 2.0. See [LICENSE](LICENSE).
The impeccable skill builds on [Anthropic's original frontend-design skill](https://github.com/anthropics/skills/tree/main/skills/frontend-design). See [NOTICE.md](NOTICE.md) for attribution.
---
Created by [Paul Bakaus](https://www.paulbakaus.com)
+3 -2
View File
@@ -189,8 +189,9 @@ impeccable/
providers.js # Provider config map
index.js # Re-exports
tests/ # Bun test suite
HARNESSES.md # Provider capabilities reference
DEVELOP.md # This file
docs/
HARNESSES.md # Provider capabilities reference
DEVELOP.md # This file
README.md # User documentation
```
View File
View File
-48
View File
@@ -1,48 +0,0 @@
# Issue 150 Live Preview Plan
## Current Bug Summary
Live preview can lose framework state when variants are written directly into watched component source. The Svelte reproduction is a stateful expense row: after adding an expense, generating variants for the row should not reset the component or render raw Svelte expressions such as `{expenses[0].name}`.
The current branch uses **Svelte component injection** for `.svelte` targets: variants are real components under `src/lib/impeccable/<id>/`, mounted in the browser via Svelte 5 `mount()`, and inlined back into the route source on live exit. Accept keeps the mounted component visible immediately while deferring the route write until `live-server stop`.
## Manual test apps (home directory)
Stateful framework repros live outside this repo:
- **Svelte:** `~/impeccable-live-svelte` (see its `README.md`)
- **React:** `~/impeccable-live-react` (see its `README.md`)
Each app includes a copied `.cursor/skills/impeccable` build from the local impeccable branch for live-server / inject / poll.
## Current Status
- Svelte component-injection Accept fix is implemented.
- Svelte manual pass with the user is complete (`~/impeccable-live-svelte`).
- React manual user check and the DeepSeek-backed final run are still pending (`~/impeccable-live-react`).
- Focused live tests, build, and full test suite should pass after harness refresh.
## Svelte Fix Plan
- Keep component injection scoped to `.svelte` targets.
- Extract `propContract` from the picked route markup and author variants as real `.svelte` files with `{propName}` bindings.
- Mount compiled variants in the browser with the app's shared Svelte runtime.
- On Accept, keep the chosen mounted variant visible immediately.
- Defer the real route source inline until live shutdown to avoid accept-time remounts.
- On live shutdown, inline accepted markup + CSS into the route and remove temp component files.
- Keep the connected indicator stable while an event is leased or actively being handled.
## React Parity Test Plan
- Same shape as the Svelte case in `~/impeccable-live-react`.
- Run the same Go, cycle, Accept flow against the React row.
- React already uses direct source wrap + Fast Refresh; keep that path unchanged unless manual testing shows state loss.
## Validation Checklist (impeccable repo)
- [ ] `node --test tests/live-browser-regression.test.mjs tests/live-accept.test.mjs tests/live-poll.test.mjs tests/live-server.test.mjs tests/live-svelte-component.test.mjs`
- [ ] Manual Svelte run in `~/impeccable-live-svelte`
- [ ] `bun run build`
- [ ] `bun run test`
The PR should remain draft until the React manual check in `~/impeccable-live-react` passes.
-132
View File
@@ -1,132 +0,0 @@
# Handoff: Svelte live mode bar stuck at `0/0` (CYCLING with no variants)
Date: 2026-05-30
Branch: `codex/issue-150-svelte-live`
Status: **Unresolved.** Two rounds of fixes landed and were synced to the manual test repo, but the symptom still reproduces.
---
## Symptom
In `~/impeccable-live-svelte`, after running a live action (Polish/Bolder) on the expense row, the floating live bar shows the **CYCLING** layout (prev/next arrows, two faint dots, `✓ Accept`, `✕`) but the counter reads `0/0` and every control is disabled (`opacity: 0.3`, `pointer-events: none`). No variant is mounted. The bar persists across reloads.
Captured DOM (trimmed): `<div id="impeccable-live-bar">``<span>0/0</span>``✓ Accept` (disabled) `✕`. Full markup is in the chat history if needed.
`0/0` = `visibleVariant=0 / arrivedVariants=0`. The two dots = `expectedVariants=2` rendered as "pending" (see `buildDots`, both unfilled because `arrivedVariants=0`).
---
## Context: what this feature is
Issue 150 replaced the Svelte "source-shadow" live preview with **real component injection**. See `docs/issue-150-live-preview-plan.md` and `skill/reference/live.md` (the `svelte-component` paragraph). Key pieces:
- `skill/scripts/live-svelte-component.mjs` — scaffolds `src/lib/impeccable/<id>/` with `manifest.json`, `v1.svelte``vN.svelte`, and a one-time `__runtime.js`. Inlines the accepted variant back into the route on exit.
- `skill/scripts/live-wrap.mjs``.svelte` targets return `previewMode: "svelte-component"`, `file` = manifest path.
- `skill/scripts/live-browser.js` — mounts compiled variants via Svelte 5 `mount()` into a `display:contents` slot that replaces the original element. Cycling = unmount + remount.
- `skill/scripts/live-accept.mjs` / `live-server.mjs` — defer the route source write to `live-server stop`.
The Svelte wrapper in the live DOM is **runtime-injected** and holds a single mount target (`[data-impeccable-component-mount]`), **not** `[data-impeccable-variant]` children like the HTML/JSX path. This distinction is the source of most of the trouble.
---
## What has already been fixed (and synced to the test repo)
All in `skill/scripts/live-browser.js`, rebuilt via `bun run build:skills` and rsync'd to `~/impeccable-live-svelte/.cursor/skills/impeccable/`.
1. **Params sidecar (the original compile error).** Svelte parses `{` inside an attribute value as an expression, so `data-impeccable-params='[{…}]'` broke compilation (`Expected token }`). Params now load from `componentDir/params.json` keyed by variant number:
- `loadSvelteComponentParams(manifest)` fetches `params.json`.
- `parseVariantParams()` reads from `svelteComponentSession.paramsByVariant` for the component path instead of the DOM attribute.
- Agent contract updated in `live-svelte-component.mjs` (`buildSvelteComponentCssAuthoring`) and `skill/reference/live.md`.
2. **Resume guard.** `resumeSession()` now drops an orphaned `svelte-component` wrapper (no live in-memory mount) instead of resuming it into an empty bar. Without this, every reload resumed `arrivedVariants=0`.
3. **Abort-on-failure.** New `abortSvelteComponentInjection(sessionId, message)` resets the bar to PICKING (restores the original element, clears session, toast) when the picked element can't be found OR the initial `mountSvelteComponentVariant` returns false (compile/mount throw). Called from `injectSvelteComponentsFromManifest`.
Tests added in `tests/live-browser-regression.test.mjs` (all green): orphan reset, abort helper, sidecar params. Full focused suite passes: `node --test tests/live-browser-regression.test.mjs tests/live-svelte-component.test.mjs tests/live-accept.test.mjs`.
---
## Why it probably STILL reproduces (hypotheses, in priority order)
### H1. The served `live.js` is stale (verify FIRST)
The browser loads `live.js` from the running live-server, not from disk directly. If the server wasn't restarted, or the `<script src=".../live.js">` is cached, the page is still running the OLD code without the abort/resume guards.
- **Check:** in the page console, search the loaded script for `abortSvelteComponentInjection` / `loadSvelteComponentParams`. If absent, the fix isn't loaded.
- **Fix:** `live-server.mjs --stop` then `--background`, hard-reload (Cmd+Shift+R), reload Cursor.
- Confirm the test repo copy is current: `grep -l abortSvelteComponentInjection ~/impeccable-live-svelte/.cursor/skills/impeccable/scripts/live-browser.js`.
### H2. The variant still fails to compile, and a path other than `injectSvelteComponentsFromManifest` sets CYCLING
The abort only fires inside `injectSvelteComponentsFromManifest`. If the agent-authored `v1.svelte` still has a compile error (another `{` in an attribute, a bad expression, etc.), the mount fails. Confirm the abort path is actually reached:
- Look for the console line `[impeccable] Failed to mount Svelte variant N` and the toast.
- If CYCLING is being set somewhere else, audit every `updateBarContent('cycling')` / `state = 'CYCLING'` site (grep finds ~12) for one that runs with `arrivedVariants===0`.
### H3. Server-side event replay re-injects after reset
`live-server.mjs` redelivers unacknowledged events and persists a session journal. After the browser aborts to PICKING, a redelivered `done` event (SSE reconnect, or the agent reply not acked) could re-trigger injection. The `done` handler (`live-browser.js` ~line 4868) only injects when `state === 'GENERATING'`, so a clean PICKING state should be safe — but verify the state at the moment of replay. The durable session may also need clearing: check `.impeccable/live/sessions/` and `.impeccable/live/deferred-svelte-component-accepts.json` in the test repo.
### H4. Stale localStorage session keeps re-priming the bar
The bar persists `state`/`id`/`visible` to localStorage (keys prefixed `impeccable-live`). A reload can rehydrate CYCLING before any wrapper exists.
- **Check:** `Object.keys(localStorage).filter(k=>k.startsWith('impeccable-live'))` in the console.
- **Clear:** remove those keys and reload (see "Reset procedure" below).
### H5. Defect-class root cause: empty CYCLING is representable at all
The deepest fix is to make `0/0 CYCLING` an impossible state. A cheap, robust guard: in the cycling render path (`buildCyclingRow` / wherever `updateBarContent('cycling')` resolves), if `arrivedVariants === 0`, refuse to render CYCLING and fall back to PICKING (or hide). That self-heals regardless of which upstream path is buggy. Consider adding this as belt-and-suspenders even after the specific path is found.
---
## Reset procedure (clears the currently-stuck bar)
```js
// page console
Object.keys(localStorage).filter(k => k.startsWith('impeccable-live')).forEach(k => localStorage.removeItem(k));
location.reload();
```
```bash
cd ~/impeccable-live-svelte
node .cursor/skills/impeccable/scripts/live-server.mjs --stop
node .cursor/skills/impeccable/scripts/live-server.mjs --background
# also clear any orphaned session state if H3 suspected:
rm -rf .impeccable/live/sessions/* .impeccable/live/deferred-svelte-component-accepts.json
rm -rf src/lib/impeccable/*/ # leftover variant component dirs (keep __runtime.js)
```
Reload Cursor so `/impeccable` picks up the synced skill.
---
## Repro
1. `cd ~/impeccable-live-svelte && npm run dev`
2. New terminal: `node .cursor/skills/impeccable/scripts/live-server.mjs --background`, note the port.
3. `node .cursor/skills/impeccable/scripts/live-inject.mjs --port <port>` then `node .cursor/skills/impeccable/scripts/live-poll.mjs`.
4. In the browser: add an expense, pick the expense row, run Polish/Bolder, let variants generate.
5. Observe whether the bar reaches `1/3` (good) or `0/0` (the bug).
6. With `IMPECCABLE_E2E_DEBUG`-style logging: open devtools console and watch for `[impeccable]` lines during step 4.
The expense row source is `src/routes/+page.svelte` lines ~34-37 (`<article class="expense-row …">{expenses[0].name}…`). `propContract` derives `name` and `amount`.
---
## Suggested investigation order for tomorrow
1. **Confirm the new code is actually live** (H1) — single biggest time-saver. Grep the loaded script + the test-repo file for `abortSvelteComponentInjection`.
2. With confirmed-fresh code, reproduce and **capture the console**. Determine whether `injectSvelteComponentsFromManifest` runs and whether `mountSvelteComponentVariant` throws. Read the actual `src/lib/impeccable/<id>/v1.svelte` the agent wrote — is it valid Svelte?
3. If mount throws: the agent is still authoring an invalid component. Tighten the `live.md` contract / scaffold stub, OR make the inline-accept/scaffold validate. Capture the exact error.
4. If mount succeeds but bar still `0/0`: trace which `state='CYCLING'` site runs with `arrivedVariants=0` (H2) and add the H5 guard.
5. Add an E2E regression once root cause is known. Note `tests/live-e2e.test.mjs` still references the old `source-shadow` markers for the Svelte fixture (lines ~311, ~891, ~918) — that opt-in suite needs updating to the component-injection model regardless.
---
## Key files / line anchors (as of this handoff)
| File | What |
|---|---|
| `skill/scripts/live-browser.js` ~4242 | `injectSvelteComponentsFromManifest` |
| `skill/scripts/live-browser.js` (`abortSvelteComponentInjection`) | clean-reset helper |
| `skill/scripts/live-browser.js` (`mountSvelteComponentVariant`) | dynamic import + `mount()` |
| `skill/scripts/live-browser.js` (`loadSvelteComponentParams`, `parseVariantParams`) | sidecar params |
| `skill/scripts/live-browser.js` (`resumeSession`) | orphan-wrapper guard |
| `skill/scripts/live-browser.js` ~4868 | SSE `done` handler |
| `skill/scripts/live-browser.js` (`buildCyclingRow`, `buildDots`, ~2102/2262) | bar render — candidate for H5 guard |
| `skill/scripts/live-svelte-component.mjs` | scaffold / inline-accept / cssAuthoring contract |
| `skill/reference/live.md` (`svelte-component` paragraph + Parameters §7) | agent contract |
Remember: source of truth is `skill/`. After any edit run `bun run build:skills`, then rsync to `~/impeccable-live-svelte/.cursor/skills/impeccable/`. Do not hand-edit the harness copies.
+5 -6
View File
@@ -66,7 +66,6 @@ function generateCounts(rootDir, skills, buildDir) {
const filesToCheck = [
'site/pages/index.astro',
'README.md',
'NOTICE.md',
'AGENTS.md',
'.claude-plugin/plugin.json',
'.claude-plugin/marketplace.json',
@@ -131,7 +130,7 @@ function validateSkillFrontmatter(skills) {
* - double-hyphen substitutes (` -- `)
* - denylisted phrases that read as AI tells in marketing copy
*
* The denylist is the editorial brief in STYLE.md, enforced. Each rule has a
* The denylist is the editorial brief in docs/STYLE.md, enforced. Each rule has a
* rationale that prints with the failure so the next author understands why.
*
* Scope: every surface a reader sees. Not skill/, where
@@ -155,7 +154,7 @@ function validateProse(rootDir) {
// the prose gate: its job is to show the slop, not to avoid it.
const excludedPrefixes = ['site/pages/slop'];
const emDashPatterns = [/—/g, /&mdash;/gi, /&#8212;/gi, /&#x2014;/gi];
// Phrase rules: { re, rationale }. Add to STYLE.md when adding here.
// Phrase rules: { re, rationale }. Add to docs/STYLE.md when adding here.
const phraseRules = [
{ re: /\bload-bearing\b/i, rationale: 'AI tell. Stolen-engineer diction; almost always vague. Name what the thing actually does.' },
{ re: /\bhighest-leverage\b/i, rationale: 'AI tell. Vague claim of impact. Say what specifically pays off.' },
@@ -229,7 +228,7 @@ function validateProse(rootDir) {
if (errors === 0) {
console.log(`✓ Prose validator: no AI tells in user-facing copy`);
} else {
console.error(`\n${errors} prose issue(s) in user-facing copy. See STYLE.md for the rules.`);
console.error(`\n${errors} prose issue(s) in user-facing copy. See docs/STYLE.md for the rules.`);
}
return errors;
}
@@ -239,7 +238,7 @@ function validateProse(rootDir) {
*
* The full validateProse rules don't fit LLM-facing reference instructions:
* the hardening repetition and triadic checklists those files use exist on
* purpose, and the structural-prose rules in STYLE.md require human judgment.
* purpose, and the structural-prose rules in docs/STYLE.md require human judgment.
* This validator only enforces the mechanical wins: em dashes (which are
* pure punctuation laziness regardless of audience) and the small handful
* of denylisted phrases that have no technical reading. Em-dash creep is the
@@ -317,7 +316,7 @@ function validateSkillProse(rootDir) {
if (errors === 0) {
console.log(`✓ Skill prose validator: skill/ is clean`);
} else {
console.error(`\n${errors} prose issue(s) in skill/. See STYLE.md.`);
console.error(`\n${errors} prose issue(s) in skill/. See docs/STYLE.md.`);
}
return errors;
}