taste-lint 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +4 -2
  2. package/data/corpus/copywriting.jsonl +4 -4
  3. package/data/rule-drafts/interaction/interaction-layout-shift.yaml +3 -3
  4. package/data/rules/copywriting/copywriting-antithesis-contrast.yaml +43 -0
  5. package/data/rules/copywriting/copywriting-antithesis-frame.yaml +4 -2
  6. package/data/rules/copywriting/copywriting-artificial-balance.yaml +4 -1
  7. package/data/rules/copywriting/copywriting-boilerplate-offer.yaml +1 -1
  8. package/data/rules/copywriting/copywriting-canned-phrasing.yaml +1 -1
  9. package/data/rules/copywriting/copywriting-claim-without-evidence.yaml +10 -1
  10. package/data/rules/copywriting/copywriting-copula-avoidance.yaml +1 -1
  11. package/data/rules/copywriting/copywriting-dead-link.yaml +28 -0
  12. package/data/rules/copywriting/copywriting-em-dash-in-ui.yaml +23 -0
  13. package/data/rules/copywriting/copywriting-excessive-hedging.yaml +1 -1
  14. package/data/rules/copywriting/copywriting-external-arrow-internal-link.yaml +26 -0
  15. package/data/rules/copywriting/copywriting-fabricated-specificity.yaml +6 -3
  16. package/data/rules/copywriting/copywriting-generic-framing.yaml +12 -6
  17. package/data/rules/copywriting/copywriting-generic-language.yaml +12 -4
  18. package/data/rules/copywriting/copywriting-generic-validation.yaml +1 -1
  19. package/data/rules/copywriting/copywriting-machine-prose.yaml +4 -1
  20. package/data/rules/copywriting/copywriting-mechanism-over-outcome.yaml +4 -1
  21. package/data/rules/copywriting/copywriting-meta-commentary.yaml +1 -1
  22. package/data/rules/copywriting/copywriting-missing-reasoning-link.yaml +4 -1
  23. package/data/rules/copywriting/copywriting-over-explanation.yaml +4 -1
  24. package/data/rules/copywriting/copywriting-participle-tail.yaml +1 -1
  25. package/data/rules/copywriting/copywriting-prompt-restatement.yaml +5 -2
  26. package/data/rules/copywriting/copywriting-punctuation-tics.yaml +4 -1
  27. package/data/rules/copywriting/copywriting-reader-first.yaml +4 -1
  28. package/data/rules/copywriting/copywriting-repetitive-transitions.yaml +1 -1
  29. package/data/rules/copywriting/copywriting-semantic-links.yaml +2 -2
  30. package/data/rules/copywriting/copywriting-slogan-headings.yaml +27 -0
  31. package/data/rules/copywriting/copywriting-structural-symmetry.yaml +4 -1
  32. package/data/rules/copywriting/copywriting-tee-up.yaml +4 -2
  33. package/data/rules/copywriting/copywriting-unexplained-action.yaml +5 -2
  34. package/data/rules/copywriting/copywriting-unnatural-polish.yaml +8 -5
  35. package/data/rules/copywriting/copywriting-unverifiable-proof.yaml +1 -1
  36. package/data/rules/copywriting/copywriting-vague-error.yaml +2 -0
  37. package/data/rules/craft/craft-atmosphere-gradient.yaml +1 -1
  38. package/data/rules/craft/craft-decoration-no-role.yaml +2 -1
  39. package/data/rules/craft/craft-decorative-ordinals.yaml +26 -0
  40. package/data/rules/craft/craft-faux-product-chrome.yaml +4 -1
  41. package/data/rules/engineering/engineering-catch-rethrow.yaml +42 -0
  42. package/data/rules/engineering/engineering-ci-cancels-default-branch.yaml +39 -0
  43. package/data/rules/engineering/engineering-ci-coverage-on-pr.yaml +38 -0
  44. package/data/rules/engineering/engineering-ci-full-clone.yaml +39 -0
  45. package/data/rules/engineering/engineering-ci-no-concurrency.yaml +33 -0
  46. package/data/rules/engineering/engineering-ci-uncached-install.yaml +32 -0
  47. package/data/rules/engineering/engineering-dead-branch.yaml +42 -0
  48. package/data/rules/engineering/engineering-debug-log-in-ui.yaml +42 -0
  49. package/data/rules/engineering/engineering-empty-catch.yaml +50 -0
  50. package/data/rules/engineering/engineering-hardcoded-secret.yaml +49 -0
  51. package/data/rules/engineering/engineering-heavy-dependency.yaml +42 -0
  52. package/data/rules/engineering/engineering-insecure-random-token.yaml +43 -0
  53. package/data/rules/engineering/engineering-non-atomic-writes.yaml +53 -0
  54. package/data/rules/engineering/engineering-per-render-allocation.yaml +50 -0
  55. package/data/rules/engineering/engineering-query-in-loop.yaml +51 -0
  56. package/data/rules/engineering/engineering-request-waterfall.yaml +50 -0
  57. package/data/rules/engineering/engineering-restating-comment.yaml +61 -0
  58. package/data/rules/engineering/engineering-sensitive-data-logged.yaml +53 -0
  59. package/data/rules/engineering/engineering-silent-fallback.yaml +51 -0
  60. package/data/rules/engineering/engineering-stale-feature-flag.yaml +51 -0
  61. package/data/rules/engineering/engineering-sync-io-in-handler.yaml +59 -0
  62. package/data/rules/engineering/engineering-template-residue.yaml +43 -0
  63. package/data/rules/engineering/engineering-test-focused.yaml +33 -0
  64. package/data/rules/engineering/engineering-test-mock-echo.yaml +41 -0
  65. package/data/rules/engineering/engineering-test-render-only.yaml +41 -0
  66. package/data/rules/engineering/engineering-test-self-comparison.yaml +33 -0
  67. package/data/rules/engineering/engineering-test-skipped.yaml +33 -0
  68. package/data/rules/engineering/engineering-token-in-local-storage.yaml +43 -0
  69. package/data/rules/engineering/engineering-unbounded-read.yaml +52 -0
  70. package/data/rules/schema/rule.schema.json +13 -1
  71. package/data/rules/seo/seo-canonical-wrong-host.yaml +43 -0
  72. package/data/rules/seo/seo-sitemap-build-time-lastmod.yaml +32 -0
  73. package/data/rules/seo/seo-unconditional-noindex.yaml +51 -0
  74. package/dist/cli.js +1313 -120
  75. package/dist/cli.js.map +1 -1
  76. package/dist/index.d.ts +47 -22
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +1134 -151
  79. package/dist/index.js.map +1 -1
  80. package/package.json +2 -2
  81. package/data/rules/copywriting/copywriting-idea-repetition.yaml +0 -34
