From 1f309c02a64a698565a04c8e2665411c7d1d7c34 Mon Sep 17 00:00:00 2001 From: Vivek Date: Sat, 8 Aug 2026 18:03:16 +0530 Subject: [PATCH] fix: scan JSON and front-matter prose values for banned punctuation The prose-punctuation hook gated its pause-hyphen and pause-semicolon rules on four line shapes (a comment, a markdown heading, a blockquote, an HTML prose tag), so a JSON string value matched none of them and invariant 11 shipped straight through. The repo root manifest description and the ui registry description both carried a pause-hyphen because of it, and the repo-wide punctuation cleanup that introduced one of them was not caught. Rules 2 and 3 now also scan a description / title / displayName value, in JSON and in column-0 YAML front matter. The scope is the KEY, not the file, which is what keeps the rule off semver ranges, script commands, urls, paths and globs, since every one of those lives under a different key. Rules 1 through 4 also silently stopped enforcing on a payload past the pipe buffer: grep -q exits on its first match, that closes the pipe under printf, and under pipefail the SIGPIPE became the pipeline status, so the if was false and the rule skipped. Measured 0 of 8 blocks at 200 KB before, 8 of 8 after. Every match now reads from a here-string. --- .claude/hooks/block-prose-punctuation.sh | 92 ++++++++---- AGENTS.md | 2 +- .../.claude/hooks/block-prose-punctuation.sh | 90 +++++++++--- package.json | 2 +- .../.claude/hooks/block-prose-punctuation.sh | 90 +++++++++--- packages/server/src/actions.js | 2 +- packages/server/src/check.js | 2 +- packages/ui/packages/registry/package.json | 2 +- .../packages/registry/themes/base-colors.js | 2 +- test/hooks/block-prose-punctuation.test.mjs | 139 ++++++++++++++++++ 10 files changed, 344 insertions(+), 79 deletions(-) diff --git a/.claude/hooks/block-prose-punctuation.sh b/.claude/hooks/block-prose-punctuation.sh index d48f8bf16..eb409e3a5 100755 --- a/.claude/hooks/block-prose-punctuation.sh +++ b/.claude/hooks/block-prose-punctuation.sh @@ -2,14 +2,16 @@ # # PreToolUse hook: block prose-punctuation patterns the webjs convention bans. # -# Catches four classes of new content in tool calls: +# Catches five classes of new content in tool calls: # # 1. U+2014 em-dash, anywhere. # 2. Space-hyphen-space " - " in PROSE contexts (comment lines, markdown -# lines, headings, blockquotes). Math expressions in code like +# lines, headings, blockquotes, a JSON "description" / "title" / +# "displayName" string value, and a column-0 YAML front-matter +# description: / title: / displayName: line). Math expressions in code like # `Math.abs(a - b)` or `arr.length - 1` are NOT flagged. -# 3. Space-semicolon-space " ; " in PROSE contexts. JS / CSS statement -# terminators (`;\n`) are NOT flagged. +# 3. Space-semicolon-space " ; " in the same PROSE contexts as rule 2. +# JS / CSS statement terminators (`;\n`) are NOT flagged. # 4. Code-shaped left-hand side immediately followed by a colon and prose: # - `foo():` (markdown code-LHS in docs) # - `:` (custom-element tag with hyphen) @@ -19,7 +21,7 @@ # except a `webjs ` CLI command and literal code tokens # (@webjsdev, webjs.dev, "webjs", WEBJS_*, webjsdev/webjs, code spans). # -# Why this exists: see AGENTS.md "Invariants", item 10. These patterns +# Why this exists: see AGENTS.md "Invariants", item 11. These patterns # confuse AI agents that try to parse the prose as TypeScript / shorthand- # method / object-literal syntax, and trip humans reading API docs. # @@ -50,8 +52,13 @@ if [ -z "$new_content" ]; then exit 0 fi +# Every match below reads from a here-string, never a pipe. `grep -q` exits on +# the first match, which closes a pipe under `printf`, and with `set -o pipefail` +# that SIGPIPE became the pipeline status, so the rule silently skipped on any +# payload past the pipe buffer (measured: 0 of 8 blocks at 128 KB). + # --- 1. U+2014 em-dash -------------------------------------------------- -if printf '%s' "$new_content" | grep -q $'\xe2\x80\x94'; then +if grep -q $'\xe2\x80\x94' <<< "$new_content"; then cat >&2 <<'EOF' BLOCKED: em-dash (U+2014) detected in this tool call. @@ -61,7 +68,7 @@ restructured sentence. Do NOT replace it with " - " or " ; " or a trailing colon on code: those are also banned. See rule 2 / 3 / 4 below for the alternatives. -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 @@ -85,25 +92,42 @@ block_pause_hyphen=0 # `*` (markdown bold-start would have a letter after, distinguishable), # followed by prose with `\w+ - \w+` pattern. Specifically: catch lines # like `// foo - bar`, ` * foo - bar`, `* foo - bar`. -if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then +if grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then block_pause_hyphen=1 fi # Markdown heading " - " pause: line starts with `#` followed by prose # and ` - ` pattern. -if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then +if grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then block_pause_hyphen=1 fi # Markdown blockquote " - " pause: line starts with `>` followed by prose # and ` - ` pattern. (Single `>` blockquote, not table.) -if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then +if grep -qE '^>[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then block_pause_hyphen=1 fi # HTML / markdown

