<!-- drafting-content -->
<!-- One Claude skill, flattened into a single markdown file. -->
<!-- To install: create ~/.claude/skills/drafting-content/ and split the FILE blocks below back out. -->
<!-- SKILL.md is everything above the first FILE heading. -->

---
name: drafting-content
description: Writes a long-form draft from a finished content brief, then blocks it from shipping until a mechanical gate passes. Drafts section by section against the brief's word budgets and format tags, then runs scripts/quality_gate.py, which exits nonzero on banned vocabulary, reframe constructions, anaphora, banned words in headings, over-long paragraphs, cadence metronomes, and unsourced figures. Use when a brief exists and a draft, article, or first version is requested. Do not use without a brief (use briefing-content) or for the final polish pass (use editing-content).
---

# Drafting Content

## Workflow

Copy this checklist and track progress:

```
Draft progress:
- [ ] Step 1: Read the brief twice
- [ ] Step 2: Draft the body, section by section
- [ ] Step 3: Write the introduction last
- [ ] Step 4: Run the gate
- [ ] Step 5: Fix and re-run until it exits 0
```

### Step 1: Read the brief twice

First pass for what the article covers. Second pass for the word budgets and format tags.

The format tags carry more weight than they look. Two consecutive sections tagged `paragraph` read as a wall by the second one, however good the sentences are.

### Step 2: Draft the body, section by section

One section at a time, in the brief's order.

Before each section, re-read the one before it and answer one question: where is the reader's attention now, and where does it need to be for the next section to land. That answer is the transition. Write it into the prose instead of announcing it.

Follow `reference/prose-rules.md` while drafting. Reading it after the fact means rewriting rather than writing.

### Step 3: Write the introduction last

An introduction written before the body promises what the body does not deliver. Written afterward, it promises what arrived.

### Step 4: Run the gate

```bash
python3 scripts/quality_gate.py draft.md
```

Standard library only, no packages to install. Add `--verbose` to see every hit instead of the first eight.

The gate scans headings and list items for vocabulary, and prose only for cadence. A bullet is meant to be terse; counting one as a performed punch sentence flags good writing as bad.

Fenced code blocks and blockquotes are excluded from the vocabulary scan, so a banned word can be quoted as data. Do not move prose into a fence to clear a check.

### Step 5: Fix and re-run until it exits 0

Fix every `FAIL`. Read every `WARN` and decide.

Rewrite the sentence, never swap the word. A line that only worked with "seamless" in it was doing nothing.

## Verification loop

The gate covers what a script can check. These three need a person, and the draft is not finished without them:

1. **Value per sentence.** Every line carries information or rhythm. Cut the ones carrying neither, even when true.
2. **Side of the desk.** Read each paragraph and ask whether it is written from the reader's position or the writer's. A paragraph describing the article's own contents belongs to the writer.
3. **Read aloud.** Every sentence that would not be said to a colleague gets rewritten.

## Test it

1. A draft opening with a scene ("Imagine a marketer..."). The gate should refuse it on banned openers.
2. A draft carrying a banned word inside a fenced code block. The gate should pass it.
3. A draft where every sentence runs 12 to 16 words. The gate should flag a flat metronome.

## What still needs a person

Roughly seven tenths of the information in the output survives editing, and roughly one tenth of the sentences survive word for word. The what comes out mostly right and the how mostly wrong. Budget accordingly: less time than expected for drafting, considerably more for the pass after it.


---

## FILE: `drafting-content/reference/prose-rules.md`

Save this block at `~/.claude/skills/drafting-content/reference/prose-rules.md`

