diff --git a/.claude/rules/comments.md b/.claude/rules/comments.md new file mode 100644 index 0000000..77a2b11 --- /dev/null +++ b/.claude/rules/comments.md @@ -0,0 +1,52 @@ +--- +paths: + - "src/**" + - "test/**" + - "scripts/**" +--- + +# Comments + +A code comment has exactly one purpose: to help a future maintainer change +this code correctly, by carrying the constraints the code and tests cannot +carry themselves. A comment addressed to anyone else (a reviewer, a diff +reader, the author's process) does not belong in the source. + +A comment may only state a constraint the code cannot show: an external fact +(a library's hidden behavior, a remote API quirk, a verified real-world +payload) or the why of a deliberately surprising choice. Never write a comment +that: + +- Explains what the adjacent code plainly shows, or restates a test name, an + assertion, or a log line next to it. +- Tells past-tense history, justifies a change, or talks to a reviewer ("used + to", "previously", "fixed", "split out from", "now", referencing a bug story + or issue number): the next reader sees only the final code. +- Guards a behavior a test could pin: if the constraint is testable and + untested, write the test, not the comment. A comment survives alongside a + test only where the code locally reads as a mistake (error swallowing, odd + ordering); then say why, clearly. +- Pads with redundant contrast ("X, not Y" where Y is merely not-X): state + the constraint once, positively. + +No literal em or en dashes in any tracked .ts file, comments and +user-facing strings included (scripts/check-dashes.sh gates the staged +content locally and in CI); use a comma, colon, or parenthetical. A fixture +or quoted payload that genuinely needs the character uses the \u2013 or +\u2014 escape, which the gate does not match. + +Every comment must pass the strip test: remove it, and if a competent reader +could recover the content from the code, the names, the contracts of the +functions called, and the tests, it should not exist. A causal veneer +("so ...", "because ...") does not turn narration into a why. A comment sits +ON the line it explains. Show, don't tell: real payloads and verified +behaviors belong in test fixtures, and once a test pins a constraint, a +comment restating it dies. Length is never the criterion: an +unneeded explanation is noise at any length, and a needed one gets however +many words it takes to explain clearly. A long comment on something simple is a +smell that warrants checking whether the content is needed at all, never a +violation by itself. The file's existing comment density +is not a license: judge each comment alone, and prefer a clearer name or a +test over any comment. When a review-refuted finding keeps resurfacing, first +try to make the code read right; an intent pin at the flagged site is the +last resort. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23f46f9..ad02576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - run: bunx oxlint --deny-warnings + # same dash gate as the local hook, for pushes that bypassed it + - run: ./scripts/check-dashes.sh # db.ts opens /storage/mp4ify.db and blob-store.ts creates /storage/blobs # at import time, so /storage must exist and be writable - run: sudo mkdir -p -m 777 /storage diff --git a/check.sh b/check.sh index 71d49c0..0bf5101 100755 --- a/check.sh +++ b/check.sh @@ -13,6 +13,8 @@ else exit 1 fi +./scripts/check-dashes.sh + # `timeout` runs inside the container, so a hung/non-exiting `bun test` (a leaked # handle, a runaway loop) self-kills and `--rm` cleans up, instead of leaving an # orphaned container pinning a CPU. -k force-kills if SIGTERM is ignored. diff --git a/scripts/check-dashes.sh b/scripts/check-dashes.sh new file mode 100755 index 0000000..ae56eec --- /dev/null +++ b/scripts/check-dashes.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# En/em dashes are banned in .ts source (see .claude/rules/comments.md). +# git grep --cached scans the INDEX, i.e. exactly what a commit would record +# (the worktree can differ both ways), and the '*.ts' pathspec tracks every +# .ts file wherever it lives, so a new top-level dir can't dodge the gate. +# LC_ALL=C pins byte-wise PCRE: \x{2013}-style escapes error under C locales +# while \xe2-style bytes mis-match under UTF-8 mode. The case turns a scan +# ERROR (e.g. a git without PCRE support) into a loud failure, never a +# silent pass. +st=0 +dashes=$(LC_ALL=C git grep --cached -nP '\xe2\x80[\x93\x94]' -- '*.ts') || st=$? +case $st in + 0) + printf '%s\n' "$dashes" + echo "em/en dash found (use a comma, colon, or parenthetical instead)" >&2 + exit 1 + ;; + 1) ;; + *) + echo "em/en dash scan failed to run" >&2 + exit 1 + ;; +esac