,

  • , body " - " pause: line contains a # closing HTML tag from a prose context, then prose-style ` - `. -if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then +if grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then + block_pause_hyphen=1 +fi + +# JSON prose-value " - " pause: a string assignment whose KEY is one of the +# three prose-bearing keys this project's JSON uses. Scoping to the key is what +# keeps this off semver ranges, script commands, urls, paths and globs, every +# one of which lives under a different key. Shape, not file path: the Bash +# payload carries no file_path, so a heredoc writing a manifest is covered too. +if grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then + block_pause_hyphen=1 +fi + +# YAML front-matter " - " pause, same three keys. Anchored at column 0 with no +# leading whitespace, which is what confines it to document front matter: every +# nested YAML mapping is indented, including the workflow-input `description:` +# values in .github/workflows/release.yml. +if grep -qE '^(description|title|displayName):[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]' <<< "$new_content"; then block_pause_hyphen=1 fi @@ -122,13 +146,17 @@ restructured phrasing. Bad:
  • Foo - bar.
  • Good:
  • Foo, with bar.
  • + Bad: "description": "A library - for things" + Good: "description": "A library for things" + Plain hyphens are still fine in compound words (`AI-first`), CLI flags (`--http2`), filenames, ranges, and math expressions in code (`arr.length - 1`, `Math.abs(a - b)`). The hook only flags the ` < word > - < word > ` pause-pattern in prose contexts (comments, -markdown headings, blockquotes, HTML prose tags). +markdown headings, blockquotes, HTML prose tags, and a JSON or +front-matter description / title / displayName value). -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 @@ -138,19 +166,29 @@ fi # Same prose-context guard as #2. block_pause_semicolon=0 -if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then +if grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then block_pause_semicolon=1 fi -if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then +if grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then block_pause_semicolon=1 fi -if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then +if grep -qE '^>[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then block_pause_semicolon=1 fi -if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then +if grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then + block_pause_semicolon=1 +fi + +# JSON prose-value " ; " pause, same three keys as rule 2. +if grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then + block_pause_semicolon=1 +fi + +# YAML front-matter " ; " pause, column-0 anchored like rule 2. +if grep -qE '^(description|title|displayName):[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]' <<< "$new_content"; then block_pause_semicolon=1 fi @@ -165,10 +203,14 @@ two sentences (period) or with a conjunction (", and", ", but", ", so"). Good: // Forms work. Links work too. Good: // Forms work, and links work too. + Bad: "description": "Forms work ; links work too." + Good: "description": "Forms work. Links work too." + Semicolons stay fine inside code (JS statement terminators, CSS -declarations) since those are not flagged. +declarations) since those are not flagged. Only the space-surrounded +form is banned, so an ordinary English semicolon is untouched. -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 @@ -179,7 +221,7 @@ fi # lowercase prose. The `):` shape is unambiguous: this is markdown, # not code, AND the inner code ends in `()` so the colon visually parses # as a return-type annotation. -if printf '%s' "$new_content" | grep -qE '\):[[:space:]][a-z]'; then +if grep -qE '\):[[:space:]][a-z]' <<< "$new_content"; then cat >&2 <<'EOF' BLOCKED: code-LHS colon-then-prose detected ("foo(): ..."). @@ -190,7 +232,7 @@ parses as a TypeScript return-type annotation. Rewrite verb-led. Good: repeat() is the keyed list directive Good: startServer() creates an HTTP(S) server -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 @@ -199,7 +241,7 @@ fi # --- 4b. Custom-element-tag : prose ------------------------------ # HTML reserves hyphenated tag names for custom elements (W3C spec), so # `:` is unambiguous prose, never JSX / TS / CSS. -if printf '%s' "$new_content" | grep -qE '<[a-z][a-z0-9]*(-[a-z0-9]+)+([[:space:]][^>]*)?>:[[:space:]][a-z]'; then +if grep -qE '<[a-z][a-z0-9]*(-[a-z0-9]+)+([[:space:]][^>]*)?>:[[:space:]][a-z]' <<< "$new_content"; then cat >&2 <<'EOF' BLOCKED: custom-element-tag colon-then-prose detected (": ..."). @@ -210,7 +252,7 @@ webjs bans `: ` in comments and docs. Rewrite verb-led. Bad: // : the centered panel. Good: // is the centered panel. -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 @@ -220,7 +262,7 @@ fi # Match comment-line prefix (`//` or leading `*`) before `\w+(...): ` and # lowercase prose. Avoids TS return-type annotations because those never # appear inside comment lines. -if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]][^(]*[A-Za-z_][A-Za-z0-9_]*\([^)]*\):[[:space:]][a-z]'; then +if grep -qE '^[[:space:]]*(//|\*)[[:space:]][^(]*[A-Za-z_][A-Za-z0-9_]*\([^)]*\):[[:space:]][a-z]' <<< "$new_content"; then cat >&2 <<'EOF' BLOCKED: comment-line code-LHS colon-then-prose detected ("// foo(): ..."). @@ -231,7 +273,7 @@ webjs bans `xyz(): ` inside comments and JSDoc. Rewrite verb-led. Bad: // closest(): null if the click wasn't inside a frame Good: // closest() returns null when the click wasn't inside a frame -Rule: AGENTS.md, Invariants section, item 10. +Rule: AGENTS.md, Invariants section, item 11. Hook: .claude/hooks/block-prose-punctuation.sh. EOF exit 2 diff --git a/AGENTS.md b/AGENTS.md index 16da4e9df..be8646165 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -482,7 +482,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); 9. **No backtick characters inside `html\`...\`` template bodies**, even inside CSS / HTML comments. A nested backtick closes the literal at JS-parse time and 500s in prod. 10. **TypeScript must be erasable.** Set `compilerOptions.erasableSyntaxOnly: true`. No `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators with `emitDecoratorMetadata`, no `import = require`. Types are stripped via Node 24+'s `module.stripTypeScriptTypes` (buildless, no bundler fallback); non-erasable syntax 500s at strip time. Enforced by `erasable-typescript-only` (tsconfig flag) and `no-non-erasable-typescript` (source scan). See `references/typescript.md`. -11. **No em-dashes (U+2014), no hyphen or semicolon used as pause-punctuation in prose, and no colon attached to a code-shaped LHS.** Banned as a pause: U+2014, a space-surrounded hyphen between words, a space-surrounded semicolon between words. Banned colon attachments: a colon-then-prose after `xyz()`, a ``, an `[expr]` subscript, or a `foo()` definition list (rephrase verb-led). Prefer a period, comma, a colon on a plain-noun LHS, parentheses, or a restructure. Plain hyphens stay fine in compound words, flags, filenames, ranges; semicolons and colons stay fine inside code / TS / JSON / CSS. The same hook also enforces brand casing with one simple rule: `WebJs` is a proper noun, so write it capitalized wherever it NAMES the project in prose, at a sentence start AND mid-sentence (`WebJs ships`, `Most WebJs apps`, `the WebJs serializer`). It stays lowercase `webjs` ONLY as a literal code token: a `webjs ` CLI command (`webjs dev`, `webjs db migrate`), a `webjs.dev` domain, an `@webjsdev` package, a `"webjs"` config key, a `WEBJS_*` env var, the `webjsdev/webjs` org path, or anything inside a `` `code` `` span or fenced block. If you mean the literal config key or command in prose, wrap it in backticks. Enforced via `.claude/hooks/block-prose-punctuation.sh`, which scans only NEW content (you can still edit an existing line to fix a glyph or casing). +11. **No em-dashes (U+2014), no hyphen or semicolon used as pause-punctuation in prose, and no colon attached to a code-shaped LHS.** Banned as a pause: U+2014, a space-surrounded hyphen between words, a space-surrounded semicolon between words. Banned colon attachments: a colon-then-prose after `xyz()`, a ``, an `[expr]` subscript, or a `foo()` definition list (rephrase verb-led). Prefer a period, comma, a colon on a plain-noun LHS, parentheses, or a restructure. Plain hyphens stay fine in compound words, flags, filenames, ranges, and semicolons and colons stay fine inside code / TS / CSS and inside JSON SYNTAX. A JSON or front-matter `description`, `title`, or `displayName` VALUE is prose, not code, and is scanned like any other prose. The same hook also enforces brand casing with one simple rule: `WebJs` is a proper noun, so write it capitalized wherever it NAMES the project in prose, at a sentence start AND mid-sentence (`WebJs ships`, `Most WebJs apps`, `the WebJs serializer`). It stays lowercase `webjs` ONLY as a literal code token: a `webjs ` CLI command (`webjs dev`, `webjs db migrate`), a `webjs.dev` domain, an `@webjsdev` package, a `"webjs"` config key, a `WEBJS_*` env var, the `webjsdev/webjs` org path, or anything inside a `` `code` `` span or fenced block. If you mean the literal config key or command in prose, wrap it in backticks. Enforced via `.claude/hooks/block-prose-punctuation.sh`, which scans only NEW content (you can still edit an existing line to fix a glyph or casing). 12. **A form that writes binds its action: `
    `, and a form whose buttons run different actions binds each on its submitter, `