```markdown
# Prose Rules

## Contents
- Banned vocabulary
- Reframe constructions
- Anaphora and negation runs
- Openers
- Cadence and paragraph shape
- Figures and links
- What the gate cannot check

## Banned vocabulary

The gate fails the build on these, in prose, headings, and list items.

```
actually, shift, significantly, fundamentally, leverage, robust,
seamless, streamline, optimize (verb), delve, realm, harness,
unlock, elevate, empower, holistic, comprehensive, innovative,
game-changer, cutting-edge, transformative, revolutionize,
testament, pivotal, crucial, meticulously, showcase, foster,
underscore, tapestry, move the needle, real, really, fix,
ship, shipped, rather than, compound
```

Also banned: `serves as`, `stands as`, `marks a`, `boasts a`, `represents a`, `features a` where `is` or `has` would do. Say `is`.

`optimization` inside a fixed term such as "answer engine optimization" passes. The verb does not. `fixed` as a pre-modifier ("fixed order") passes; as a verb it does not.

Headings get scanned like everything else. A heading is the most read line on the page.

## Reframe constructions

Any sentence that negates a framing, then asserts the corrected one. The most reliable single tell in machine-written prose.

```
This isn't X. This is Y.
Not X. Y.
X, not Y.
It's not just about X, it's about Y.
Less X, more Y.
The question isn't X, it's Y.
You don't need X. You need Y.
X is dead. Y is the future.
```

Three disguises that clear most greps:

```
Sure, X works. But Y is where...
X gets all the attention, but Y...
which isn't X, it's Y
```

The repair never varies: delete everything before the positive claim and state the claim.

## Anaphora and negation runs

Three or more sentences opening on the same word. Also the negation list: "no outline, no brief, no draft." Both are rhythm a vocabulary scan walks straight past.

Rewrite to a single positive statement, or to a real list with distinct openers.

## Openers

The gate refuses a draft starting with any of these:

```
Imagine / Picture this / Picture the scene
It's 2am / It's early
In today's / In this article / Welcome to
Let's dive in / Let's explore / Let's unpack
```

Open on the reader's situation, never on a scene invented for them.

## Cadence and paragraph shape

Target spread inside every section:

| Sentence length | Share |
|---|---|
| 1 to 9 words | a good number, never three in a row |
| 10 to 18 words | the bulk |
| 19 to 28 words | a solid chunk |
| 29+ words | a few, each earning its length |

All-short reads as social filler. All-long reads as dense filler. Both are metronomes.

Paragraphs run one to three lines, ceiling of three, and the lengths rotate. Never two consecutive paragraphs of the same shape. Single-sentence paragraphs for impact: three in a whole piece, maximum.

No lists of three parallel items used to sound complete. Two, four, or the one that matters. Genuine factual sets are fine.

## Figures and links

Every figure carries a linked source. A proof point about your own company appears once, twice at the outside; a hard number lands the first time and reads as padding by the third.

Anchor text runs two to four words. Never one, never five.

## What the gate cannot check

Whether a sentence carries content value or stylistic value. Whether a paragraph is written from the reader's side of the desk. Whether a transition follows the reader's attention or the outline's logic.

Read the draft aloud at the end. The gate cannot hear.
```


---

## FILE: `drafting-content/scripts/quality_gate.py`

Save this block at `~/.claude/skills/drafting-content/scripts/quality_gate.py`

