@holdyourvoice/hyv 3.1.0 → 3.2.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.
package/Readme.md CHANGED
@@ -8,7 +8,7 @@ Hold Your Voice is an MIT-licensed, local-first writing gate for people who want
8
8
  It checks a draft through two separate programs:
9
9
 
10
10
  - **VoiceDNA** compares the draft with 13 observable elements from your own local writing samples.
11
- - **AI Editor** flags a small, versioned set of editorial patterns that can make writing generic, formulaic, or inflated.
11
+ - **AI Editor** flags a reviewed, versioned set of editorial patterns that can make writing generic, formulaic, or inflated.
12
12
 
13
13
  Those programs keep separate findings, scores, and pass states. A strong result from one never cancels a failure in the other. The tool creates a tiered editing brief, then checks the candidate again before you accept it.
14
14
 
@@ -77,11 +77,81 @@ The result is JSON with independent reports:
77
77
  {
78
78
  "voiceDna": { "score": 93, "passed": true, "findings": [] },
79
79
  "aiEditor": { "score": 88, "passed": true, "findings": [] },
80
+ "hygiene": { "suspiciousCount": 0, "fixableCount": 0, "hits": [] },
80
81
  "passed": true
81
82
  }
82
83
  ```
83
84
 
84
- Read both reports. The outer `passed` field means each engine passed. Scores remain independent.
85
+ Read both scored reports and the separate hygiene inspection. The outer `passed` field means each engine passed. Unicode hygiene never changes either score or the release decision.
86
+
87
+ ### Gate every final text output
88
+
89
+ `final-check` is a profile-free last-mile boundary for text from any model, tool, agent, API, template, CLI, or GUI:
90
+
91
+ ```bash
92
+ producer | hyv final-check -
93
+ hyv final-check final-response.md
94
+ ```
95
+
96
+ Clean text is written to stdout byte-for-byte. A leading U+FEFF byte-order mark is removed before output. When other hidden Unicode remains, stdout stays empty, the report goes to stderr, and the command exits `2`. Put this command immediately before display, copy, export, posting, or an API response. The producer and the presence of a VoiceDNA profile do not change the policy.
97
+
98
+ This is an integration boundary, not a background interceptor. A GUI, agent host, or external tool must pass its exact final text to `hyv final-check -` or the read-only `hyv_final_check` MCP tool and deliver only accepted output. Run it after the last rewrite, formatter, template expansion, or manual edit; checking an earlier draft does not cover later changes.
99
+
100
+ ### Inspect and clean hidden Unicode
101
+
102
+ Use the profile-free hygiene command to inspect zero-width characters, bidirectional controls, Unicode tag characters, and unusual spaces:
103
+
104
+ ```bash
105
+ hyv hygiene draft.md
106
+ ```
107
+
108
+ Add `--fix` to create `draft.cleaned.md` while keeping `draft.md` unchanged:
109
+
110
+ ```bash
111
+ hyv hygiene draft.md --fix
112
+ hyv hygiene draft.md --fix --output=review-copy.md
113
+ ```
114
+
115
+ The fix receipt lists every changed UTF-16 offset and code point. The conservative cleaner removes only a leading U+FEFF byte-order mark. It reports other zero-width characters, unusual spaces, bidirectional controls, and tag characters without changing them because they can carry legitimate language, typography, or emoji behavior. Existing output files are never overwritten.
116
+
117
+ ### Add contextual editorial guidance
118
+
119
+ Use an optional local WritingBrief when the same writer needs different guidance for a social post, deck, outreach note, blog, audit, or website. A brief activates only the relevant advisory format checks and can block explicitly prohibited local terms. It never changes your VoiceDNA profile or the default two-engine analysis.
120
+
121
+ ```json
122
+ {
123
+ "version": "1",
124
+ "audience": "technical founders",
125
+ "intent": "start a useful discussion",
126
+ "format": "social",
127
+ "readerKnowsAuthor": false,
128
+ "vocabulary": ["deployment", "incident"],
129
+ "prohibitedTerms": ["internal contract value"],
130
+ "evidenceStatus": "attributed",
131
+ "argumentMap": {
132
+ "observation": "A worker fails during a live request.",
133
+ "mechanism": "Its in-memory state is lost.",
134
+ "consequence": "The request restarts instead of resuming.",
135
+ "readerValue": "Avoid the cost of a cold restart."
136
+ }
137
+ }
138
+ ```
139
+
140
+ ```bash
141
+ hyv analyze draft.md profile.json writing-brief.json
142
+ hyv rewrite-prompt draft.md profile.json writing-brief.json > rewrite-brief.md
143
+ hyv verify original.md candidate.md profile.json writing-brief.json
144
+ ```
145
+
146
+ Format checks, an `unverified` evidence state, and a missing configured reader-value cue are yellow review cues. `argumentMap` is a soft editorial contract surfaced to the rewrite prompt; it does not become a universal formula. Explicit `prohibitedTerms` are red release blockers. Keep client-specific briefs outside public repositories unless you have the right to publish them.
147
+
148
+ ### Inspect a batch
149
+
150
+ Use batch analysis to catch exact repeated opening or closing sentences across two or more drafts. It is advisory and keeps all drafts local.
151
+
152
+ ```bash
153
+ hyv batch-analyze posts/one.md posts/two.md posts/three.md
154
+ ```
85
155
 
86
156
  ### Create an editing brief
87
157
 
@@ -99,6 +169,34 @@ npx @holdyourvoice/hyv verify draft.md candidate.md profile.json
99
169
 
100
170
  `verify` returns the original and candidate reports, identifies newly introduced findings, calculates a coarse preservation score, and exits with status `2` when the candidate fails the dual gate. A passing verification automatically records only the resolved finding IDs for that profile in local learning state. It exits with `1` for a usage or runtime error. Treat status `2` as a release signal in scripts or CI.
101
171
 
172
+ ### Lock factual claims with a CopySpec
173
+
174
+ Use `verify-spec` when a draft has claims that must remain verbatim unless they declare atomic facts. A local CopySpec records each immutable claim alongside its evidence, then blocks a candidate if its required text or atoms are absent, or if it is joined by a prohibited claim.
175
+
176
+ ```json
177
+ {
178
+ "version": "1",
179
+ "audience": "operators",
180
+ "intent": "explain a launch date",
181
+ "channel": "email",
182
+ "claims": [
183
+ {
184
+ "id": "launch-date",
185
+ "text": "The launch is on 14 August.",
186
+ "atoms": ["The launch is on 14 August."],
187
+ "evidence": "Release calendar, checked 7 August."
188
+ }
189
+ ],
190
+ "prohibitedClaims": ["The launch is guaranteed to double revenue."]
191
+ }
192
+ ```
193
+
194
+ ```bash
195
+ hyv verify-spec original.md candidate.md profile.json copy-spec.json
196
+ ```
197
+
198
+ The check is deterministic. Without `atoms`, an immutable claim remains a verbatim sentence check. With `atoms`, every declared phrase must remain somewhere in the candidate, allowing independent facts to be split or reordered. Atoms are lexical-presence checks, not factual validation: put the whole relationship in one atom when it must stay true (for example, `Kimi K2.6 uses INT4 weights` rather than `INT4`). It covers declared claims and prohibited text; arbitrary unsupported assertions need a separate factual evaluator.
199
+
102
200
  ### Local voice memory
103
201
 
104
202
  Learning is on by default. After a successful `verify`, Hold Your Voice records resolved rule IDs under `~/.hyv/learning/`, scoped to a fingerprint of the portable profile. It stores no draft or candidate text. The next `rewrite-prompt` uses a bounded list of those verified repairs.
@@ -170,15 +268,19 @@ Read the full [VoiceDNA reference](docs/VOICE-DNA.md) and [Wiki guide](https://g
170
268
 
171
269
  ## AI Editor: inspectable rules
172
270
 
173
- AI Editor uses a local, deterministic ruleset. Each rule has a stable ID, severity, reason, and repair direction. Run this command to see the rules that actually execute in your checkout:
271
+ AI Editor uses a local, deterministic ruleset. The current `2.9.24-static.2` ruleset restores the reviewed 143-rule static catalog from the published `@holdyourvoice/hyv@2.9.24` `signals.ts` artifact and retains two detectors introduced in 3.1, for 145 rules total. Most rules inspect sentences; selected inherited rules inspect one physical line to preserve multi-sentence and line-start behavior. Each rule has a stable ID, severity, reason, repair direction, reconstructable expression, and explicit scope. Intentional inherited overlaps remain visible as separate findings.
272
+
273
+ Run this command to see the rules and ruleset version that actually execute in the published CLI:
174
274
 
175
275
  ```bash
