Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
MixedArticle text · 1,602 words · 10 segments analyzed
My book has a linter that yells at me for hyphenating “open source.”1 It runs ~5,500 automated checks, rebuilds five formats on every git push, and fails the build if I so much as imply I still work at a job I left. For a book. That one person wrote. I didn’t set out to do this. Most people start a writing project by firing up Word or Google Docs, and I started down that same path. I even tried some purpose-built authoring tools, but every tool felt inferior to the ones I used as a developer every day. I did “the only reasonable” thing and threw all of them out, writing the whole book on Git, Markdown, and a CI build pipeline. If you’ve read how I over-engineered my home network — twice — none of this will surprise you. I’ve been making websites for decades, so the leap was short: the same tools that build websites could build books, no last-minute magic-trick reveal required to abracadabra a standard Word doc into a full-blown book. There were missteps,2 but in the end, I would not have written Open and Async any other way. Here’s how: Content# Naturally, the content itself lived as Markdown files in a Git repository. After all, that’s where I spend most of my day. I used VS Code, with a handful of prose extensions (listed below). Each chapter was its own Markdown file, and a single index.yml file defined the order, making it easy to re-order chapters or add new ones.
Practically, I wrote most of this book on an iPad — Codespaces in a browser tab, a Bluetooth keyboard, often nights and weekends while away from my desk — and the Git repository kept everything in sync no matter where I opened it.
I could focus on the words, and a bad idea was one git revert away from gone. Not to mention, I had real-time feedback on my writing right in my IDE from the various prose linters, just as I would have real-time feedback on my code from ESLint or Prettier. Testing# With content as code, the next logical step — and the point where “reasonable” quietly left the building — was to set up automated tests. Testing prose the way you test code is something I’d argued for years; this was me taking it to an absurd extreme. I did that two ways: real-time, and on push (CI). Real-time# Locally, as I typed, I ran several VS Code extensions all giving me real-time feedback. Specifically: Markdownlint — Markdown syntax and formatting consistency Harper3 — grammar and word choice, entirely on-device LanguageTool — grammar, punctuation, and style4 Vale — my own house-style rules and banned terms Alex — insensitive or exclusionary phrasing Write-good — weak prose: passive voice, weasel words, clichés All six also ran in CI (Alex and Write-good folded into Vale there; more on that below). Together, they layered hundreds of curated style rules over a full grammar engine — all of it underlining my mistakes in real time, the way a red squiggle flags a type error. On each push# In addition to running those open source linters in CI (some blocking), I built a custom test suite of my own: a standalone Node script of content validators, a Vitest suite, and Playwright specs. The validators are the fun part. Each is a few lines that read the Markdown and push an error with a file-and-line pointer. My favorite: I don’t work at GitHub anymore, so any sentence claiming I still do fails the build.
// My time at GitHub has to read as past tense — present-tense// employment claims about it fail the build.function validateGitHubTense(files) { const patterns = [ // "is/are ... at GitHub" — a current-employment claim /\b(is|are)\b[^.!?\n]{1,80}?\bat GitHub\b/i, // "works/leads/runs at GitHub" — current-employment activity /\b(works?|leads?|runs?|manages?|directs?)\s+(at|for)\s+GitHub\b/i, ]; return flagLinesMatching(files, patterns);} That validator exists because I made the mistake once — which is the whole pattern. The first time an error got past me, I didn’t just fix that one sentence; I wrote a rule so I’d never have to catch it by eye again. It’s cattle, not pets for prose: don’t hand-nurse each chapter, govern the whole herd with policy. A mistake caught once becomes a check that sweeps every chapter and fails the build if it ever wanders back. That’s one of about thirty. Others I’m proud of: validateOpenSourceHyphenation — “open source” is a noun, not a verb, and never hyphenated. validateHypotheticalHooks — formulaic AI-tell openers (“Picture this…”, “Imagine…”, “Consider a…”) at the start of a paragraph. validateSentenceStarters — three-plus sentences in a row opening with the same word. validateNoBareUrlLinkText — no link whose visible text is just the raw URL. validateCalloutBalance — the book speaks to managers and individual contributors, so a “For managers” callout has to have a “For ICs” counterpart nearby. validateCrossReferences — every [text](#anchor) cross-reference resolves to a real heading. …plus a couple dozen more for small caps, em-dashes, doubled words, en-dash ranges, TL;DR length, and every other tic I could name.5 One of these caught me. validateHypotheticalHooks flagged the opening of a paragraph I was certain I’d written myself — and I had. But reading it back cold, it did sound ghost-authored; I’d absorbed the cadence from reading too much generated text and produced a fluent imitation of nothing. A linter can’t tell good prose from bad. What it can flag are the patterns you reach for when you’ve stopped thinking — which is exactly the thing you can’t see in your own draft. Same reason eslint earns its keep: it can’t tell good code from bad either, but it catches the autopilot mistakes your own eye skates right over.
All in all, the CI suite ran ~70 test files with 2,204 test cases and ~3,900 expect() assertions — plus another ~1,600 per-chapter structural checks from the validators above.
Audits# The linters caught mistakes, but they couldn’t tell me whether the book repeated itself or whether a chapter was any good. For that I built a second layer of tooling: audits that judge the writing, not just check it. One thing they never did, though, was write it. Every word is mine; these tools are readers of last resort — catching what I’d stopped being able to see. Duplication detection# After reading the book over and over, I was convinced I’d repeated the same idea across chapters. I wanted proof, not a hunch — so I built three layers of duplication detection, each catching what the one before it misses: jscpd — token-level copy-paste detection.
Catches longer verbatim blocks I’d pasted between chapters, but nothing subtler.6 n-gram — tokenizes every chapter in index.yml, strips Markdown/Pandoc syntax, builds word n-grams (phrases of n words), and flags phrases appearing in more than one chapter (plus a “most-repeated stock wording” ranking). Two modes: cross-chapter (default --n) and intra-chapter (--scope=intra --n=8) for a chapter repeating itself.
semantic — the one the other two can’t do: the same point restated in different words. An on-demand LLM audit, designed so it never feeds the whole book to a model — three passes, each over small units: intra — one call per chapter: “where does this chapter restate itself?” cross — a single call over every chapter’s TL;DR, producing a map of conceptually overlapping chapters. A whole-book scan for the cost of one call. arguments — extract each chapter’s load-bearing claims one call at a time, then cluster the same argument across all chapters in one final call. Catches arguments made in body prose that cross misses. Was I actually repeating myself? By this final pass the two mechanical layers came up clean — but they’d earned it: jscpd and the n-gram scan had already caught the copy-paste and recycled phrasing (a doubled motif here, a reused TL;DR structure there), and I’d fixed each. What neither could see was the subtler kind — nothing was duplicated word-for-word anymore, just the same point in different words.
The semantic pass caught that, and it was right: I’d made the same argument, that moving office habits online isn’t the same as working remote-first, in five separate chapters, on top of 166 smaller self-restatements scattered across 51 chapters. I’d paraphrased myself too well for anything cheaper than an LLM to catch me. (share this quote) My hunch was correct; I’d just needed three escalating tools to prove what re-reading my own book to blurry-eyed exhaustion couldn’t.
Content audits# Beyond duplication, a second set of tools graded the prose itself — split by how they judge. Some are probabilistic (an LLM reads the chapter and forms an opinion; run it twice and the findings can shift), and some are deterministic (rules and arithmetic — same input, same output, every time). Probabilistic (LLM) audits# The Claims lens paid for itself early. It stopped on a sentence claiming GitHub’s monthly all-hands “dedicated roughly half of each session to live Q&A” — a number I was certain of and had wrong; it was closer to a third, and the share moved around over the years. No linter flags that: it’s clean, confident prose that happens to be false. Only a reader asking “is this actually true?” catches it, and I’d have shipped it otherwise. Claims is one of 20 single-purpose lenses in a “prose audits” test — a per-chapter LLM auditor where each lens asks one narrow question so the model can’t hand-wave a vague “looks good.” --lens=all runs every lens; --models/--rounds add a deduplicated multi-model and self-consistency panel, so a finding has to survive more than one model (or more than one run) to count.