```python
#!/usr/bin/env python3
"""
quality_gate.py — pre-ship gate for a long-form draft.

Runs every deterministic check in one pass, prints PASS / WARN / FAIL per
check, and exits 1 if any hard check fails. A draft carrying a banned pattern
cannot reach the publish step even when you are in a hurry and want it to.

Usage:
    python3 quality_gate.py draft.md
    python3 quality_gate.py draft.md --verbose    # every hit, not the first 8

Scope rules, each of which exists because of a specific miss:

  * Headings ARE scanned for vocabulary. An earlier version skipped every line
    starting with "#", and a banned word sat in an H2 through four clean runs.
  * List items ARE scanned for vocabulary and are EXCLUDED from cadence. A
    bullet is meant to be terse; counting one as a performed punch sentence
    flags good writing as bad.
  * Fenced code blocks and blockquotes are excluded from vocabulary and
    construction scans, so a banned word can be quoted as data. That exemption
    is what makes the rest of the scan trustworthy. Do not move prose into a
    fence to clear a check.
  * Cadence checks need 25+ sentences. On a handful, "100% in one bucket" is
    arithmetic, not a metronome, and firing there teaches you to ignore it.

Standard library only.
"""

import re
import sys

# ---------------------------------------------------------------- vocabulary

BANNED_WORDS = [
    r"\bactually\b", r"\bshifts?\b", r"\bshifted\b", r"\bshifting\b",
    r"\bsignificantly\b", r"\bfundamentally\b", r"\bfundamental\b",
    r"\bleverag(?:e|es|ed|ing)\b", r"\brobust\b", r"\bseamless(?:ly)?\b",
    r"\bstreamlin(?:e|es|ed|ing)\b", r"\bdelve\b", r"\brealm\b",
    r"\bharness(?:es|ed|ing)?\b", r"\bunlock(?:s|ed|ing)?\b",
    r"\belevat(?:e|es|ed|ing)\b", r"\bempower(?:s|ed|ing)?\b",
    r"\bholistic\b", r"\bcomprehensive\b", r"\binnovative\b",
    r"\bgame[- ]changer\b", r"\bcutting[- ]edge\b", r"\btransformative\b",
    r"\brevolutioni[sz]e(?:s|d)?\b", r"\btestament\b", r"\bpivotal\b",
    r"\bcrucial\b", r"\bmeticulous(?:ly)?\b", r"\bshowcas(?:e|es|ed|ing)\b",
    r"\bfoster(?:s|ed|ing)?\b", r"\bunderscor(?:e|es|ed|ing)\b",
    r"\btapestry\b", r"\bmove the needle\b", r"\breally\b", r"\breal\b",
    r"\bfix(?:es|ing)?\b", r"\bfix\b",
    r"\bships?\b", r"\bshipped\b", r"\bshipping\b",
    r"\brather than\b", r"\bcompound(?:s|ed|ing)?\b",
]

# "optimize" the verb fails; "optimization" inside a fixed term passes.
BANNED_VERB_OPTIMIZE = r"\boptimi[sz]e(?:s|d)?\b"

# "fixed" is banned as a verb ("we fixed it") and allowed as a pre-modifier
# ("fixed order"), which is ordinary English and not a tell. Without the split
# the gate cries wolf on every legitimate use, and a gate you learn to ignore
# is worse than no gate.
BANNED_FIXED_VERB = (
    r"\bfixed\b(?!\s+(?:order|orders|section|sections|list|set|sequence|"
    r"width|height|size|cost|costs|price|prices|fee|fees|term|terms|"
    r"point|position|template|format|number|rate|window|schedule))"
)

COPULA_DODGES = [
    r"\bserves as\b", r"\bstands as\b", r"\bmarks a\b", r"\bboasts a\b",
    r"\brepresents a\b", r"\bfeatures a\b", r"\bholds the distinction\b",
]

# ------------------------------------------------------------- constructions

REFRAMES = [
    r"\b(?:this|that|it)\s+(?:isn't|is not)\s+[^.!?]{2,60}?[.,]\s*(?:this|that|it)?\s*(?:is|it's)\b",
    r"\bit'?s not (?:just )?about\b[^.!?]{2,60}?,?\s*it'?s about\b",
    r"\bnot only\b[^.!?]{2,60}?\bbut also\b",
    r"\bless\b\s+\w+,\s*\bmore\b\s+\w+",
    r"\bforget\b[^.!?]{2,40}?[.,]\s*(?:this is|here'?s)\b",
    r"\bthe question isn'?t\b[^.!?]{2,60}?,?\s*(?:it'?s|the question is)\b",
    r"\byou don'?t need\b[^.!?]{2,60}?[.,]\s*you need\b",
    r"\bis dead\b[^.!?]{0,40}?\bis the future\b",
    r"\bsure,\s+\w+[^.!?]{2,60}?\bbut\b[^.!?]{2,60}?\b(?:real|actual|where)\b",
    r"\bgets? all the attention,?\s*but\b",
    r"\bwhile\b[^.!?]{2,60}?\bmight seem\b[^.!?]{2,60}?,\s*\w+\s+is\b",
    r"\bstop thinking\b[^.!?]{2,40}?[.,]\s*start thinking\b",
    r"^\s*not\s+[^.!?]{2,40}\.\s*[A-Z]",
    # Mid-sentence "X, not Y" is the commonest form; an earlier version of this
    # list only caught it line-initial.
    r",\s*not\s+(?:a|an|the|on|in|at|to|from|for|by|with|because|when|where)\b[^.!?]{2,50}",
    r",\s*not\s+\w+ing\b[^.!?]{0,40}",
    # The relative-clause disguise, which slips past anything anchored on
    # this/that/it.
    r"\b(?:which|that|and it)\s+(?:isn'?t|is not|wasn'?t|was not)\b[^.!?]{2,60}?,\s*(?:it'?s|it is|they'?re)\b",
    # "Not X. Y." landing mid-paragraph rather than line-initial.
    r"(?<=[.!?])\s+Not\s+(?:the|a|an|his|her|their|its|my|your)\b[^.!?]{2,50}\.",
    # Concession-pivot: "sounds like X, and it's Y."
    r"\b(?:sounds|seems|looks|reads)\s+like\b[^.!?]{2,50}?\band it'?s\b",
    r"\bthe \w+ (?:isn'?t|is not)\b[^.!?]{2,50}?,\s*it'?s\b",
    # Broad catch-all, added after a third variant survived two rounds of
    # patching ("is usually not the competitor, it's absence"). Any negation
    # followed by a corrective "it's" inside one sentence is the skeleton,
    # whatever sits between them. Over-flags occasionally, which is the right
    # trade for the one pattern that matters most.
    r"\b(?:not|isn'?t|wasn'?t|aren'?t|never)\b[^.!?]{2,70}?,\s*it(?:'?s| is)\b",
    r"\b(?:not|isn'?t|wasn'?t|aren'?t|never)\b[^.!?]{2,70}?\.\s*It(?:'?s| is)\b",
]

BANNED_OPENERS = [
    r"^imagine\b", r"^picture this\b", r"^picture the scene\b",
    r"^it'?s 2\s?am\b", r"^it'?s early\b", r"^in today'?s\b",
    r"^in this (?:article|post|guide|piece)\b", r"^welcome to\b",
    r"^let'?s (?:dive in|explore|unpack)\b",
]

DEAD_PHRASES = [
    r"\bit'?s (?:important|worth) (?:to note|noting)\b",
    r"\bat the end of the day\b", r"\bmoving forward\b",
    r"\bin order to\b", r"\bfurthermore\b", r"\bmoreover\b",
    r"\badditionally\b", r"\bthat being said\b",
    r"\bto put this in perspective\b", r"\bin other words\b",
    r"\blet that sink in\b", r"\bread that again\b",
    r"\bwhat nobody tells you\b", r"\bmost people don'?t realize\b",
    r"\bhere'?s the thing\b",
]

# Writer-side scaffolding: sentences whose subject is the article rather than
# the work. These pass every vocabulary check and still read as a report.
SCAFFOLDING = [
    r"\bthe (?:top|first|last|second) (?:row|column|item) is\b",
    r"\bis the one worth\b", r"\bworth sitting with\b",
    r"\bhere is what (?:that|this) looks like\b",
    r"\bwhat follows is\b", r"\bthe rule that does the work\b",
    r"\bearn(?:s)? (?:its|their) place\b",
    r"\bworth something to you\b", r"\btraced end to end\b",
    r"\bknowing them makes\b", r"\bwhich is the point\b",
    r"\bin the section (?:above|below)\b",
    r"\bas (?:noted|mentioned) (?:above|earlier)\b",
]

EM_DASH = r"[—–]"


# ------------------------------------------------------------------ plumbing

def strip_exempt(text):
    """Blank fenced code, inline code, and blockquotes, preserving line count."""
    out, in_fence = [], False
    for line in text.split("\n"):
        if line.lstrip().startswith("```"):
            in_fence = not in_fence
            out.append("")
            continue
        if in_fence or line.lstrip().startswith(">"):
            out.append("")
            continue
        out.append(re.sub(r"`[^`]*`", "", line))
    return "\n".join(out)


def scan_lines(text):
    """Every line worth scanning for vocabulary: prose, headings, list items."""
    keep = []
    for i, line in enumerate(text.split("\n"), 1):
        s = line.strip()
        if not s or s.startswith(("|", "!", "---")):
            continue
        if s.startswith("#"):
            keep.append((i, s.lstrip("#").strip()))
            continue
        if re.match(r"^([-*+]|\d+\.)\s", s):
            keep.append((i, re.sub(r"^([-*+]|\d+\.)\s+", "", s)))
            continue
        keep.append((i, s))
    return keep


def headings(text):
    return [(i, l.strip().lstrip("#").strip())
            for i, l in enumerate(text.split("\n"), 1)
            if l.strip().startswith("#")]


def prose_only(text):
    """Prose lines, for cadence. No headings, no list items, no tables."""
    keep = []
    for line in text.split("\n"):
        s = line.strip()
        if not s or s.startswith(("#", "|", "!", "---")):
            continue
        if re.match(r"^([-*+]|\d+\.)\s", s):
            continue
        keep.append(s)
    return keep


def sentences(text):
    body = " ".join(prose_only(text))
    parts = re.split(r"(?<=[.!?])\s+(?=[A-Z\"'(])", body)
    return [p.strip() for p in parts if len(p.strip()) > 1]