176
276
  npx @holdyourvoice/hyv patterns
177
277
  ```
178
278
 
279
+ From a built source checkout, run `node dist/cli.js patterns` instead.
280
+
179
281
  Red findings are release blockers. Yellow findings are a request to inspect a sentence in context. A match never proves who wrote the text, and a clean scan never proves the text is good.
180
282
 
181
- The repository also includes a public [220-pattern editorial catalog](docs/patterns/AI-WRITING-PATTERNS-1-220.md). That catalog is broader than the executable ruleset on purpose. A catalog entry becomes executable only after the project has defined its counterexamples, reviewed public provenance, written tests, and decided the rule is narrow enough to help without creating noise.
283
+ The repository also includes a public [220-pattern editorial catalog](docs/patterns/AI-WRITING-PATTERNS-1-220.md). It is broader editorial guidance, not a claim that all 220 entries execute. A catalog entry becomes executable only after the project has defined its counterexamples, reviewed public provenance, written tests, and decided the rule is narrow enough to help without creating noise. Neither the executable rules nor the editorial catalog can prove authorship.
182
284
 
183
285
  ## Verification contract
184
286
 
@@ -197,8 +299,11 @@ The preservation score is a guardrail based on retained original words longer th
197
299
  | --- | --- | --- | --- |
198
300
  | `hyv profile <profile.json> <sample...>` | Two or more text files | Profile JSON | You need a new local reference. |
199
301
  | `hyv analyze <draft> <profile.json>` | Draft and profile | Analysis JSON | You need both reports before editing. |
302
+ | `hyv hygiene <draft> [--fix] [--output=path]` | Draft | Hygiene report or cleaned copy plus receipt | You need to inspect or conservatively clean hidden Unicode. |
303
+ | `hyv final-check <path\|->` | Any final text | Exact accepted text on stdout or a withheld-output report on stderr | Text is about to cross a user-facing boundary. |
200
304
  | `hyv rewrite-prompt <draft> <profile.json>` | Draft and profile | Markdown editing brief | You need a constrained request for an editor or model. |
201
305
  | `hyv verify <original> <candidate> <profile.json>` | Original, candidate, profile | Verification JSON and exit code | You need the candidate gate. |
306
+ | `hyv verify-spec <original> <candidate> <profile.json> <copy-spec.json>` | Original, candidate, profile, CopySpec | Verification JSON with hard claim gate | A brief contains locked facts or prohibited claims. |
202
307
  | `hyv learning <show\|add\|clear> <profile.json>` | Profile and optional instruction | Local learning JSON | You need to inspect or manage profile-scoped learning. |
203
308
  | `hyv patterns` | None | Ruleset JSON | You need the exact enabled rules. |
204
309
 
@@ -210,8 +315,10 @@ Every file argument can be `-` when the command accepts text input from standard
210
315
  | --- | --- |
211
316
  | `src/contracts.ts` | Profiles, findings, reports, analysis, and verification data shapes. |
212
317
  | `src/text.ts` | Sentence, paragraph, word, and basic statistics helpers. |
318
+ | `src/hygiene.ts` | Profile-free hidden Unicode inspection and conservative cleaning. |
213
319
  | `src/voice-dna.ts` | Builds profiles and runs VoiceDNA checks. |
214
320
  | `src/ai-editor.ts` | Owns the versioned deterministic editorial rules. |
321
+ | `src/editorial-packs.ts` | Parses WritingBrief context and runs format and batch checks. |
215
322
  | `src/learning.ts` | Stores text-free, profile-scoped verified repairs and composes bounded local preferences. |
216
323
  | `src/pipeline.ts` | Combines pass states, makes briefs, and verifies candidates. |
217
324
  | `src/cli.ts` | Local file and standard-input command adapter. |
@@ -0,0 +1,148 @@
1
+ /** Reviewed static sentence-compatible rules shipped in @holdyourvoice/hyv@2.9.24. */
2
+ export const rules = [
3
+ { id: "ai.delve", severity: "red", expression: /\bdelve\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use a specific verb: dig, explore, look at" },
4
+ { id: "ai.leverage", severity: "red", expression: /\bleverage\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: use, apply, build on" },
5
+ { id: "ai.tapestry", severity: "red", expression: /\btapestry\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "be specific about what you mean" },
6
+ { id: "ai.holistic", severity: "red", expression: /\bholistic\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the actual approach" },
7
+ { id: "ai.robust", severity: "yellow", expression: /\brobust\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say what actually makes it strong" },
8
+ { id: "ai.pivotal", severity: "yellow", expression: /\bpivotal\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say why it matters specifically" },
9
+ { id: "ai.foster", severity: "yellow", expression: /\bfoster\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: build, grow, encourage, support" },
10
+ { id: "ai.harness", severity: "yellow", expression: /\bharness\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: use, apply, work with" },
11
+ { id: "ai.illuminate", severity: "yellow", expression: /\billuminate\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: show, explain, highlight" },
12
+ { id: "ai.ever-evolving", severity: "red", expression: /\b(?:ever[\s-]evolving|ever[\s-]changing)\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "cut this \u2014 it says nothing" },
13
+ { id: "ai.fast-paced", severity: "red", expression: /\bfast[\s-]paced\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "cut this \u2014 every industry says this" },
14
+ { id: "ai.game-changer", severity: "red", expression: /\bgame[\s-]changer\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "explain what actually changed" },
15
+ { id: "ai.paradigm", severity: "red", expression: /\bparadigm\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the actual shift" },
16
+ { id: "ai.synergy", severity: "red", expression: /\bsynergy\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what works together and why" },
17
+ { id: "ai.ecosystem", severity: "yellow", expression: /\becosystem\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific tools/partners/platforms" },
18
+ { id: "ai.seamless", severity: "yellow", expression: /\bseamless\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe how it actually works" },
19
+ { id: "ai.actionable", severity: "yellow", expression: /\bactionable\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just give the action, don't label it" },
20
+ { id: "ai.granular", severity: "yellow", expression: /\bgranular\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say: specific, detailed, or name the level" },
21
+ { id: "ai.impactful", severity: "yellow", expression: /\bimpactful\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the actual impact" },
22
+ { id: "ai.transform", severity: "yellow", expression: /\btransform\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what specifically changed" },
23
+ { id: "ai.empower", severity: "yellow", expression: /\bempower\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what people can actually do" },
24
+ { id: "ai.unlock", severity: "yellow", expression: /\bunlock\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what they get access to" },
25
+ { id: "ai.nuanced", severity: "yellow", expression: /\bnuanced\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "explain the specific complexity" },
26
+ { id: "ai.multifaceted", severity: "red", expression: /\bmultifaceted\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific aspects" },
27
+ { id: "ai.comprehensive", severity: "yellow", expression: /\bcomprehensive\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say what it covers specifically" },
28
+ { id: "ai.innovative", severity: "yellow", expression: /\binnovative\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what's actually new" },
29
+ { id: "ai.cut[t]?ing-edge", severity: "red", expression: /\bcut(?:t)?ing[\s-]edge\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific technology" },
30
+ { id: "ai.state-of-the-art", severity: "red", expression: /\bstate[\s-]of[\s-]the[\s-]art\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what makes it current" },
31
+ { id: "ai.best-practices", severity: "yellow", expression: /\bbest[\s-]practices?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific practice" },
32
+ { id: "ai.world-class", severity: "red", expression: /\bworld[\s-]class\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "prove it with specifics" },
33
+ { id: "ai.second-to-none", severity: "red", expression: /\bsecond[\s-]to[\s-]none\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "drop the superlative, show the work" },
34
+ { id: "ai.utilize", severity: "red", expression: /\butilize[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'use "use" instead' },
35
+ { id: "ai.landscape", severity: "red", expression: /\blandscape\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific market/field/area" },
36
+ { id: "ai.realm", severity: "red", expression: /\brealm\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific domain" },
37
+ { id: "ai.straightforward", severity: "yellow", expression: /\bstraightforward\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just explain it directly" },
38
+ { id: "ai.seamlessly", severity: "yellow", expression: /\bseamlessly\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe how it actually works" },
39
+ { id: "ai.transformative", severity: "yellow", expression: /\btransformative\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what specifically changed" },
40
+ { id: "ai.meaningful", severity: "yellow", expression: /\bmeaningful\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "be specific about what makes it meaningful" },
41
+ { id: "ai.profound", severity: "yellow", expression: /\bprofound\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the actual depth/impact" },
42
+ { id: "ai.intricate", severity: "yellow", expression: /\bintricate\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say what specifically is complex" },
43
+ { id: "ai.cornerstone", severity: "red", expression: /\bcornerstone\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific foundation" },
44
+ { id: "ai.navigate", severity: "yellow", expression: /\bnavigate\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the specific action" },
45
+ { id: "ai.endeavor", severity: "yellow", expression: /\bendeavor\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: effort, project, attempt" },
46
+ { id: "ai.encapsulate", severity: "yellow", expression: /\bencapsulate[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: capture, contain, describe" },
47
+ { id: "ai.facilitate", severity: "yellow", expression: /\bfacilitate[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: help, enable, support" },
48
+ { id: "ai.bolster", severity: "yellow", expression: /\bbolster[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: strengthen, support, boost" },
49
+ { id: "ai.streamline", severity: "yellow", expression: /\bstreamline[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what specifically was simplified" },
50
+ { id: "ai.differentiate", severity: "yellow", expression: /\bdifferentiate[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what specifically is different" },
51
+ { id: "ai.myriad", severity: "yellow", expression: /\bmyriad\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific number or items" },
52
+ { id: "ai.elevate", severity: "yellow", expression: /\belevate[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what specifically improved" },
53
+ { id: "ai.revolutionize", severity: "red", expression: /\brevolutionize[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the specific change" },
54
+ { id: "ai.supercharge", severity: "red", expression: /\bsupercharge[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe what it actually does" },
55
+ { id: "ai.ensure", severity: "yellow", expression: /\bensure\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: make sure, confirm, verify" },
56
+ { id: "ai.crucial", severity: "yellow", expression: /\bcrucial\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "explain why it matters specifically" },
57
+ { id: "ai.vital", severity: "yellow", expression: /\bvital\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "describe the specific importance" },
58
+ { id: "ai.cultivate", severity: "yellow", expression: /\bcultivate[sd]?\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "use: build, grow, develop" },
59
+ { id: "ai.plethora", severity: "yellow", expression: /\bplethora\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "name the specific quantity or items" },
60
+ { id: "ai.embark", severity: "red", expression: /\bembark\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just start. no embarking needed." },
61
+ { id: "formula.firstly", severity: "yellow", expression: /\bfirstly\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'just start \u2014 "firstly" is filler' },
62
+ { id: "formula.secondly", severity: "yellow", expression: /\bsecondly\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'just continue \u2014 "secondly" is filler' },
63
+ { id: "formula.lastly", severity: "yellow", expression: /\blastly\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'just end \u2014 "lastly" is filler' },
64
+ { id: "formula.moreover", severity: "yellow", expression: /\bmoreover\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just add the point" },
65
+ { id: "formula.furthermore", severity: "yellow", expression: /\bfurthermore\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just add the point" },
66
+ { id: "formula.in-conclusion", severity: "red", expression: /\bin conclusion\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just end. readers know it's the end." },
67
+ { id: "formula.in-summary", severity: "red", expression: /\bin summary\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just summarize. the label is redundant." },
68
+ { id: "formula.it-is-important", severity: "yellow", expression: /\bit is important to note\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just note it. skip the preamble." },
69
+ { id: "formula.at-the-end", severity: "yellow", expression: /\bat the end of the day\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "cut this \u2014 it means nothing" },
70
+ { id: "formula.needless-to-say", severity: "yellow", expression: /\bneedless to say\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "if it's needless, don't say it" },
71
+ { id: "formula.it-goes-without", severity: "yellow", expression: /\bit goes without saying\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "then don't say it" },
72
+ { id: "formula.in-today", severity: "red", expression: /\bin today'?s\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "start with your actual point instead" },
73
+ { id: "formula.lets-dive", severity: "red", expression: /\blet'?s (?:dive|jump|dig|delve)\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just start. no diving needed." },
74
+ { id: "formula.without-further", severity: "red", expression: /\bwithout further ado\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "cut this \u2014 just get to it" },
75
+ { id: "formula.its-worth-noting", severity: "yellow", expression: /\bit'?s worth noting\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just note it directly" },
76
+ { id: "formula.moving-forward", severity: "yellow", expression: /\bmoving forward\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just say what happens next" },
77
+ { id: "formula.to-put-in-perspective", severity: "yellow", expression: /\bto put this in perspective\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just give the perspective directly" },
78
+ { id: "formula.what-makes-interesting", severity: "yellow", expression: /\bwhat makes this particularly interesting\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just say the interesting thing" },
79
+ { id: "formula.implications", severity: "yellow", expression: /\bthe implications here are\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "state the implications directly" },
80
+ { id: "formula.in-other-words", severity: "red", expression: /\bin other words\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "say it once, well" },
81
+ { id: "formula.here-the-thing", severity: "yellow", expression: /\bhere'?s the thing\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just say the thing" },
82
+ { id: "formula.lets-be-clear", severity: "yellow", expression: /\blet'?s be clear\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just be clear. the announcement is filler." },
83
+ { id: "formula.the-bottom-line", severity: "yellow", expression: /\bthe bottom line\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just state the conclusion" },
84
+ { id: "formula.picture-this", severity: "yellow", expression: /\bpicture this\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just describe the scene" },
85
+ { id: "formula.think-of-it", severity: "yellow", expression: /\bthink of it this way\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just explain it directly" },
86
+ { id: "formula.when-it-comes-to", severity: "yellow", expression: /\bwhen it comes to\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just address the topic directly" },
87
+ { id: "formula.the-reality-is", severity: "yellow", expression: /\bthe reality is\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "just state the reality" },
88
+ { id: "hedge.some-might", severity: "yellow", expression: /\bsome might say\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "commit to the claim or drop it" },
89
+ { id: "hedge.arguably", severity: "yellow", expression: /\barguably\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "commit. say it or don't." },
90
+ { id: "hedge.worth-noting", severity: "yellow", expression: /\bit'?s worth noting\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "just note it directly" },
91
+ { id: "hedge.to-some-extent", severity: "yellow", expression: /\bto some extent\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "be specific about the extent" },
92
+ { id: "hedge.perhaps", severity: "yellow", expression: /\bperhaps\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "commit or cut" },
93
+ { id: "hedge.maybe", severity: "yellow", expression: /\bmaybe\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "commit or cut" },
94
+ { id: "hedge.possibly", severity: "yellow", expression: /\bpossibly\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "commit or cut" },
95
+ { id: "hedge.it-seems", severity: "yellow", expression: /\bit seems\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "state it directly" },
96
+ { id: "hedge.i-think", severity: "yellow", expression: /\bi think\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: 'just say it. the "i think" is implied.' },
97
+ { id: "hedge.in-my-opinion", severity: "yellow", expression: /\bin my opinion\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "just state it. it's your article." },
98
+ { id: "struct.antithesis", severity: "yellow", expression: /\bnot (?:just|only) .{3,50}, but .{3,50}/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "this antithesis pattern is an AI tell \u2014 restructure" },
99
+ { id: "struct.not-just-but-also", severity: "yellow", expression: /\bnot\s+just\b.{3,80}\bbut\s+(?:also\s+)?/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "say what it IS, not what it isn't" },
100
+ { id: "struct.more-than-just", severity: "yellow", expression: /\bmore\s+than\s+just\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "say what it IS, not what it isn't" },
101
+ { id: "struct.in-order-to", severity: "yellow", expression: /\bin order to\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: 'just use "to"' },
102
+ { id: "struct.due-to-the-fact", severity: "yellow", expression: /\bdue to the fact that\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: 'use "because"' },
103
+ { id: "struct.for-the-purpose", severity: "yellow", expression: /\bfor the purpose of\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: 'use "to"' },
104
+ { id: "struct.which-is-another", severity: "red", expression: /\bwhich is another way of saying\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just say the thing directly" },
105
+ { id: "struct.in-other-words", severity: "red", expression: /\bin other words\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just say it once, well" },
106
+ { id: "struct.rhetorical-truth", severity: "yellow", expression: /\b(?:the\s+)?(?:uncomfortable|hard|harsh|brutal|ugly|unsexy|real|honest)\s+(?:truth|reality)\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "state the fact directly, skip the framing" },
107
+ { id: "struct.truth-is", severity: "yellow", expression: /\bthe\s+truth\s+is\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just say the truth. the framing is filler." },
108
+ { id: "struct.lesson-setup", severity: "yellow", expression: /\b(?:here'?s\s+)?what\s+.{3,80}\s+(?:taught|teaches)\s+(?:me|us|you|everyone)\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just share the lesson" },
109
+ { id: "struct.negation-cascade", severity: "yellow", expression: /\b(?:no|not)\s+\w[^.!?\n]{0,80}[.!?][ \t]*\n?[ \t]*(?:no|not)\s+\w[^.!?\n]{0,80}[.!?][ \t]*\n?[ \t]*(?:no|not)\s+\w/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "three negations in a row reads like AI performance", scope: "line" },
110
+ { id: "struct.inline-triplet", severity: "yellow", expression: /\bit'?s\s+\w[^,.!?;\n]{0,30},\s*it'?s\s+\w[^,.!?;\n]{0,30}[,.]?\s*(?:and\s+)?it'?s\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: `the "it's X, it's Y, it's Z" pattern is an AI tell` },
111
+ { id: "struct.moment-becomes", severity: "red", expression: /\b(?:the\s+)?moment\b.{3,80}\bbecomes?\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: '"the moment X becomes Y" is polished founder cadence \u2014 restructure' },
112
+ { id: "struct.same-better", severity: "red", expression: /\bsame\s+[^.!?\n]{1,35}[.!?]\s*(?:better|nicer|cleaner|calmer|safer)\s+[^.!?\n]{1,35}[.!?]?/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: '"same X. better Y." is screenshot-ready founder cadence', scope: "line" },
113
+ { id: "struct.this-is-why", severity: "yellow", expression: /\bthis\s+is\s+(?:also\s+)?(?:why|how|where|what\b)/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "signpost claims are AI tells \u2014 just make the point" },
114
+ { id: "struct.heres-where", severity: "yellow", expression: /\b(?:here'?s|here\s+is)\s+(?:where|why|what|the\s+part|the\s+(?:harder|real|actual|main|bigger)\s+problem)\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "just make the point without the signpost" },
115
+ { id: "struct.generic-buyer", severity: "red", expression: /\bpeople\s+don'?t\s+just\s+buy\b|\bpeople\s+buy\s+the\s+feeling\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "generic buyer psychology is AI filler" },
116
+ { id: "struct.this-isnt-x-this-is-y", severity: "red", expression: /\bthis isn'?t .{2,40}\.?\s*(?:this is|it'?s) .{2,40}/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "FATAL: delete the negation, just state the positive claim", scope: "line" },
117
+ { id: "struct.not-x-y", severity: "red", expression: /\bnot .{2,30}\.?\s*.{2,30}\b/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: 'the "Not X. Y." pattern is an AI tell \u2014 just state Y', scope: "line" },
118
+ { id: "struct.forget-x", severity: "red", expression: /\bforget .{2,40}\.?\s*(?:this is|it'?s|you need)/i, reason: "A formulaic structure can make the sentence feel manufactured.", suggestion: "don't negate \u2014 just state what you mean", scope: "line" },
119
+ { id: "punct.em-dash", severity: "yellow", expression: /—/, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "em dashes are an AI tell \u2014 use a period, comma, or parentheses" },
120
+ { id: "bait.let-that-sink", severity: "red", expression: /\blet that sink in\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "cut the sink. make your point and move on." },
121
+ { id: "bait.read-that-again", severity: "red", expression: /\bread that again\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "if it needs repeating, repeat it yourself" },
122
+ { id: "bait.full-stop", severity: "red", expression: /\bfull stop\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "the period already does this job" },
123
+ { id: "bait.this-changes-everything", severity: "red", expression: /\bthis changes everything\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "prove it with specifics" },
124
+ { id: "bait.paying-attention", severity: "red", expression: /\bare you paying attention\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "don't patronize the reader" },
125
+ { id: "bait.not-ready", severity: "red", expression: /\byou'?re not ready for this\b/i, reason: "The phrase asks for attention instead of earning it.", suggestion: "just deliver the content" },
126
+ { id: "cringe.future-proof", severity: "red", expression: /\bfuture[\s-]proof\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "explain what specifically makes it durable" },
127
+ { id: "cringe.10x", severity: "red", expression: /\b10x\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "use the actual numbers" },
128
+ { id: "cringe.ai-revolution", severity: "red", expression: /\bthe ai revolution\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "describe the specific change" },
129
+ { id: "cringe.age-of-ai", severity: "red", expression: /\bin the age of ai\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "just talk about what's happening now" },
130
+ { id: "cringe.happy-to-help", severity: "red", expression: /\bi'?d be happy to help\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "just help. don't announce it." },
131
+ { id: "cringe.of-course", severity: "yellow", expression: /^of course!?\s/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "just answer. skip the enthusiasm.", scope: "line" },
132
+ { id: "cringe.great-question", severity: "red", expression: /\bgreat question!?\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "just answer the question" },
133
+ { id: "insider.nobody-talking", severity: "red", expression: /\bhere'?s the part nobody'?s? talking about\b/i, reason: "The framing claims privileged insight instead of stating the point.", suggestion: "just say the thing. the framing is noise." },
134
+ { id: "insider.nobody-tells", severity: "red", expression: /\bwhat nobody tells you\b/i, reason: "The framing claims privileged insight instead of stating the point.", suggestion: "just tell them." },
135
+ { id: "insider.most-people", severity: "yellow", expression: /\bmost people don'?t realize\b/i, reason: "The framing claims privileged insight instead of stating the point.", suggestion: "just explain it. skip the setup." },
136
+ { id: "insider.nobody-realizes", severity: "red", expression: /\bnobody (?:realizes|talks about|mentions)\b/i, reason: "The framing claims privileged insight instead of stating the point.", suggestion: "if nobody talks about it, just talk about it" },
137
+ { id: "ogilvy.bandwidth", severity: "yellow", expression: /\bbandwidth\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'Ogilvy: say "time" or "capacity"' },
138
+ { id: "ogilvy.circle-back", severity: "red", expression: /\bcircle back\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'Ogilvy: say "follow up" or "talk later"' },
139
+ { id: "ogilvy.low-hanging", severity: "red", expression: /\blow-hanging fruit\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "Ogilvy: name the specific easy win" },
140
+ { id: "ogilvy.move-the-needle", severity: "red", expression: /\bmove the needle\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "Ogilvy: describe the actual impact" },
141
+ { id: "ogilvy.touch-base", severity: "red", expression: /\btouch base\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'Ogilvy: say "talk" or "meet"' },
142
+ { id: "ogilvy.take-it-offline", severity: "red", expression: /\btake it offline\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'Ogilvy: say "discuss later"' },
143
+ { id: "ogilvy.deep-dive", severity: "yellow", expression: /\bdeep dive\b/i, reason: "A stock phrase can flatten the writer's meaning.", suggestion: 'Ogilvy: say "look closely at" or "examine"' },
144
+ { id: "ogilvy.preamble-i-want-to", severity: "yellow", expression: /^\s*i want to (?:share|talk about|discuss|mention)\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "Ogilvy: just say it. skip the preamble.", scope: "line" },
145
+ { id: "ogilvy.preamble-just-wanted", severity: "yellow", expression: /^\s*(?:i just wanted|i wanted to)\b/i, reason: "A hedge or preamble weakens the direct claim.", suggestion: "Ogilvy: just say it.", scope: "line" },
146
+ { id: "ai.question-hook", severity: "yellow", expression: /^(?:have you|do you|what if|why do|how do)\b/i, reason: "A question opener delays the concrete observation.", suggestion: "Open from an observation." },
147
+ { id: "ai.abstract-cluster", severity: "yellow", expression: /\b(?:alignment|authenticity|clarity|strategy|value)\b.*\b(?:alignment|authenticity|clarity|strategy|value)\b/i, reason: "Abstract nouns pile up without a mechanism.", suggestion: "Use concrete nouns and actions." },
148
+ ];
package/dist/ai-editor.js CHANGED
@@ -1,9 +1,62 @@
1
+ import { rules } from './ai-editor-rules.js';
1
2
  import { sentences } from './text.js';
2
- export const RULESET_VERSION = '2026.08.04.1';
3
- export const rules = [
4
- ['ai.delve', 'red', /\bdelve\b/i, 'Generic AI verb.', 'Use a concrete verb.'], ['ai.leverage', 'red', /\bleverage\b/i, 'Business-jargon default.', 'Say what is being used.'], ['ai.tapestry', 'red', /\btapestry\b/i, 'Abstract metaphor without content.', 'Name the thing.'], ['ai.holistic', 'red', /\bholistic\b/i, 'Vague qualifier.', 'Describe the actual approach.'], ['ai.robust', 'yellow', /\brobust\b/i, 'Claims strength without evidence.', 'State what makes it strong.'], ['ai.landscape', 'yellow', /\blandscape\b/i, 'Generic context word.', 'Name the field.'], ['ai.game-changer', 'red', /\bgame[ -]changer\b/i, 'Promotional cliche.', 'Explain what changed.'], ['ai.formulaic-connector', 'yellow', /\b(firstly|secondly|moreover|furthermore|in conclusion)\b/i, 'Formulaic connector.', 'State the next point directly.'], ['ai.hedging', 'yellow', /\b(arguably|perhaps|it is worth noting|some might say)\b/i, 'Hedge hides the claim.', 'Make or remove the claim.'], ['ai.signpost', 'yellow', /\b(this is why|this is how|here.?s why)\b/i, 'Meta-signpost.', 'Lead with the observation.'], ['ai.not-just', 'red', /\bnot just\b.{0,80}\bbut\b/i, 'Binary persuasion template.', 'State the point plainly.'], ['ai.truth-setup', 'yellow', /\b(the hard|the brutal|the uncomfortable) truth\b/i, 'Manufactured revelation.', 'Name the fact.'], ['ai.em-dash', 'yellow', /—/, 'Dramatic em-dash use.', 'Use a sentence break when needed.'], ['ai.question-hook', 'yellow', /^(have you|do you|what if|why do|how do)\b/i, 'Question opener instead of a concrete start.', 'Open from an observation.'], ['ai.abstract-cluster', 'yellow', /\b(alignment|authenticity|clarity|strategy|value)\b.*\b(alignment|authenticity|clarity|strategy|value)\b/i, 'Abstract nouns pile up.', 'Use concrete nouns and actions.']
5
- ].map(([id, severity, expression, reason, suggestion]) => ({ id: id, severity: severity, expression: expression, reason: reason, suggestion: suggestion }));
6
- export function analyzeAiEditor(text) { const findings = []; for (const s of sentences(text))
7
- for (const r of rules)
8
- if (r.expression.test(s.text))
9
- findings.push({ engine: 'ai_editor', id: r.id, severity: r.severity, sentence: s.index, excerpt: s.text, reason: r.reason, suggestion: r.suggestion }); const reds = findings.filter(f => f.severity === 'red').length, score = Math.max(0, 100 - reds * 18 - (findings.length - reds) * 6); return { engine: 'ai_editor', version: RULESET_VERSION, score, passed: reds === 0, findings }; }
3
+ export { rules } from './ai-editor-rules.js';
4
+ export const RULESET_VERSION = '2.9.24-static.2';
5
+ const sentenceRules = rules.filter((rule) => rule.scope !== 'line');
6
+ const lineRules = rules.filter((rule) => rule.scope === 'line');
7
+ const ruleOrder = new Map(rules.map((rule, index) => [rule.id, index]));
8
+ export function serializedRules() {
9
+ return rules.map((rule) => ({
10
+ id: rule.id,
11
+ severity: rule.severity,
12
+ reason: rule.reason,
13
+ suggestion: rule.suggestion,
14
+ expression: { source: rule.expression.source, flags: rule.expression.flags },
15
+ scope: rule.scope ?? 'sentence',
16
+ }));
17
+ }
18
+ export function analyzeAiEditor(text) {
19
+ const findings = [];
20
+ const mapped = sentences(text);
21
+ for (const sentence of mapped) {
22
+ for (const rule of sentenceRules) {
23
+ if (rule.expression.test(sentence.text)) {
24
+ findings.push({
25
+ engine: 'ai_editor',
26
+ id: rule.id,
27
+ severity: rule.severity,
28
+ sentence: sentence.index,
29
+ excerpt: sentence.text,
30
+ reason: rule.reason,
31
+ suggestion: rule.suggestion,
32
+ });
33
+ }
34
+ }
35
+ }
36
+ let lineStart = 0;
37
+ for (const line of text.split('\n')) {
38
+ for (const rule of lineRules) {
39
+ const result = rule.expression.exec(line);
40
+ if (!result)
41
+ continue;
42
+ const matchStart = lineStart + result.index;
43
+ const sentence = mapped.find((candidate) => candidate.start <= matchStart && matchStart < candidate.end);
44
+ if (!sentence)
45
+ continue;
46
+ findings.push({
47
+ engine: 'ai_editor',
48
+ id: rule.id,
49
+ severity: rule.severity,
50
+ sentence: sentence.index,
51
+ excerpt: sentence.text,
52
+ reason: rule.reason,
53
+ suggestion: rule.suggestion,
54
+ });
55
+ }
56
+ lineStart += line.length + 1;
57
+ }
58
+ findings.sort((left, right) => left.sentence - right.sentence || (ruleOrder.get(left.id) ?? 0) - (ruleOrder.get(right.id) ?? 0));
59
+ const reds = findings.reduce((count, finding) => count + Number(finding.severity === 'red'), 0);
60
+ const score = Math.max(0, 100 - reds * 18 - (findings.length - reds) * 6);
61
+ return { engine: 'ai_editor', version: RULESET_VERSION, score, passed: reds === 0, findings };
62
+ }
@@ -1,15 +1,18 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { analyzeAiEditor, rules } from './ai-editor.js';
3
+ import { analyzeAiEditor, rules, serializedRules } from './ai-editor.js';
4
4
  test('publishes executable rules with stable IDs and repair directions', () => {
5
- assert.ok(rules.length > 0);
5
+ assert.equal(rules.length, 145);
6
+ assert.equal(new Set(rules.map((rule) => rule.id)).size, rules.length);
6
7
  for (const rule of rules) {
7
- assert.match(rule.id, /^ai\./);
8
+ assert.match(rule.id, /^(ai|formula|hedge|struct|punct|bait|cringe|insider|ogilvy)\./);
8
9
  assert.ok(rule.reason.length > 0);
9
10
  assert.ok(rule.suggestion.length > 0);
11
+ assert.equal(rule.expression.global, false, rule.id);
12
+ assert.equal(rule.expression.sticky, false, rule.id);
10
13
  }
11
14
  });
12
- test('detects every executable rule against its exact sentence', () => {
15
+ test('detects representative rules from every inherited rule family', () => {
13
16
  const examples = [
14
17
  ['ai.delve', 'we will delve into it.'],
15
18
  ['ai.leverage', 'we leverage the existing logs.'],
@@ -18,21 +21,23 @@ test('detects every executable rule against its exact sentence', () => {
18
21
  ['ai.robust', 'robust evidence supports the claim.'],
19
22
  ['ai.landscape', 'the market landscape changed.'],
20
23
  ['ai.game-changer', 'this is a game-changer.'],
21
- ['ai.formulaic-connector', 'Firstly, check the invoice.'],
22
- ['ai.hedging', 'Perhaps the invoice is late.'],
23
- ['ai.signpost', 'This is why the invoice matters.'],
24
- ['ai.not-just', 'this is not just fast but reliable.'],
25
- ['ai.truth-setup', 'the hard truth is in the logs.'],
26
- ['ai.em-dash', 'the logs failed — retry later.'],
27
- ['ai.question-hook', 'Have you checked the logs?'],
28
- ['ai.abstract-cluster', 'alignment and clarity are missing.'],
24
+ ['formula.firstly', 'Firstly, check the invoice.'],
25
+ ['hedge.perhaps', 'Perhaps the invoice is late.'],
26
+ ['struct.this-is-why', 'This is why the invoice matters.'],
27
+ ['struct.not-just-but-also', 'this is not just fast but reliable.'],
28
+ ['struct.rhetorical-truth', 'the hard truth is in the logs.'],
29
+ ['punct.em-dash', 'the logs failed — retry later.'],
30
+ ['bait.let-that-sink', 'Let that sink in.'],
31
+ ['cringe.10x', 'The change delivered a 10x result.'],
32
+ ['insider.nobody-tells', 'What nobody tells you is in the report.'],
33
+ ['ogilvy.bandwidth', 'We lack the bandwidth this week.'],
29
34
  ];
30
35
  for (const [id, example] of examples) {
31
36
  const report = analyzeAiEditor(example);
32
- assert.deepEqual(report.findings.map((finding) => [finding.id, finding.sentence]), [[id, 1]], id);
37
+ assert.ok(report.findings.some((finding) => finding.id === id && finding.sentence === 1), id);
33
38
  }
34
39
  });
35
- test('keeps a counterexample for every executable rule', () => {
40
+ test('keeps counterexamples for representative inherited rules', () => {
36
41
  const counterexamples = [
37
42
  ['ai.delve', 'we inspect the logs.'],
38
43
  ['ai.leverage', 'we use the existing logs.'],
@@ -41,14 +46,16 @@ test('keeps a counterexample for every executable rule', () => {
41
46
  ['ai.robust', 'the evidence includes three dated reports.'],
42
47
  ['ai.landscape', 'the market changed after the price cut.'],
43
48
  ['ai.game-changer', 'the release removed a manual step.'],
44
- ['ai.formulaic-connector', 'next, check the invoice.'],
45
- ['ai.hedging', 'the invoice is late.'],
46
- ['ai.signpost', 'the invoice matters because it is overdue.'],
47
- ['ai.not-just', 'the service is fast and reliable.'],
48
- ['ai.truth-setup', 'the logs show the service failed.'],
49
- ['ai.em-dash', 'the logs failed; retry later.'],
50
- ['ai.question-hook', 'the reviewer asked, have you checked the logs?'],
51
- ['ai.abstract-cluster', 'the editor checked the contract and sent the invoice.'],
49
+ ['formula.firstly', 'next, check the invoice.'],
50
+ ['hedge.perhaps', 'the invoice is late.'],
51
+ ['struct.this-is-why', 'the invoice matters because it is overdue.'],
52
+ ['struct.not-just-but-also', 'the service is fast and reliable.'],
53
+ ['struct.rhetorical-truth', 'the logs show the service failed.'],
54
+ ['punct.em-dash', 'the logs failed; retry later.'],
55
+ ['bait.let-that-sink', 'The invoice is overdue.'],
56
+ ['cringe.10x', 'The result rose from 2 to 20.'],
57
+ ['insider.nobody-tells', 'The report explains the missing step.'],
58
+ ['ogilvy.bandwidth', 'We lack time this week.'],
52
59
  ];
53
60
  for (const [id, example] of counterexamples) {
54
61
  assert.equal(analyzeAiEditor(example).findings.some((finding) => finding.id === id), false, id);
@@ -59,3 +66,57 @@ test('keeps yellow findings as review cues rather than release blockers', () =>
59
66
  assert.ok(report.findings.every((finding) => finding.severity === 'yellow'));
60
67
  assert.equal(report.passed, true);
61
68
  });
69
+ test('restores the benchmark signals from the 2.9.24 executable catalog', () => {
70
+ const report = analyzeAiEditor("This work is meaningful. Here's the part nobody is talking about. The change delivered a 10x result.");
71
+ assert.deepEqual(report.findings.map((finding) => [finding.id, finding.sentence]), [
72
+ ['ai.meaningful', 1],
73
+ ['struct.heres-where', 2],
74
+ ['cringe.10x', 3],
75
+ ]);
76
+ });
77
+ test('returns the same findings across repeated sentence and line analysis', () => {
78
+ for (const text of ['We leverage logs. We leverage traces.', 'No demos. No decks. No distractions. Same team. Better results.']) {
79
+ assert.deepEqual(analyzeAiEditor(text), analyzeAiEditor(text));
80
+ }
81
+ });
82
+ test('reports the same rule in multiple sentences', () => {
83
+ const report = analyzeAiEditor('We leverage logs. We leverage traces.');
84
+ assert.deepEqual(report.findings.filter((finding) => finding.id === 'ai.leverage').map((finding) => finding.sentence), [1, 2]);
85
+ });
86
+ test('executes inherited cross-sentence rules and maps them to the first sentence', () => {
87
+ const report = analyzeAiEditor('No demos. No decks. No distractions. Same team. Better results.');
88
+ assert.deepEqual(report.findings
89
+ .filter((finding) => finding.id === 'struct.negation-cascade' || finding.id === 'struct.same-better')
90
+ .map((finding) => [finding.id, finding.sentence]), [
91
+ ['struct.negation-cascade', 1],
92
+ ['struct.same-better', 4],
93
+ ]);
94
+ });
95
+ test('preserves inherited physical-line matching and line-start anchors', () => {
96
+ const sameLine = analyzeAiEditor("This isn't positioning. This is proof. Forget vanity metrics. You need retention.");
97
+ assert.ok(sameLine.findings.some((finding) => finding.id === 'struct.this-isnt-x-this-is-y'));
98
+ assert.ok(sameLine.findings.some((finding) => finding.id === 'struct.forget-x'));
99
+ assert.equal(analyzeAiEditor("This isn't positioning.\nThis is proof.").findings.some((finding) => finding.id === 'struct.this-isnt-x-this-is-y'), false);
100
+ assert.equal(analyzeAiEditor('The logs failed. Of course we can retry.').findings.some((finding) => finding.id === 'cringe.of-course'), false);
101
+ assert.ok(analyzeAiEditor('The logs failed.\nOf course we can retry.').findings.some((finding) => finding.id === 'cringe.of-course'));
102
+ });
103
+ test('retains the current question-hook and abstract-cluster detectors', () => {
104
+ assert.ok(analyzeAiEditor('Have you checked the invoice?').findings.some((finding) => finding.id === 'ai.question-hook'));
105
+ assert.ok(analyzeAiEditor('Clarity and strategy are missing.').findings.some((finding) => finding.id === 'ai.abstract-cluster'));
106
+ });
107
+ test('serializes reconstructable regular expressions and explicit scopes', () => {
108
+ const catalog = serializedRules();
109
+ assert.equal(catalog.length, 145);
110
+ assert.ok(catalog.every((rule) => rule.scope === 'sentence' || rule.scope === 'line'));
111
+ const meaningful = catalog.find((rule) => rule.id === 'ai.meaningful');
112
+ assert.ok(meaningful);
113
+ assert.equal(new RegExp(meaningful.expression.source, meaningful.expression.flags).test('Meaningful work.'), true);
114
+ });
115
+ test('keeps intentional inherited overlaps visible and scores each finding', () => {
116
+ const report = analyzeAiEditor('In other words, use logs.');
117
+ assert.deepEqual(report.findings.map((finding) => finding.id), ['formula.in-other-words', 'struct.in-other-words']);
118
+ assert.equal(report.score, 64);
119
+ });
120
+ test('returns zero AI findings for clean input', () => {
121
+ assert.deepEqual(analyzeAiEditor('The launch starts Tuesday. The owner signed the release checklist.').findings, []);
122
+ });