package/README.md CHANGED
@@ -43,11 +43,13 @@ Checks bill your key and cache between runs. `--dry-run` previews the cost first
43
43
  - **Motion:** `transition-all` and `transition: all`, ease-in, linear easing, transitions over 300ms, entrances from scale zero, `framer-motion` imports.
44
44
  - **Copy:** the [ghostwriter](https://github.com/mblode/agent-skills/tree/main/skills/ghostwriter) structure tells (participle tails, “it’s not X, it’s Y”, “Here’s the thing”), “click here” links, “successfully” toasts, emoji in UI, nominalisations, anthropomorphism, long sentences that chain clauses.
45
45
  - **Typography:** straight quotes, three-dot ellipses, hyphens for dashes, `x` for ×, missing space before a unit, light weights on small body text, uppercase without tracking.
46
+ - **Code** (`--profile code`): N+1 queries, unbounded reads, blocking sync calls in handlers, tests that assert nothing or only that a mock was called, logic your coverage report shows no test runs, commented-out code, unused dependencies, dropped errors, secrets and personal data in logs, CI runs that never cancel.
47
+ - **Search:** pages with no metadata, a missing `metadataBase`, sitemap or robots, noindex on public pages, canonical URLs on a dev host.
46
48
  - **Under review:** vague errors, empty states with no action, bare “Confirm” labels, claims with nothing to check, missing error states. These go to [Jev by TypeSafe AI](https://docs.typesafe.ai/introduction), which answers with a probability.
47
49
 
48
- 28 checks block. 125 more report and never fail a run. They are pattern ports nobody has yet watched on a real codebase, and Jev questions no labelled corpus has yet promoted. See [usage](https://blode.co/taste-lint/docs/usage) for how promotion works.
50
+ 28 checks block. 177 more report and never fail a run. They are pattern ports nobody has yet watched on a real codebase, and Jev questions no labelled corpus has yet promoted. See [usage](https://blode.co/taste-lint/docs/usage) for how promotion works.
49
51
 
50
- Profiles scope a run: `product` (tsx, jsx, css), `writing` (md, mdx, README), `instructions` (AGENTS.md, skills, plans), `all`.
52
+ Profiles scope a run: `product` (tsx, jsx, css), `writing` (md, mdx, README), `instructions` (AGENTS.md, skills, plans), `code` (source, tests, CI), `all`.
51
53
 
52
54
  Add your own voice with a rule pack: `"rules": ["$GHOSTWRITER_HOME/taste-lint"]` in `taste-lint.config.json` checks drafts against your [ghostwriter](https://github.com/mblode/agent-skills/tree/main/skills/ghostwriter) profile.
53
55
 
@@ -133,7 +133,7 @@
133
133
  {"categoryId":"actionable-cta","id":"tt-x-qc-locate-default-button","kind":"jsx-text","labelSource":"manifest-weak","labels":{"copywriting-cta-names-outcome":true,"copywriting-friction-cta":true},"source":{"id":"qc-locate-default","path":"apps/web/content/exercises/train-your-eye.tsx","repo":"mblode/taste-training","option":"button"},"split":"dev","text":"Submit","context":{"docType":"ui","role":"body"}}
134
134
  {"categoryId":"typographic-detail","id":"tt-x-tye-repair-quotes-before","kind":"jsx-text","labelSource":"manifest-weak","labels":{"typography-straight-quotes":true,"typography-dashes":true,"typography-ellipsis":true,"typography-nbsp-value-unit":true},"source":{"id":"tye-repair-quotes","path":"apps/web/content/exercises/train-your-eye.tsx","repo":"mblode/taste-training","option":"before"},"split":"dev","text":"\"Sync everything.\"","context":{"docType":"ui","role":"body"}}
135
135
  {"categoryId":"typographic-detail","id":"tt-x-tye-repair-quotes-fixed","kind":"jsx-text","labelSource":"manifest-weak","labels":{"typography-straight-quotes":false,"typography-dashes":false,"typography-ellipsis":false,"typography-nbsp-value-unit":false},"source":{"id":"tye-repair-quotes","path":"apps/web/content/exercises/train-your-eye.tsx","repo":"mblode/taste-training","option":"curly"},"split":"dev","text":"“Sync everything.”","context":{"docType":"ui","role":"body"}}
136
- {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-1","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":true,"copywriting-machine-prose":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"We believe that teams of all sizes deserve powerful, easy to use analytics. Our mission is to help you be more productive and save time and money."}
137
- {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-2","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":true,"copywriting-machine-prose":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"This is a comprehensive platform that provides seamless integration with all the tools you need. It is a robust solution designed to be flexible and intuitive."}
138
- {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-3","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":true,"copywriting-machine-prose":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"Whether you're a startup founder or an enterprise leader, our cutting-edge product helps you grow your business and take it to the next level. In order to get started, sign up and create an account."}
139
- {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-4","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":true,"copywriting-machine-prose":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"Furthermore, our innovative approach utilizes next-generation technology to deliver best-in-class results. Contact us to request a demo and see everything you need, and much more."}
136
+ {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-1","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":false},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"We believe that teams of all sizes deserve powerful, easy to use analytics. Our mission is to help you be more productive and save time and money."}
137
+ {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-2","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":false},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"This is a comprehensive platform that provides seamless integration with all the tools you need. It is a robust solution designed to be flexible and intuitive."}
138
+ {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-3","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":true,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":false,"copywriting-machine-prose":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"Whether you're a startup founder or an enterprise leader, our cutting-edge product helps you grow your business and take it to the next level. In order to get started, sign up and create an account."}
139
+ {"categoryId":"evidence-over-claims","context":{"docType":"marketing","role":"body"},"id":"tt-sweep-sample-4","kind":"paragraph","labelSource":"sweep","labels":{"copywriting-dead-weight-hedges":false,"copywriting-generic-audience":true,"copywriting-claim-without-evidence":true,"copywriting-register-shift":true},"source":{"id":"SAMPLE_COPY","path":"apps/web/components/demos/copywriting/sweep-runner.tsx","repo":"mblode/taste-training"},"split":"dev","text":"Furthermore, our innovative approach utilizes next-generation technology to deliver best-in-class results. Contact us to request a demo and see everything you need, and much more."}
@@ -31,11 +31,11 @@ question:
31
31
  - TODO
32
32
  what: >-
33
33
  - Skip files with `// ui-audit-ignore:states-layout-shift`.
34
-
34
+
35
35
  - Below-the-fold `content-visibility: auto` may CLS-shift inside its own subtree (acceptable).
36
-
36
+
37
37
  - Skip components declaring `min-height` via CSS class (Tailwind `min-h-*`); inspect class strings first.
38
-
38
+
39
39
  - Skip Storybook fixtures.
40
40
  "true":
41
41
  examples:
@@ -0,0 +1,43 @@
1
+ # The short denial forms models write ("A folder, not a portal", "Not another
2
+ # place to work."). A regex cannot tell them from a plain instruction ("Use
3
+ # their numbers, not benchmarks") or a clarification ("Unknown: ... Not a
4
+ # pass."), so the regex only finds candidates and Jev decides.
5
+ id: copywriting-antithesis-contrast
6
+ title: Denies a thing nobody claimed to sharpen a point
7
+ categoryId: machine-prose
8
+ source:
9
+ {
10
+ repo: mblode/agent-skills,
11
+ path: skills/ghostwriter/references/tells.md,
12
+ line: 12,
13
+ }
14
+ unit: [paragraph, heading, jsx-text]
15
+ mechanical:
16
+ regex: "[\\p{L}\\p{N}’'], not (?:a |an |the |another |just |only )?[\\p{L}\\p{N}’'-]+|(?:^|[.!?]\\s+)Not (?:a|an|another|the|just|only|one) [\\p{L}\\p{N}’'-]+"
17
+ flags: gu
18
+ question:
19
+ context: [docType, element]
20
+ instructions: >-
21
+ Does TEXT deny something that nobody in the text claimed, only to make its own point sound sharper? Return false when the contrast is an instruction choosing between two real options the reader has, when it corrects a misreading the reader is likely to make, or when it states a real trade-off or limit.
22
+ criteria:
23
+ true:
24
+ what: A denial of a straw alternative, used as rhetoric.
25
+ examples:
26
+ - "A folder, not a portal."
27
+ - "A shared folder. Not another place to work."
28
+ - "Conflicts are kept, not resolved for you."
29
+ - "Side by side, not overwritten."
30
+ false:
31
+ what: A contrast the reader can act on or would otherwise get wrong.
32
+ examples:
33
+ - "Use their numbers, not generic benchmarks."
34
+ - "Unknown: a check that could not decide. Not a pass."
35
+ - "Put links in the comments, not the post body."
36
+ - "Billed per seat, not per device."
37
+ thresholds: { review: 0.4, act: 0.75 }
38
+ severity: minor
39
+ fix:
40
+ hint: Say the positive thing. "A folder, not a portal" becomes "A folder on every laptop."
41
+ preconditions: { notInCode: true }
42
+ status: review-only
43
+ handWritten: [mechanical, question, fix, preconditions, status]
@@ -1,3 +1,5 @@
1
+ # The short denial forms ("A folder, not a portal") need a judge, so they live
2
+ # in copywriting-antithesis-contrast; this rule stays exact enough to be active.
1
3
  # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
4
  id: copywriting-antithesis-frame
3
5
  title: Sets up a claim by denying one nobody made
@@ -8,9 +10,9 @@ source:
8
10
  path: skills/ghostwriter/references/tells.md,
9
11
  line: 12,
10
12
  }
11
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
12
14
  mechanical:
13
- regex: "(?<![\"“‘'])\\bnot (?:just|only|merely) (?:a |an |the |about )?[\\w-]+(?: [\\w-]+){0,4}[,;]? but\\b|(?<![\"“‘'])\\b(?:it|this|that)(?:['’]s| is)(?: not|n['’]t) (?:just |only )?(?:about )?[^.!?,;]{1,40}[.,;] (?:it|this|that)(?:['’]s| is)\\b"
15
+ regex: "(?<![\"“‘'])\\bnot (?:just|only|merely) (?:a |an |the |about )?[\\w-]+(?: [\\w-]+){0,4}(?:[,;]? but|[,;] (?:it|they|we|this)(?:['’](?:s|re)| is| are))\\b|(?<![\"“‘'])\\b(?:it|this|that)(?:['’]s| is)(?: not|n['’]t) (?:just |only )?(?:about )?[^.!?,;]{1,40}[.,;] (?:it|this|that)(?:['’]s| is)\\b"
14
16
  flags: giu
15
17
  severity: minor
16
18
  fix:
@@ -10,7 +10,10 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: artificial_balance
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
18
  context: [docType]
16
19
  instructions: >-
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: boilerplate_offer
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  flags: gui
16
16
  regex: "let me know|feel free to|happy to help|hope this helps|if you have any (?:other |further )?questions|reach out if"
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: canned_phrasing
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  phrases:
16
16
  - in today's fast-paced world
@@ -30,10 +30,17 @@ mechanical:
30
30
  - next-generation
31
31
  - enterprise-grade
32
32
  - blazing
33
+ - faster
34
+ - fastest
35
+ - simpler
36
+ - easier
37
+ - smarter
38
+ - trusted by
39
+ - loved by
33
40
  question:
34
41
  context: [headingAbove, docType]
35
42
  instructions: >-
36
- Read the TEXT. Return true when the TEXT describes a product, feature, service or company with an evaluative adjective or adverb (for example powerful, seamless, fast, intuitive, robust, best-in-class, simple) and the same TEXT gives the reader nothing they could check: no number with a unit or scope, no named mechanism that explains how, no named standard, customer or third party. Return false when the TEXT contains such a checkable fact, when the adjective names a category a reader can verify (open source, end-to-end encrypted, GDPR compliant), or when the TEXT is not describing a product at all (instructions, narrative, a lesson explaining the rule). Ignore code, URLs and the HEADING line; judge only the TEXT.
43
+ Read the TEXT. Return true when the TEXT describes a product, feature, service or company with an evaluative adjective or adverb (for example powerful, seamless, fast, intuitive, robust, best-in-class, simple) and the same TEXT gives the reader nothing they could check: no measurement with its conditions, no named mechanism that explains how, no named standard, customer or third party. A rounded boast is a claim, not evidence: a user count ("10,000+ teams"), an "up to" multiplier, or "trusted by" with no names. Return false when the TEXT contains such a checkable fact, when the adjective names a category a reader can verify (open source, end-to-end encrypted, GDPR compliant), or when the TEXT is not describing a product at all (instructions, narrative, a lesson explaining the rule). Ignore code, URLs and the HEADING line; judge only the TEXT.
37
44
  criteria:
38
45
  true:
39
46
  what: A quality is asserted about a product and nothing in the text lets the reader check it.
@@ -42,6 +49,8 @@ question:
42
49
  - "Enterprise-grade security, built for teams that take compliance seriously."
43
50
  - "Our blazing-fast platform simply supports parallel execution."
44
51
  - "Seamless sync across all your devices."
52
+ - "Loved by 50,000+ designers worldwide."
53
+ - "Up to 5x faster builds."
45
54
  false:
46
55
  what: The text names what happens, how much, or by which standard, or is not selling anything.
47
56
  examples:
@@ -8,7 +8,7 @@ source:
8
8
  path: skills/ghostwriter/references/tells.md,
9
9
  line: 13,
10
10
  }
11
- unit: [paragraph]
11
+ unit: [paragraph, jsx-text]
12
12
  mechanical:
13
13
  regex: "(?<![\"“‘'])\\b(?:serves|stands) as (?:a|an|the)\\b|(?<![\"“‘'])\\bboasts\\b"
14
14
  flags: giu
@@ -0,0 +1,28 @@
1
+ # Four of five models wrote href="#" on nav and CTA links (2026-09-23 generation
2
+ # test). Demo and registry files use it on purpose, so they are excluded.
3
+ id: copywriting-dead-link
4
+ title: Link with nowhere to go
5
+ categoryId: page-structure
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/ui-design/rules/nav-semantic-links.md
9
+ line: 10
10
+ ruleId: nav-semantic-links
11
+ scope:
12
+ include: ["**/*.tsx", "**/*.jsx"]
13
+ exclude: ["**/examples/**", "**/registry/**", "**/*demo*"]
14
+ unit: [source]
15
+ mechanical:
16
+ regex: href=["']#["']
17
+ flags: gu
18
+ severity: major
19
+ fix:
20
+ hint: Point the link at the route its label names. If there is none yet, it is not a link; cut it or make it a button that does the thing.
21
+ status: review-only
22
+ review:
23
+ applicability: "A link or CTA whose href is a bare #."
24
+ exceptions: "Placeholder links in a demo, story or registry example."
25
+ evidence: "The link label and where the product actually sends that action."
26
+ verification: "Confirm the label names a real destination, then point the link there or remove it."
27
+ sourceHash: 156607414ec8464ecc9e40d0441c9242ec94547f5de51ed83d04d6333c305470
28
+ handWritten: [mechanical]
@@ -0,0 +1,23 @@
1
+ # Fable and Grok put em dashes in interface copy when every other tell was gone
2
+ # (2026-09-23 generation test). Unspaced em dashes are accepted (Matthew,
3
+ # 2026-09-24), matching typography-audit; a full or no-break space either side
4
+ # is the tell. Hair and thin spaces are allowed (spacing-hair-thin-spaces).
5
+ id: copywriting-em-dash-in-ui
6
+ title: Spaced em dash in interface copy
7
+ categoryId: machine-prose
8
+ source:
9
+ {
10
+ repo: mblode/agent-skills,
11
+ path: skills/typography-audit/rules/punct-dashes.md,
12
+ line: 7,
13
+ }
14
+ unit: [jsx-text, attr-string]
15
+ mechanical:
16
+ regex: "[ \\u00a0]\\u2014|\\u2014[ \\u00a0]"
17
+ flags: gu
18
+ severity: minor
19
+ fix:
20
+ hint: Close up the em dash ("fast\u2014and safe"), or use a comma, full stop or parentheses.
21
+ preconditions: { notInCode: true, docType: [ui, unknown] }
22
+ status: review-only
23
+ handWritten: [mechanical, fix, preconditions, status]
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: excessive_hedging
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  phrases:
16
16
  - might
@@ -0,0 +1,26 @@
1
+ # Hand-ported: the upstream rg pattern, narrowed to JSX.
2
+ id: copywriting-external-arrow-internal-link
3
+ title: External-link arrow on a link that stays on the site
4
+ categoryId: page-structure
5
+ source:
6
+ repo: mblode/agent-skills
7
+ path: skills/ui-design/rules/slop-external-arrow-internal-link.md
8
+ line: 10
9
+ ruleId: slop-external-arrow-internal-link
10
+ scope:
11
+ include: ["**/*.tsx", "**/*.jsx"]
12
+ unit: [source]
13
+ mechanical:
14
+ regex: '<(?:(?:a|Link)\b(?![^>]*target="_blank")(?:=>|[^>])*\bhref="(?:#|/(?!/))[^"]*"|button\b)(?:=>|[^>])*>(?:(?!</(?:a|Link|button)>)[\s\S]){0,300}?(?:↗|&#8599;|&nearr;|ArrowUpRight|ExternalLink)'
15
+ flags: gu
16
+ severity: minor
17
+ fix:
18
+ hint: Remove the arrow. It promises a new site or tab; keep it for links that leave.
19
+ status: review-only
20
+ review:
21
+ applicability: "An up-right arrow sits on a link to a route or anchor on the same site."
22
+ exceptions: "A link that opens a new tab or another site, where the arrow is true."
23
+ evidence: "The href and target of the link carrying the arrow."
24
+ verification: "Confirm the link stays on the site, then remove the arrow."
25
+ sourceHash: 526719aa2eb755c1615c9100e8c9e433ddfd2965270148b94f1a66841f5f47d4
26
+ handWritten: [mechanical]
@@ -10,11 +10,14 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: fabricated_specificity
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
- context: [docType]
18
+ context: [docType, brief]
16
19
  instructions: >-
17
- Does the writing include suspiciously specific details without grounding or attribution? Ignore a number, name or date the passage attributes to a source, a test, or the writer\u0027s own measurement.
20
+ Does the writing include suspiciously specific details without grounding or attribution? Ignore a number, name or date the passage attributes to a source, a test, or the writer’s own measurement. When BRIEF is given, it is the product’s own source of truth: a detail BRIEF states is grounded, and a specific that BRIEF does not state is ungrounded however plainly it is written.
18
21
  criteria:
19
22
  true:
20
23
  what: Precise-sounding detail with no source in sight.
@@ -1,6 +1,7 @@
1
1
  # One of the 21 AI-writing checks Every ran with Jev (Mike Taylor, "Jev judged
2
- # everything I've written in 0.7 seconds", 15 Sep 2026). The question is the
3
- # published wording; the second sentence says what to ignore.
2
+ # everything I've written in 0.7 seconds", 15 Sep 2026). The question started as
3
+ # the published wording; it now also says what cannot be a violation, because
4
+ # Jev read plain labels, identifiers and instructions as violations (2026-09-23 eval).
4
5
  # Candidate list from EveryInc/compound-writing skills/cw-ai-check/SKILL.md, category 1 (generic frames and openers).
5
6
  id: copywriting-generic-framing
6
7
  title: Relies on generic framing instead of concrete context
@@ -10,22 +11,27 @@ source:
10
11
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
12
  line: 1
12
13
  ruleId: generic_framing
13
- unit: [paragraph]
14
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
15
+ # words is a label, button or greeting, not prose.
16
+ unit: [paragraph, jsx-text]
17
+ preconditions: { minWords: 5, notInCode: true }
14
18
  question:
15
19
  context: [docType]
16
20
  instructions: >-
17
- Does the writing rely on generic framing instead of concrete context? Ignore a first sentence that names a specific product, person, number, place or date.
21
+ Does the writing open or connect with a generic frame, a sentence that sets up the topic with claims that would fit any topic, instead of concrete context? Only a sentence that frames can be generic framing. Answer no for text that frames nothing: labels, names, headings, identifiers, URLs, placeholders, instructions to the reader, and plain statements of fact or opinion. Short or unspecific is not the same as generic. Ignore a first sentence that names a specific product, person, number, place or date.
18
22
  criteria:
19
23
  true:
20
- what: A frame that would fit any topic.
24
+ what: A setup sentence whose frame would fit any topic.
21
25
  examples:
22
26
  - "In today's fast-paced world, teams need tools that help them move quickly and stay aligned."
23
27
  - "When it comes to onboarding, first impressions matter more than ever."
24
28
  false:
25
- what: A frame anchored in the specific situation.
29
+ what: A frame anchored in the specific situation, or text that does no framing at all.
26
30
  examples:
27
31
  - "Our onboarding email had a 9 percent click rate in August; the one paragraph we changed took it to 21."
28
32
  - "Three of the five new hires this quarter could not find the deploy runbook in their first week."
33
+ - "Run commands from the repository root:"
34
+ - "Pricing"
29
35
  thresholds: { review: 0.4, act: 0.75 }
30
36
  severity: minor
31
37
  fix:
@@ -10,22 +10,30 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: generic_language
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
18
  context: [docType]
16
19
  instructions: >-
17
- Does the writing rely on generic or interchangeable language? Ignore a passage where the nouns and verbs name specific things, even if a few common words appear.
20
+ Could TEXT sit unchanged on a competitor's page and stay just as true? Return true when it could because its words are abstract (platform, solution, experience, empower, seamless, value, teamwork, flow) instead of naming what this product does. Return false when TEXT names a specific object, action, number, limit or place, however short, and for instructions, validation messages, greetings, sign-offs, labels, and titles or headings that say what a page or section contains. A slogan headline is judged like any sentence.
18
21
  criteria:
19
22
  true:
20
23
  what: Words that could describe any product or team.
21
24
  examples:
22
25
  - "Our robust, seamless platform empowers teams to unlock powerful insights and drive meaningful outcomes."
23
26
  - "This comprehensive approach leverages best practices to deliver value at scale."
27
+ - "Collaboration, Elevated for the Modern Team"
28
+ - "Less busywork. More momentum."
24
29
  false:
25
- what: Words that name the thing.
30
+ what: Words that name the thing, however plainly.
26
31
  examples:
27
32
  - "The sync engine merges edits made offline without overwriting anyone else, so field staff can work in basements and tunnels."
28
- - "The audit found 14 buttons labelled Submit; renaming them to the action cut support tickets about forms by a quarter."
33
+ - "Free for up to 3 projects. No card needed."
34
+ - "Use 60 characters or fewer."
35
+ - "Where your files live, and what happens when you go offline."
36
+ - "How Northwind merges two offline edits"
29
37
  thresholds: { review: 0.4, act: 0.75 }
30
38
  severity: minor
31
39
  fix:
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: generic_validation
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  phrases:
16
16
  - great question
@@ -42,11 +42,14 @@ mechanical:
42
42
  tapestry,
43
43
  testament to,
44
44
  it's important to remember,
45
+ here is some,
46
+ here's some,
47
+ here are some,
45
48
  ]
46
49
  question:
47
50
  context: []
48
51
  instructions: >-
49
- Read the TEXT. Return true when it reads as generated boilerplate: a chatbot opener ("I'd be happy to", "Let's dive in"), a vague attribution ("experts believe", "studies show") with no source, a stock flourish ("game-changer", "unlock the power of", "in today's fast-paced world", "a testament to"), or a symmetrical both-sides summary that adds no information. Return false when the TEXT makes a specific claim with a named source, gives an instruction, tells a concrete story, or quotes these phrases in order to warn against them.
52
+ Read the TEXT. Return true when it reads as generated boilerplate: a chatbot opener or preamble ("I'd be happy to", "Let's dive in", "Here is some copy for"), a vague attribution ("experts believe", "studies show") with no source, a stock flourish ("game-changer", "unlock the power of", "in today's fast-paced world", "a testament to"), a sentence that announces its own importance instead of making the point ("here is where it gets interesting", "the implications are significant"), a false contrast that denies a claim nobody made ("not just a tool, it is a way of working"), or a symmetrical both-sides summary that adds no information. Return false when the TEXT makes a specific claim with a named source, gives an instruction, tells a concrete story, or quotes these phrases in order to warn against them.
50
53
  criteria:
51
54
  true:
52
55
  what: Generated boilerplate.
@@ -8,7 +8,10 @@ source:
8
8
  repo: every.to
9
9
  path: https://every.to/also-true-for-humans/mini-vibe-check-typesafe-s-jev-judged-everything-i-ve-written-in-0-7-seconds
10
10
  line: 1
11
- unit: [paragraph]
11
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
12
+ # words is a label, button or greeting, not prose.
13
+ unit: [paragraph, jsx-text]
14
+ preconditions: { minWords: 5, notInCode: true }
12
15
  question:
13
16
  context: [docType]
14
17
  instructions: >-
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: meta_commentary
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  phrases:
16
16
  - in this section
@@ -8,7 +8,10 @@ source:
8
8
  repo: every.to
9
9
  path: https://every.to/also-true-for-humans/mini-vibe-check-typesafe-s-jev-judged-everything-i-ve-written-in-0-7-seconds
10
10
  line: 1
11
- unit: [paragraph]
11
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
12
+ # words is a label, button or greeting, not prose.
13
+ unit: [paragraph, jsx-text]
14
+ preconditions: { minWords: 5, notInCode: true }
12
15
  question:
13
16
  context: [docType]
14
17
  instructions: >-
@@ -9,7 +9,10 @@ source:
9
9
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
10
10
  line: 1
11
11
  ruleId: over_explanation
12
- unit: [paragraph]
12
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
13
+ # words is a label, button or greeting, not prose.
14
+ unit: [paragraph, jsx-text]
15
+ preconditions: { minWords: 5, notInCode: true }
13
16
  question:
14
17
  context: [docType]
15
18
  instructions: >-
@@ -8,7 +8,7 @@ source:
8
8
  path: skills/ghostwriter/references/tells.md,
9
9
  line: 16,
10
10
  }
11
- unit: [paragraph]
11
+ unit: [paragraph, jsx-text]
12
12
  mechanical:
13
13
  regex: "(?<![\"“‘']),\\s+(?:ensuring|highlighting|underscoring|showcasing|emphasi[sz]ing|reinforcing|cementing|solidifying|fostering|signall?ing)\\b"
14
14
  flags: giu
@@ -10,11 +10,14 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: prompt_restatement
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
18
  context: [docType]
16
19
  instructions: >-
17
- Does the writing read like it is restating an implied prompt or assignment? Ignore a heading or a stated question the passage then answers.
20
+ Does the writing read like it is restating an implied prompt or assignment? Ignore a heading or a stated question the passage then answers. Return false for a greeting, a sign-off, a button or link label, a bare list label, or a heading on its own: they are not prose.
18
21
  criteria:
19
22
  true:
20
23
  what: The passage announces the task it was given instead of doing it.
@@ -10,7 +10,10 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: punctuation_tics
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
18
  context: [docType]
16
19
  instructions: >-
@@ -27,17 +27,20 @@ mechanical:
27
27
  we made,
28
28
  the tool,
29
29
  a tool,
30
+ now with,
31
+ now available,
30
32
  ]
31
33
  question:
32
34
  context: [docType, headingAbove]
33
35
  instructions: >-
34
- The TEXT is a headline or hero line on a page selling or introducing a product. Return true when it opens by naming what the product is (a platform, a tool, a solution, "Introducing X") before saying why that matters to the reader. Return false when it leads with the reader's problem or outcome, when the DOC TYPE is documentation, reference or a lesson where naming the thing first is correct, or when it is not a headline.
36
+ The TEXT is a headline or hero line on a page selling or introducing a product. Return true when it opens by naming what the product is (a platform, a tool, a solution, "Introducing X") or what it now has ("Now with X") before saying why that matters to the reader. Return false when it leads with the reader's problem or outcome, when the DOC TYPE is documentation, reference or a lesson where naming the thing first is correct, or when it is not a headline.
35
37
  criteria:
36
38
  true:
37
39
  what: What-first headline.
38
40
  examples:
39
41
  - "Introducing Northwind, the all-in-one platform for modern teams"
40
42
  - "A collaboration tool for growing companies"
43
+ - "Now with dark mode"
41
44
  false:
42
45
  what: Why-first, or a context where What belongs first.
43
46
  examples:
@@ -10,7 +10,7 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: repetitive_transitions
13
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
14
14
  mechanical:
15
15
  phrases:
16
16
  - moreover
@@ -4,7 +4,7 @@ id: copywriting-semantic-links
4
4
  title: Use Semantic Links for Navigation
5
5
  categoryId: page-structure
6
6
  source:
7
- line: 9
7
+ line: 10
8
8
  path: skills/ui-design/rules/nav-semantic-links.md
9
9
  repo: mblode/agent-skills
10
10
  ruleId: nav-semantic-links
@@ -26,4 +26,4 @@ review:
26
26
  exceptions: A redundant card shortcut around an accessible real link can be intentional.
27
27
  evidence: The actual destination, nested links, handler behavior, and keyboard or browser navigation options.
28
28
  verification: Verify keyboard navigation, open-in-new-tab, and copy-link behavior while preserving legitimate larger click targets.
29
- sourceHash: 09e2de7a3e263b2dd145eb661ac460d9e49e3b360841bb3e8484f5f62d602347
29
+ sourceHash: 156607414ec8464ecc9e40d0441c9242ec94547f5de51ed83d04d6333c305470
@@ -0,0 +1,27 @@
1
+ # Hand-ported: the upstream rg pattern, narrowed to heading text. The
2
+ # "X, not Y" heading is copywriting-antithesis-contrast.
3
+ id: copywriting-slogan-headings
4
+ title: Headings built from stacked slogan fragments
5
+ categoryId: machine-prose
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/ui-design/rules/slop-slogan-headings.md
9
+ line: 10
10
+ ruleId: slop-slogan-headings
11
+ scope:
12
+ include: ["**/*.tsx", "**/*.jsx"]
13
+ unit: [source]
14
+ mechanical:
15
+ regex: '<h[1-3]\b[^>]*>(?:\s|<br\s*/>|<span[^>]*>|</span>)*[A-Z$\d][\w’''$,-]*(?:[ \t]+[\w’''$,-]+){0,3}[.!?](?:\s|<br\s*/>|<span[^>]*>|</span>)+[A-Z$\d][\w’''$,-]*(?:[ \t]+[\w’''$,-]+){0,3}[.!?]'
16
+ flags: gu
17
+ severity: minor
18
+ fix:
19
+ hint: Say the one thing the product does in a plain sentence a reader could check, instead of two or three clipped fragments.
20
+ status: review-only
21
+ review:
22
+ applicability: "A heading made of two or more clipped sentences of four words or fewer."
23
+ exceptions: "A real name or a quoted title, and a pair of short sentences that each state a checkable fact."
24
+ evidence: "Whether each fragment says something a reader could verify about the product."
25
+ verification: "Confirm the fragments carry no checkable claim, then rewrite as one plain sentence."
26
+ sourceHash: 253bbb9dd01304ad87bb0b924a6dd434780c829cec8a0fe91a4b149446fed284
27
+ handWritten: [mechanical]
@@ -10,7 +10,10 @@ source:
10
10
  path: https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)
11
11
  line: 1
12
12
  ruleId: structural_symmetry
13
- unit: [paragraph]
13
+ # Prose wherever it is: a Markdown paragraph or a JSX sentence. Under five
14
+ # words is a label, button or greeting, not prose.
15
+ unit: [paragraph, jsx-text]
16
+ preconditions: { minWords: 5, notInCode: true }
14
17
  question:
15
18
  context: [docType]
16
19
  instructions: >-
@@ -1,3 +1,5 @@
1
+ # Widened 2026-09-23 with the tee-ups Opus, Fable and Grok wrote in the
2
+ # generation test.
1
3
  # Hand-cut from mblode/agent-skills skills/ghostwriter/references/tells.md (structure tells).
2
4
  id: copywriting-tee-up
3
5
  title: Tees up a point instead of stating it
@@ -8,9 +10,9 @@ source:
8
10
  path: skills/ghostwriter/references/tells.md,
9
11
  line: 15,
10
12
  }
11
- unit: [paragraph]
13
+ unit: [paragraph, jsx-text]
12
14
  mechanical:
13
- regex: "(?<![\"“‘'])\\bhere['’]s the (?:thing|kicker|catch|deal|twist)\\b|(?<![\"“‘'])\\bthe (?:kicker|catch|twist)\\?|(?<![\"“‘'])\\bwhat struck me (?:was|is)\\b|(?<![\"“‘'])\\blet that sink in\\b"
15
+ regex: "(?<![\"“‘'])\\bhere['’]s the (?:thing|kicker|catch|deal|twist)\\b|(?<![\"“‘'])\\bthe (?:kicker|catch|twist)\\?|(?<![\"“‘'])\\bwhat struck me (?:was|is)\\b|(?<![\"“‘'])\\blet that sink in\\b|(?<![\"“‘'])\\bhere['’]s (?:a quick way to|the short version)\\b|(?<![\"“‘'])\\bthe part worth knowing\\b|(?<![\"“‘'])\\bthat['’]s the whole (?:pitch|idea|point|story)\\b"
14
16
  flags: giu
15
17
  severity: minor
16
18
  fix: