Skip to content
Donate

Writing style

How JellyRock’s docs and code comments read: README, user guides, dev guides, architecture docs, ADRs, journals, the agent files (CLAUDE.md, skills, rules) and the comments in our code. People read the agent files too, to improve them or to do a task by hand, so they follow the same rules.

The goal is one plain voice that is quick to scan. If a sentence makes the reader work, rewrite it.

Each doc has one reader. Write for that person.

DocReaderThey want
README.md, docs/user/Someone using JellyRockWhat to do and what they will see. No internals.
docs/dev/A contributorSteps that work the first time.
docs/architecture/, docs/adr/A maintainerWhy the code is shaped this way, and what breaks if it changes.
CLAUDE.md, .claude/An agent, and the person improving itThe rule, the reason, and how to tell it applies.
Code commentsThe next person to change that codeWhat the code can’t say: the reason, the platform quirk, the trap.

Use a period, a comma, a colon or parentheses. An em dash joins two thoughts that usually read better as two sentences.

Before: Nothing else observes a Task's `state` — the task ledger reads it instead,
by design — and the scoped pair that would drop only this one needs Roku OS 12.
After: Nothing else observes a Task's `state`: the task ledger reads it instead.
The scoped pair would drop only this observer, but it needs Roku OS 12.

Capitalize the first word and proper nouns only: “Install the companion plugin”, not “Install The Companion Plugin”.

Put the conclusion or the action first, then the detail. Keep one idea per sentence and aim for under 25 words. A long sentence is usually two.

User docs never mention Task threads, observers or file paths. If a user doc needs that detail, link to the dev or architecture doc instead.

Bold what the reader sees on screen: Settings → Playback. Use bold for at most one warning per section. Never write words in capitals for emphasis (NOT, ONLY, MUST). If a word needs stress, the sentence probably needs restructuring.

Write “for example” and “that is”, not e.g. and i.e..

“Press Back to stop the load”, not “The load can be stopped by pressing Back”.

→ belongs in a bold UI path (Dashboard → Plugins). In prose, say what happens: “which leads to”, “then”.

Avoid: load-bearing, deliberately, genuinely, crucially, simply, just, note that. They add length and no meaning.

10. State the rule and why; keep the proof elsewhere

Section titled “10. State the rule and why; keep the proof elsewhere”

A doc says what is true and why it matters. Measurements, reproductions and the history of how you found out belong in the commit message or the ADR, where git blame reaches them. A dated measurement that is the point stays, with its date and device.

11. American spelling, symbols not line numbers

Section titled “11. American spelling, symbols not line numbers”

Write “behavior”, “color”, “license”. Cite a function by name (ApiQueueTask.forgetCaller()), never by line number. See writing-style.md and derive-dont-duplicate.md.

A comment follows the same rules as a doc. Rules 1, 5, 6, 8 and 9 apply as written. Rule 10 matters most: a comment carries what the next person needs to change the code correctly, and the proof (measurements, reproductions, how you found out) goes in the commit message. See code-style.md for what belongs in a comment at all.

Code a tool reads is not prose: a marker such as bsc-disable-next-line dead-code keep: api: <reason> keeps the form its tool parses.

Two checks run in pre-push and CI. npm run lint:voice runs voice-check.sh over every tracked Markdown file, against .voice-baseline.tsv; lint-staged runs it on each staged .md too. npm run lint:comment-voice runs comment-voice-ratchet.js over the comments of tracked code. Both count these per file (the Markdown check also counts line-number citations):

CategoryWhat it counts
em-dash—
fillerThe words in rule 9
caps-emphasisCommon words written in capitals for emphasis (NOT, ONLY, NEVER, MUST, ALWAYS)
prose-arrow→ outside bold text
latin-abbreve.g. and i.e.

It skips fenced code blocks, inline code and link targets, so commands and quoted tool output are never counted. A table cell holding only — is a “none” marker, not prose, and is skipped too. The Markdown check covers every tracked Markdown file except CHANGELOG.md (generated by CI) and the paths in .voice-ignore (locale/ and components/vendor/).

The comment ratchet counts the same categories in the comments of tracked code (.bs, .brs, .js, .cjs, .mjs, .xml, .yml, .yaml, .sh and the git hooks in .husky/), under the file’s own path in .doc-voice-baseline.json. Only comment text is read, by a parser per language, so a quote mark or // inside a string is never a comment:

LanguageHow comments are found
BrightScriptThe BrighterScript lexer (' and REM)
JavaScriptespree; a file that does not parse stops the run, naming the file
YAMLThe yaml lexer, so a # in a quoted value or URL is not a comment
XML<!-- --> blocks
ShellWhole-line # comments only, since a trailing # can sit inside a string and nothing parses shell here

Inline code and URLs inside a comment are skipped. Vendored and generated code (components/vendor/, roku_modules/, node_modules/, build/, out/) is out of scope.

Both checks are ratchets. Each file has an allowance in its baseline, and a file may only improve. A new file starts at zero. When a file drops below its allowance, lock the gain in: bash .claude/skills/log/voice-check.sh --lower for Markdown, node scripts/lint/comment-voice-ratchet.js --lower-voice-baseline for code comments.

The gate cannot judge rules 2, 3, 4, 7 and 10. Reviewers check those.