def paragraphs(text):
    blocks, cur = [], []
    for line in text.split("\n"):
        s = line.strip()
        if not s:
            if cur:
                blocks.append(cur)
                cur = []
            continue
        if s.startswith(("#", "|", "!", "```", ">", "---")) or re.match(r"^([-*+]|\d+\.)\s", s):
            if cur:
                blocks.append(cur)
                cur = []
            continue
        cur.append(s)
    if cur:
        blocks.append(cur)
    return blocks


def scan(text, patterns):
    hits = []
    for num, line in scan_lines(text):
        for pat in patterns:
            for m in re.finditer(pat, line, re.I):
                hits.append((num, m.group(0).strip(), line[:88]))
    return hits


def anaphora_hits(text):
    """Repeated sentence openers, and negation-run lists. Both are AI rhythm
    that every vocabulary scan walks straight past."""
    hits = []
    for num, line in scan_lines(text):
        for m in re.finditer(r"\b(?:no|not|never)\b[^.!?]{0,120}", line, re.I):
            if len(re.findall(r"\bno\s+\w+", m.group(0), re.I)) >= 3:
                hits.append((num, "no X, no Y, no Z run", line[:88]))
                break
        opens = re.findall(r"(?:^|(?<=[.!?])\s)([A-Z][a-z']+)\s", line)
        for w in set(opens):
            if opens.count(w) >= 3:
                hits.append((num, f"'{w}' opens {opens.count(w)} sentences", line[:88]))
    sents = sentences(text)
    firsts = []
    for s in sents:
        m = re.match(r"^([A-Za-z']+)", s)
        firsts.append(m.group(1).lower() if m else "")
    run = 1
    for i in range(1, len(firsts)):
        if firsts[i] and firsts[i] == firsts[i - 1]:
            run += 1
            if run >= 3:
                hits.append((0, f"'{firsts[i]}' opens {run} consecutive sentences", sents[i][:88]))
        else:
            run = 1
    return hits


# -------------------------------------------------------------------- report

