mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
docs: strip AI prose, add STYLE.md and validateProse (#134)
Site copy was being called out as AI slop (specifically the word
"load-bearing"). Five-pass cleanup with a build validator to keep it
from creeping back.
Pass 1 — mechanical purge:
- Remove "load-bearing" from impeccable.md, brand.md, live.md,
iterate-live.md
- Remove "highest-leverage" from critique.md, typeset.md, designing
- Remove em dashes from all 9 slop-page rule cards
- Replace "leverage" verb in personas.md
Pass 2 — rewrite the worst offenders:
- Changelog v2.0 "Data-driven skill rewrite" entry: drop "data-driven",
"frontier models", "collapses into monoculture", "biggest unlock",
"reflex defaults"; name the actual mechanism
- README opener: drop "deeper expertise and more control"; replace with
three concrete differentiators (7 reference files, 23 commands, 27
detection rules)
- Neo Mirai case study opener: action-first, name the image model used
Pass 3 — editorials:
- Fix negation pivot in distill.md ("simplicity is not about ... It is
about ...")
Pass 4 — homepage why-panels:
- Foundation lead: name the 7 reference files specifically
- Language lead: show the discipline mapping with real command names
- Production-codebases panel: drop "Impeccable isn't a sketchpad"
negation pivot
- Ships-code panel: replace "is native to that world" with "runs there"
Pass 5 — STYLE.md and validator:
- New STYLE.md at root: editorial brief with 12 principles and the
enforced denylist (each rule with a rationale and a suggested
replacement)
- scripts/build.js: validateNoEmDashes becomes validateProse. Adds 21
phrase rules with rationales, catches the \`--\` em-dash substitute,
expands target list to site/pages, site/content, README.md,
README.npm.md
- CLAUDE.md: replace the em-dash section with a STYLE.md pointer and
document the two-content-tree footgun (content/site/ vs site/content/
must be edited in lockstep until they're unified)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+72
-20
@@ -129,22 +129,83 @@ function validateSkillFrontmatter(skills) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan user-facing copy for em dashes (— or —).
|
||||
* Em dashes in project copy are a known anti-pattern here; flag them loudly.
|
||||
* Only scans files where we author copy, not vendored or generated output.
|
||||
* Scan user-facing copy for AI-prose anti-patterns:
|
||||
* - em dashes (— or —)
|
||||
* - double-hyphen substitutes (` -- `)
|
||||
* - denylisted phrases that read as AI tells in marketing copy
|
||||
*
|
||||
* Returns the number of occurrences found.
|
||||
* The denylist is the editorial brief in 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 source/skills/impeccable/, where
|
||||
* LLM-facing reference instructions can use technical phrasings the marketing
|
||||
* copy can't.
|
||||
*
|
||||
* Returns the number of occurrences found. Build fails if > 0.
|
||||
*/
|
||||
function validateNoEmDashes(rootDir) {
|
||||
function validateProse(rootDir) {
|
||||
const targets = [
|
||||
'content/site',
|
||||
'site/components',
|
||||
'site/content',
|
||||
'site/layouts',
|
||||
'site/pages',
|
||||
'README.md',
|
||||
'README.npm.md',
|
||||
];
|
||||
const extensions = new Set(['.html', '.md', '.js', '.mjs', '.css', '.astro']);
|
||||
const emDashPatterns = [/—/g, /—/gi, /—/gi, /—/gi];
|
||||
// Phrase rules: { re, rationale }. Add to 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.' },
|
||||
{ re: /\bbiggest unlock\b/i, rationale: 'AI tell. Marketing-speak. Describe the actual change.' },
|
||||
{ re: /\breflex defaults?\b/i, rationale: 'Internal jargon leaking into user-facing copy. Say "instincts" or "first guesses".' },
|
||||
{ re: /\bcollapses? into monoculture\b/i, rationale: 'Internal eval-speak. Describe what actually went wrong.' },
|
||||
{ re: /\bdata-driven\b/i, rationale: 'Empty marketing adjective. Cite the data instead.' },
|
||||
{ re: /\bseamless(?:ly)?\b/i, rationale: 'Hollow positive. Say what specifically works without friction.' },
|
||||
{ re: /\brobust(?:ness)?\b/i, rationale: 'Hollow positive. Cite the failure mode it handles.' },
|
||||
{ re: /\bdelves?\b|\bdelved\b|\bdelving\b/i, rationale: 'Top AI tell. Use "explore", "look at", or just delete.' },
|
||||
{ re: /\belevate(?:s|d)?\b/i, rationale: 'Marketing verb. Use the specific verb (improve, raise, sharpen).' },
|
||||
{ re: /\bempower(?:s|ed|ing)?\b/i, rationale: 'Marketing verb. Use "let you" or "make possible".' },
|
||||
{ re: /\bunderscore(?:s|d)?\b/i, rationale: 'AI tell. Use "show" or "make clear".' },
|
||||
{ re: /\bpivotal\b/i, rationale: 'Hollow positive. Use "central", "key", or describe the role.' },
|
||||
{ re: /\bin today's\b/i, rationale: 'Throat-clearing opener. Cut the clause; start at the point.' },
|
||||
{ re: /\bgone are the days\b/i, rationale: 'Throat-clearing. Make the point directly.' },
|
||||
{ re: /\bwhether you're\b/i, rationale: 'Audience-pandering. Pick one reader; write to them.' },
|
||||
{ re: /\blet's dive in\b/i, rationale: 'Throat-clearing. Just start.' },
|
||||
{ re: /\bin summary\b|\bin conclusion\b/i, rationale: 'Summarizing closer. End on the strongest sentence; trust the reader.' },
|
||||
{ re: /\bmoreover\b|\bfurthermore\b/i, rationale: 'Transition crutch on a metronome. Drop, or use "also".' },
|
||||
{ re: /\btapestry\b/i, rationale: 'AI scenery noun. Cut.' },
|
||||
];
|
||||
let errors = 0;
|
||||
|
||||
const checkLine = (line, rel, lineNum) => {
|
||||
for (const re of emDashPatterns) {
|
||||
if (re.test(line)) {
|
||||
console.error(` ❌ ${rel}:${lineNum}: em dash → ${line.trim().slice(0, 120)}`);
|
||||
console.error(` Use commas, colons, semicolons, periods, or parentheses.`);
|
||||
errors++;
|
||||
re.lastIndex = 0;
|
||||
break;
|
||||
}
|
||||
re.lastIndex = 0;
|
||||
}
|
||||
if (/ -- /.test(line)) {
|
||||
console.error(` ❌ ${rel}:${lineNum}: \` -- \` em-dash substitute → ${line.trim().slice(0, 120)}`);
|
||||
console.error(` Worse than the em dash. Pick real punctuation.`);
|
||||
errors++;
|
||||
}
|
||||
for (const rule of phraseRules) {
|
||||
if (rule.re.test(line)) {
|
||||
const matched = line.match(rule.re)?.[0] ?? '';
|
||||
console.error(` ❌ ${rel}:${lineNum}: "${matched}" → ${line.trim().slice(0, 120)}`);
|
||||
console.error(` ${rule.rationale}`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scan = (absPath, rel) => {
|
||||
const stat = fs.statSync(absPath);
|
||||
if (stat.isDirectory()) {
|
||||
@@ -156,16 +217,7 @@ function validateNoEmDashes(rootDir) {
|
||||
if (!extensions.has(path.extname(absPath))) return;
|
||||
const src = fs.readFileSync(absPath, 'utf-8');
|
||||
const lines = src.split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
for (const re of emDashPatterns) {
|
||||
if (re.test(line)) {
|
||||
console.error(` ❌ ${rel}:${i + 1}: em dash in copy → ${line.trim().slice(0, 120)}`);
|
||||
errors++;
|
||||
break;
|
||||
}
|
||||
re.lastIndex = 0;
|
||||
}
|
||||
});
|
||||
lines.forEach((line, i) => checkLine(line, rel, i + 1));
|
||||
};
|
||||
|
||||
for (const target of targets) {
|
||||
@@ -174,9 +226,9 @@ function validateNoEmDashes(rootDir) {
|
||||
}
|
||||
|
||||
if (errors === 0) {
|
||||
console.log(`✓ No em dashes in project copy`);
|
||||
console.log(`✓ Prose validator: no AI tells in user-facing copy`);
|
||||
} else {
|
||||
console.error(`\n❌ ${errors} em dash(es) in project copy. Use commas, colons, or parentheses.`);
|
||||
console.error(`\n❌ ${errors} prose issue(s) in user-facing copy. See STYLE.md for the rules.`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -672,10 +724,10 @@ async function build() {
|
||||
// Verify every hand-authored HTML page carries the shared site header
|
||||
const headerErrors = validateSiteHeader(ROOT_DIR);
|
||||
|
||||
// Scan user-facing copy for em dashes
|
||||
const emDashErrors = validateNoEmDashes(ROOT_DIR);
|
||||
// Scan user-facing copy for AI tells (em dashes, marketing fluff, denylisted phrases)
|
||||
const proseErrors = validateProse(ROOT_DIR);
|
||||
|
||||
if (countErrors > 0 || headerErrors > 0 || emDashErrors > 0) {
|
||||
if (countErrors > 0 || headerErrors > 0 || proseErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user