diff --git a/EXTERNAL-VOICE.md b/EXTERNAL-VOICE.md deleted file mode 100644 index 10a5a11..0000000 --- a/EXTERNAL-VOICE.md +++ /dev/null @@ -1,119 +0,0 @@ -# External voice: no em-dashes - -This file is the canonical rule. It is copied verbatim into every repo that owns -a public surface: `agents`, `web-client`, `customer-agents`, -`test-reporter-action`. Change it in one place and port the change to the rest. - -## Scope - -Everything BuildPulse publishes or sends outside the company: - -- Blog posts and the prompts that generate them -- Marketing pages, pricing, comparison, and about copy -- Social captions, video scripts, voiceover, and audio explainers -- Public READMEs and docs -- Review comments and notices we post on customer pull requests -- In-product copy a customer reads -- Outbound email - -Out of scope: internal code comments, internal runbooks, log lines, commit -messages, and anything only employees read. Those are not worth the diff noise. - -## The rule - -Do not use the em-dash (`—`, U+2014) or the horizontal bar (`―`, U+2015). - -Do not use an en-dash (U+2013) with a space on either side in their place. That -is the same tell wearing a hat. - -Keep the unspaced en-dash in numeric ranges: `20–40%`, a `100–800` person -company. That is correct typography and reads as human. - -A range whose endpoints contain spaces takes a spaced en-dash, and that is also -correct: `Aug 7 – Sep 7, 2026`. The test bans the spaced en-dash because that is -overwhelmingly a dash joining two clauses, so a genuine range like this one is -the rare case where the guard is too broad and the code is right. Pin the line -rather than mangling the date. - -Never substitute a hyphen with a space on either side. It is a worse em-dash and -it reads as a find-and-replace job rather than a rewrite. - -(Those two rules are written out in words on purpose. Written as inline code the -significant spaces get trimmed by a formatter, which silently inverts what the -rule says.) - -And no HTML entity dashes: `—`, `—`, `—`, or a spaced -`–`. They render as exactly the characters above, but they are plain ASCII -in source, so a search for the character does not find them. In JSX they are -worse than a rendering nit: an entity inside a multi-line text run makes the -transform drop the space after an adjacent expression, which has already shipped -`vs the 30before` to production once. - -## How to rewrite, by job - -An em-dash does at least five different jobs. Each has its own correct fix. -Pick by what the dash was actually doing, not by what is quickest. - -| Job | Use instead | Before | After | -| ------------------------------------ | ------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ | -| Abrupt pivot into a punchline | Full stop, two sentences | `You didn't fix it — you bribed it.` | `You didn't fix it. You bribed it.` | -| Introducing an explanation | Colon | `Code blocks don't count — a code-heavy post needs the same prose.` | `Code blocks don't count: a code-heavy post needs the same prose.` | -| Label before a definition, in a list | Colon | `**Bugs** — logic errors, nil deref.` | `**Bugs**: logic errors, nil deref.` | -| Paired aside | Parentheses, or commas if short | `environments — fintech, healthtech — where CI gates` | `environments (fintech, healthtech) where CI gates` | -| Clause tacked on the end | Comma, or recast | `...in AI features — or email support.` | `...in AI features, or email support.` | - -If none of those land cleanly, rewrite the sentence. The dash was often hiding a -sentence that wanted to be two. - -## Why - -Two reasons, and the second is the one that matters. - -The em-dash is the single most recognizable tell of machine-written prose, and -our audience is engineers who spot it instantly. A post that reads as generated -undercuts the argument in it, and a review comment that reads as generated gets -dismissed before the finding is read. - -More importantly, the dash is a way to avoid deciding what the relationship -between two clauses actually is. Choosing a colon, a full stop, or parentheses -forces that decision, and the prose gets sharper for it. - -## For prompts specifically - -A prompt that bans em-dashes while being written in em-dash style will lose to -its own example. Models copy the register of their instructions more reliably -than they follow a rule buried in a bullet list. - -So: strip the dashes from the prompt's own prose, then state the rule. Both, not -either. - -State the rule as a taxonomy, not a prohibition. Told only "no em-dashes", a -model swaps in a spaced hyphen and you have gained nothing. - -## Enforcement - -| Surface | Guard | -| ---------------------------- | ------------------------------------------------------------------------------------- | -| Blog posts | `agents/blog-generator/validate.go` hard reject, fed back into the model's retry loop | -| Social captions | `agents/social-poster/compose.go` re-asks once, then publishes and warns | -| Video captions and voiceover | `agents/video-poster/shotlist.go` critique pass checks for it | -| Customer PR cap notice | `customer-agents/review-trigger/capnotice_test.go` | -| Marketing pages | `web-client/__tests__/marketing/emDashGuard.test.ts` | - -Anything not in that table is on the author. The guards exempt this file, since -it necessarily quotes the character it bans, and they exempt fenced code blocks, -because a dash inside sample output belongs to the code and not to our voice. - -### Coverage - -`web-client` is swept end to end: marketing, the site-wide social metadata, the -explainer prompts, the invitation emails, and the in-product dashboard copy -(tooltips, empty states, banners, chart subtitles, Stripe invoice descriptions, -the Slack quarantine notice). The guard scans all of it, and bans entity dashes -across the whole of `src` regardless. - -Not covered by any guard: post bodies already stored in `blog_posts`. Fixing the -generator did nothing for the roughly 750 em-dashes in the 88 posts published -before the rule existed. A backfill was considered and deliberately declined, so -buildpulse.io/blog will keep showing them in older posts. That is a decision, -not an oversight. diff --git a/README.md b/README.md index 5d37bff..fd48cde 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ The committed `dist/index.js` must stay in sync with `src/`. If you change `src/ ## Writing for this repo -This repository is public and this README is the first thing a prospective user reads, so its prose follows [EXTERNAL-VOICE.md](EXTERNAL-VOICE.md). +This repository is public and this README is the first thing a prospective user reads, so its prose follows the house style below. The short version: no em-dashes. Rewrite with a full stop, a colon, parentheses, or a comma depending on what the dash was doing, and never a spaced hyphen. The same rule covers the action description in `action.yml` and any log line the action prints into a user's CI output.