class Report:
    def __init__(self, verbose=False):
        self.fails = self.warns = 0
        self.verbose = verbose

    def line(self, status, name, detail=""):
        if status == "FAIL":
            self.fails += 1
        if status == "WARN":
            self.warns += 1
        print(f"  {status:<4}  {name}{('  ' + detail) if detail else ''}")

    def hits(self, status, name, hits, cap=8):
        if not hits:
            self.line("PASS", name)
            return
        self.line(status, name, f"{len(hits)} found")
        shown = hits if self.verbose else hits[:cap]
        for num, hit, ctx in shown:
            loc = f"L{num}" if num else "  "
            print(f'          {loc}: "{hit}"  |  {ctx}')
        if not self.verbose and len(hits) > cap:
            print(f"          ... {len(hits) - cap} more (--verbose for all)")


def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    verbose = "--verbose" in sys.argv
    if not args:
        print("usage: quality_gate.py draft.md [--verbose]")
        sys.exit(2)

    path = args[0]
    try:
        raw = open(path, encoding="utf-8").read()
    except OSError as e:
        print(f"cannot read {path}: {e}")
        sys.exit(2)

    text = strip_exempt(raw)
    r = Report(verbose)
    sents = sentences(text)
    words = len(" ".join(prose_only(text)).split())

    print(f"\nquality_gate: {path}")
    print(f"{words} prose words, {len(sents)} prose sentences\n")

    print("HARD CHECKS")
    r.hits("FAIL", "banned vocabulary", scan(text, BANNED_WORDS))
    r.hits("FAIL", "optimize (verb)", scan(text, [BANNED_VERB_OPTIMIZE]))
    r.hits("FAIL", "fixed (as verb)", scan(text, [BANNED_FIXED_VERB]))
    r.hits("FAIL", "reframe constructions", scan(text, REFRAMES))
    r.hits("FAIL", "anaphora and negation runs", anaphora_hits(text))
    r.hits("FAIL", "em and en dashes", scan(text, [EM_DASH]))
    r.hits("FAIL", "dead phrases", scan(text, DEAD_PHRASES))
    r.hits("FAIL", "writer-side scaffolding", scan(text, SCAFFOLDING))

    head_hits = []
    for num, h in headings(text):
        for pat in BANNED_WORDS + [BANNED_VERB_OPTIMIZE, BANNED_FIXED_VERB]:
            for m in re.finditer(pat, h, re.I):
                head_hits.append((num, m.group(0), f"heading: {h[:64]}"))
    r.hits("FAIL", "banned words in headings", head_hits)

    opener_hits = []
    for para in paragraphs(text):
        for pat in BANNED_OPENERS:
            if re.match(pat, para[0], re.I):
                opener_hits.append((0, para[0][:38], para[0][:88]))
    r.hits("FAIL", "banned openers", opener_hits)

    long_paras = [(0, f"{len(p)} lines", p[0][:88]) for p in paragraphs(text) if len(p) > 3]
    r.hits("FAIL", "paragraphs over 3 lines", long_paras)

    unsourced = []
    for num, line in scan_lines(text):
        if re.search(r"\b\d+(?:\.\d+)?%|\b\d{2,3}(?:,\d{3})+\b|\$\d", line) and "](" not in line:
            unsourced.append((num, "figure with no link", line[:88]))
    r.hits("WARN", "figures without a source link", unsourced)

    print("\nCADENCE")
    lengths = [len(s.split()) for s in sents]
    if lengths:
        total = len(lengths)
        b = {"1-9": sum(1 for n in lengths if n <= 9),
             "10-18": sum(1 for n in lengths if 10 <= n <= 18),
             "19-28": sum(1 for n in lengths if 19 <= n <= 28),
             "29+": sum(1 for n in lengths if n >= 29)}
        print("  ----  distribution   " +
              "  ".join(f"{k}: {v} ({round(100*v/total)}%)" for k, v in b.items()))
        if total < 25:
            r.line("PASS", "cadence spread", f"skipped, only {total} sentences")
        elif b["1-9"] / total > 0.42:
            r.line("FAIL", "punchy metronome", f"{round(100*b['1-9']/total)}% under 10 words")
        elif b["10-18"] / total > 0.70:
            r.line("FAIL", "flat metronome", f"{round(100*b['10-18']/total)}% in one bucket")
        else:
            r.line("PASS", "cadence spread")

        runs, cur = 0, 0
        for n in (lengths if total >= 25 else []):
            cur = cur + 1 if n <= 9 else 0
            if cur >= 3:
                runs += 1
        if runs:
            r.line("FAIL", "performed punch runs", f"{runs} run(s) of 3+ short sentences")
        else:
            r.line("PASS", "no punch-sentence runs")

    single = sum(1 for p in paragraphs(text) if len(p) == 1 and len(p[0].split()) <= 14)
    if single > 3:
        r.line("WARN", "single-sentence paragraphs", f"{single} found, ration to 3")
    else:
        r.line("PASS", "single-sentence paragraphs", str(single))

    shapes = [len([x for x in re.split(r"(?<=[.!?])\s", " ".join(p)) if x.strip()])
              for p in paragraphs(text)]
    worst, cur = 1, 1
    for i in range(1, len(shapes)):
        cur = cur + 1 if shapes[i] == shapes[i - 1] else 1
        worst = max(worst, cur)
    if worst >= 5:
        r.line("FAIL", "paragraph metronome", f"{worst} consecutive paragraphs of the same shape")
    elif worst == 4:
        r.line("WARN", "paragraph metronome", "4 consecutive paragraphs of the same shape")
    else:
        r.line("PASS", "paragraph shape varies", f"longest run {worst}")

    print("\nSTRUCTURE")
    triples = scan(text, [r"\b\w+,\s+\w+,\s+and\s+\w+\b(?!\s*[,:])"])
    if len(triples) > 3:
        r.line("WARN", "rule-of-three triplets", f"{len(triples)} found, use two or four")
    else:
        r.line("PASS", "rule-of-three triplets", str(len(triples)))

    r.hits("WARN", "copula dodges", scan(text, COPULA_DODGES))

    # Directive density. A piece written almost entirely in commands reads as a
    # lecture even when every sentence is clean. Mechanical steps ("copy the
    # folder in") are fine; judgement delivered as an order is not.
    IMPERATIVE = (r"^(Put|Write|Make|Get|Plan|Expect|Budget|Give|Take|Spend|Browse|"
                  r"Start|Leave|Fill|Store|Steal|Delete|Watch|Learn|Confirm|Flip|"
                  r"Skip|Verify|Stop|Avoid|Remember|Note|Consider|Ensure|Never|Always)\b")
    imp = [s_ for s_ in sents if re.match(IMPERATIVE, s_)]
    share = round(100 * len(imp) / len(sents)) if sents else 0
    if share > 12:
        r.line("FAIL", "directive density", f"{share}% of sentences open on a command")
        for s_ in imp[:6]:
            print(f'          "{s_[:86]}"')
    elif share > 7:
        r.line("WARN", "directive density", f"{share}% of sentences open on a command")
    else:
        r.line("PASS", "directive density", f"{share}%")

    anchors = re.findall(r"\[([^\]]+)\]\(", raw)
    bad = [(0, a, "anchor length") for a in anchors if not 2 <= len(a.split()) <= 4]
    r.hits("WARN", "anchors outside 2-4 words", bad)

    if words < 3000:
        r.line("WARN", "length", f"{words} words, target 3000+")
    else:
        r.line("PASS", "length", f"{words} words")

    print("\n" + "=" * 58)
    if r.fails:
        print(f"FAILED  {r.fails} hard check(s), {r.warns} warning(s). Not finished.")
        sys.exit(1)
    print(f"PASSED  0 hard failures, {r.warns} warning(s). Read every warning.")
    print("Now read it aloud. The gate cannot hear you.")
    sys.exit(0)


if __name__ == "__main__":
    main()
```
