@holdyourvoice/hyv 3.1.1 → 3.3.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 (53) hide show
  1. package/Readme.md +76 -17
  2. package/dist/ai-editor-rules.js +151 -0
  3. package/dist/ai-editor.js +104 -8
  4. package/dist/ai-editor.test.js +135 -22
  5. package/dist/approval-capability.js +111 -0
  6. package/dist/approval-capability.test.js +52 -0
  7. package/dist/approval-context.js +54 -0
  8. package/dist/approval-context.test.js +38 -0
  9. package/dist/benchmark.js +232 -0
  10. package/dist/benchmark.test.js +328 -0
  11. package/dist/canonical-json.js +123 -0
  12. package/dist/canonical-json.test.js +24 -0
  13. package/dist/cli.js +359 -21
  14. package/dist/cli.test.js +275 -7
  15. package/dist/copy-spec.js +35 -8
  16. package/dist/editorial-packs.js +25 -1
  17. package/dist/editorial-packs.test.js +45 -0
  18. package/dist/hygiene.js +91 -0
  19. package/dist/hygiene.test.js +73 -0
  20. package/dist/judgment-task.js +171 -0
  21. package/dist/judgment-task.test.js +162 -0
  22. package/dist/learning.js +240 -100
  23. package/dist/learning.test.js +203 -3
  24. package/dist/lifecycle-adapter.js +75 -0
  25. package/dist/lifecycle-adapter.test.js +56 -0
  26. package/dist/mcp-tools.js +110 -9
  27. package/dist/mcp-tools.test.js +188 -10
  28. package/dist/mcp.js +228 -9
  29. package/dist/mcp.test.js +248 -12
  30. package/dist/pipeline.js +81 -15
  31. package/dist/pipeline.test.js +94 -2
  32. package/dist/preservation.js +89 -0
  33. package/dist/preservation.test.js +22 -0
  34. package/dist/profile.js +87 -0
  35. package/dist/profile.test.js +114 -0
  36. package/dist/rebuild-task.js +226 -0
  37. package/dist/rebuild-task.test.js +179 -0
  38. package/dist/release-audit.test.js +144 -2
  39. package/dist/rewrite-task.js +136 -16
  40. package/dist/rewrite-task.test.js +72 -4
  41. package/dist/rule-reconciliation.test.js +50 -0
  42. package/dist/semantic-review.js +176 -7
  43. package/dist/semantic-review.test.js +98 -14
  44. package/dist/stage1-dry-run.test.js +39 -0
  45. package/dist/stage1-evaluation.js +579 -0
  46. package/dist/stage1-evaluation.test.js +184 -0
  47. package/dist/stage1-human-packet.test.js +102 -0
  48. package/dist/stage1-schema-contract.test.js +95 -0
  49. package/dist/stage2-human-packet.test.js +81 -0
  50. package/dist/version.js +1 -0
  51. package/dist/voice-dna.js +53 -1
  52. package/dist/voice-dna.test.js +79 -1
  53. package/package.json +2 -2
package/Readme.md CHANGED
@@ -8,13 +8,13 @@ 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
 
15
15
  Everything in the CLI runs from local files: accounts, API calls, telemetry, payment collection, and runtime network requests stay out of the core path. The optional Claude extension adds a local stdio MCP adapter around that same engine; it is not a hosted service.
16
16
 
17
- > **Status:** the public CLI is published as [`@holdyourvoice/hyv`](https://www.npmjs.com/package/@holdyourvoice/hyv). It runs locally and makes no runtime network requests.
17
+ > **Status:** the public CLI is published as [`@holdyourvoice/hyv`](https://www.npmjs.com/package/@holdyourvoice/hyv). It runs locally and makes no runtime network requests. Version 3.3.0 adds pre-edit judgments, range edits, and authorized rebuild. Writer-study kits remain blocked optional research. Product publish uses the version bump and CI.
18
18
 
19
19
  ## Why it exists
20
20
 
@@ -55,7 +55,7 @@ To contribute, clone this repository, run `npm install`, then run `npm test` and
55
55
 
56
56
  ### Use it in Claude Desktop
57
57
 
58
- Build the fully local Claude Desktop extension with `npm run pack:claude`, then install `dist/hold-your-voice.mcpb` from **Settings → Extensions → Advanced settings → Install Extension**. The extension accepts text and portable profile JSON in the current conversation only. A successful verification records resolved finding IDs in local learning state; it never retains writing text or makes network requests. See the [Claude Desktop guide](docs/CLAUDE-DESKTOP.md).
58
+ Build the fully local Claude Desktop extension with `npm run pack:claude`, then install `dist/hold-your-voice.mcpb` from **Settings → Extensions → Advanced settings → Install Extension**. The extension accepts text and portable profile JSON in the current conversation only. Verification is read-only. Learning requires an explicit learning command or an approved lifecycle transition; neither path retains writing text or makes network requests. See the [Claude Desktop guide](docs/CLAUDE-DESKTOP.md).
59
59
 
60
60
  ### Build a local VoiceDNA profile
61
61
 
@@ -77,11 +77,42 @@ 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.
85
116
 
86
117
  ### Add contextual editorial guidance
87
118
 
@@ -95,7 +126,14 @@ Use an optional local WritingBrief when the same writer needs different guidance
95
126
  "format": "social",
96
127
  "readerKnowsAuthor": false,
97
128
  "vocabulary": ["deployment", "incident"],
98
- "prohibitedTerms": ["internal contract value"]
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
+ }
99
137
  }
100
138
  ```
101
139
 
@@ -105,7 +143,7 @@ hyv rewrite-prompt draft.md profile.json writing-brief.json > rewrite-brief.md
105
143
  hyv verify original.md candidate.md profile.json writing-brief.json
106
144
  ```
107
145
 
108
- Format checks are yellow review cues. Explicit `prohibitedTerms` are red release blockers. Keep client-specific briefs outside public repositories unless you have the right to publish them.
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.
109
147
 
110
148
  ### Inspect a batch
111
149
 
@@ -129,11 +167,11 @@ Give the brief and draft to a human editor or any model you trust. This reposito
129
167
  npx @holdyourvoice/hyv verify draft.md candidate.md profile.json
130
168
  ```
131
169
 
132
- `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.
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. It does not mutate learning state. It exits with `1` for a usage or runtime error. Treat status `2` as a release signal in scripts or CI.
133
171
 
134
172
  ### Lock factual claims with a CopySpec
135
173
 
136
- Use `verify-spec` when a draft has claims that must remain exact. A local CopySpec records each immutable claim alongside its evidence, then blocks a candidate if that claim is absent, changed, or joined by a prohibited claim.
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.
137
175
 
138
176
  ```json
139
177
  {
@@ -145,6 +183,7 @@ Use `verify-spec` when a draft has claims that must remain exact. A local CopySp
145
183
  {
146
184
  "id": "launch-date",
147
185
  "text": "The launch is on 14 August.",
186
+ "atoms": ["The launch is on 14 August."],
148
187
  "evidence": "Release calendar, checked 7 August."
149
188
  }
150
189
  ],
@@ -156,11 +195,11 @@ Use `verify-spec` when a draft has claims that must remain exact. A local CopySp
156
195
  hyv verify-spec original.md candidate.md profile.json copy-spec.json
157
196
  ```
158
197
 
159
- The check is deterministic. It covers declared claims and prohibited text; arbitrary unsupported assertions need a separate factual evaluator.
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.
160
199
 
161
200
  ### Local voice memory
162
201
 
163
- 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.
202
+ Learning changes are explicit. Use the learning commands below, or complete the separately authorized semantic-review and final-approval lifecycle before recording approved learning. State lives under `~/.hyv/learning/`, scoped to the portable profile, and stores no draft or candidate text. The next `rewrite-prompt` uses a bounded list of approved repairs.
164
203
 
165
204
  ```bash
166
205
  hyv learning show profile.json
@@ -229,15 +268,19 @@ Read the full [VoiceDNA reference](docs/VOICE-DNA.md) and [Wiki guide](https://g
229
268
 
230
269
  ## AI Editor: inspectable rules
231
270
 
232
- 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 `3.2.0-reconciled.1` ruleset contains 148 stable catalog entries: the inherited catalog plus en-dash and performative-sincerity coverage. Applied profile policy determines whether a match blocks, advises, requires judgment, or is disabled. Duplicate legacy expressions remain cataloged for ID compatibility but emit one canonical finding. Most rules inspect sentences; selected inherited rules inspect one physical line to preserve multi-sentence and line-start behavior.
272
+
273
+ Run this command to see the rules and ruleset version that actually execute in the published CLI:
233
274
 
234
275
  ```bash
235
276
  npx @holdyourvoice/hyv patterns
236
277
  ```
237
278
 
279
+ From a built source checkout, run `node dist/cli.js patterns` instead.
280
+
238
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.
239
282
 
240
- 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.
241
284
 
242
285
  ## Verification contract
243
286
 
@@ -256,35 +299,51 @@ The preservation score is a guardrail based on retained original words longer th
256
299
  | --- | --- | --- | --- |
257
300
  | `hyv profile <profile.json> <sample...>` | Two or more text files | Profile JSON | You need a new local reference. |
258
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. |
259
304
  | `hyv rewrite-prompt <draft> <profile.json>` | Draft and profile | Markdown editing brief | You need a constrained request for an editor or model. |
305
+ | `hyv prepare-rewrite <draft> <profile.json> <task.json>` | Draft and profile | Versioned task file plus metadata | A host needs a fingerprint-bound sentence-edit task. |
306
+ | `hyv apply-rewrite <task.json> <response.json> <profile.json>` | Task, response, and profile | Candidate evaluation JSON | A host needs to apply and recheck eligible sentence replacements. |
260
307
  | `hyv verify <original> <candidate> <profile.json>` | Original, candidate, profile | Verification JSON and exit code | You need the candidate gate. |
261
308
  | `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. |
262
- | `hyv learning <show\|add\|clear> <profile.json>` | Profile and optional instruction | Local learning JSON | You need to inspect or manage profile-scoped learning. |
309
+ | `hyv learning <show\|inspect\|add\|record\|ratify\|supersede\|migrate\|clear> ...` | Profile, operation value, and bounded metadata options | Preferences or a text-free mutation receipt | You need to inspect, migrate, or manage profile-scoped learning. |
310
+ | `hyv lifecycle <prepare-semantic\|submit-verdict\|inspect\|validate-final-approval\|finalize> ...` | Versioned lifecycle artifacts | Canonical lifecycle artifact or metadata | A normal-policy semantic review or human decision must advance through the shared reducer. |
263
311
  | `hyv patterns` | None | Ruleset JSON | You need the exact enabled rules. |
264
312
 
265
313
  Every file argument can be `-` when the command accepts text input from standard input. Profile output is always written to the path you give it. Use `npx @holdyourvoice/hyv <command>` in place of `hyv <command>` when you have not installed the CLI globally.
266
314
 
315
+ Profile v3 learning is keyed by its stable local profile ID, so compatible history survives profile revisions. `record`, `ratify`, and `supersede` accept bounded `--mutation-id`, `--authority`, `--provenance`, `--weight`, and `--compatibility` options. `ratify` and `supersede` require Profile v3. `migrate` explicitly copies compatible legacy Profile v2 learning into one Profile v3 identity. Replaying an identical mutation is idempotent; reusing its ID for a different operation returns a conflict. Inspection and receipts expose event metadata only, never stored instructions or draft text.
316
+
317
+ The standalone CLI supports normal-policy semantic review. High-assurance review requires a trusted embedding and is rejected by the CLI. Approval capabilities are accepted only through `--capability-stdin` or a permission-checked `--capability-file`; adapters validate capabilities but never mint them. Rejection needs no capability. Approval and `learning record-approved` require the matching signed final-approval capability. `apply-rewrite`, `lifecycle submit-verdict`, and `lifecycle finalize` exit `2` when the candidate or transition is not accepted, while usage and runtime failures exit `1`.
318
+
267
319
  ## Project map
268
320
 
269
321
  | Path | Responsibility |
270
322
  | --- | --- |
271
323
  | `src/contracts.ts` | Profiles, findings, reports, analysis, and verification data shapes. |
272
324
  | `src/text.ts` | Sentence, paragraph, word, and basic statistics helpers. |
325
+ | `src/hygiene.ts` | Profile-free hidden Unicode inspection and conservative cleaning. |
273
326
  | `src/voice-dna.ts` | Builds profiles and runs VoiceDNA checks. |
274
327
  | `src/ai-editor.ts` | Owns the versioned deterministic editorial rules. |
275
328
  | `src/editorial-packs.ts` | Parses WritingBrief context and runs format and batch checks. |
276
329
  | `src/learning.ts` | Stores text-free, profile-scoped verified repairs and composes bounded local preferences. |
277
- | `src/pipeline.ts` | Combines pass states, makes briefs, and verifies candidates. |
330
+ | `src/pipeline.ts` | Combines scored pass states, makes briefs, and verifies candidates. |
331
+ | `src/rewrite-task.ts` | Prepares and evaluates fingerprint-bound sentence-replacement tasks. |
332
+ | `src/semantic-review.ts` | Defines and reduces semantic and human-review lifecycle artifacts. |
333
+ | `src/approval-capability.ts` | Verifies canonical signed approval capabilities. |
334
+ | `src/approval-context.ts` | Loads permission-checked trust roots and evaluator authorization. |
335
+ | `src/lifecycle-adapter.ts` | Shares lifecycle operations across CLI and MCP adapters. |
278
336
  | `src/cli.ts` | Local file and standard-input command adapter. |
337
+ | `src/mcp.ts` | Local stdio MCP registration and host-capability gating. |
279
338
  | `src/pipeline.test.ts` | Contract and regression tests. |
280
339
  | `CONTRIBUTING.md` | Public-safety rules and the contributor model. |
281
340
  | `scripts/release-audit.mjs` | Checks source files for credential and network markers. |
282
341
 
283
- `pipeline.ts` is the sole composition point. It combines pass states and preserves each engine’s separate score.
342
+ `pipeline.ts` is the sole scored output-composition point. It combines pass states and preserves each engine’s separate score. Rewrite-task and lifecycle modules compose their own versioned, non-scoring artifacts.
284
343
 
285
344
  ## Privacy and data rights
286
345
 
287
- The runtime uses files on your machine. Samples, drafts, profiles, candidates, and client data stay there. Successful verification writes a text-free local learning event under `~/.hyv/learning/`: profile fingerprint, finding IDs, severities, counts, timestamp, and an opaque one-way candidate digest for retry deduplication. An instruction added through `hyv learning add` is stored as entered.
346
+ The runtime uses files on your machine. Samples, drafts, profiles, candidates, and client data stay there. Verification is read-only. Explicit learning commands and approved lifecycle recording can write text-free local events under `~/.hyv/learning/`: profile fingerprint, finding IDs, severities, counts, timestamp, and an opaque one-way candidate digest for retry deduplication. An instruction added through `hyv learning add` is stored as entered.
288
347
 
289
348
  The package does not upload writing, use embeddings, or make runtime network requests. Keep writing samples, edit histories, client text, local learning files, and datasets out of public commits unless you hold explicit rights and a provenance record. A profile is aggregated JSON and can still reveal vocabulary and preferences. Store private profiles outside public repositories.
290
349
 
@@ -319,7 +378,7 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Keep chan
319
378
 
320
379
  ## npm releases
321
380
 
322
- `@holdyourvoice/hyv` is published automatically after a change to the package source reaches `main`. The workflow publishes only when the version in `package.json` is not already on npm, so bump that version in the same pull request as a release-worthy change. It runs the tests and release audit before publishing, then verifies that npm reports the package as MIT licensed.
381
+ `@holdyourvoice/hyv` is published automatically after a change to the package source reaches `main`. The workflow publishes only when the version in `package.json` is not already on npm, so bump that version in the same pull request as a release-worthy change. It runs the tests and release audit before publishing, then verifies that npm reports the package as MIT licensed. Writer-study kits stay optional research. Product publish uses the version bump and CI. Keep writer-checkpoint claims off the publish.
323
382
 
324
383
  ## Support
325
384
 
@@ -0,0 +1,151 @@
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: /^\s*not\s+[^.!?\n]{1,60}\.\s+[^.!?\n]{1,60}(?:[.!?]|$)/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: "red", 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: "punct.en-dash", severity: "red", expression: /–/, reason: "A stock phrase can flatten the writer's meaning.", suggestion: "en dashes are an AI tell \u2014 use a plain hyphen, period, comma, or parentheses" },
121
+ { 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." },
122
+ { 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" },
123
+ { 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" },
124
+ { 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" },
125
+ { 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" },
126
+ { 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" },
127
+ { 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" },
128
+ { id: "cringe.10x", severity: "red", expression: /\b10x\b/i, reason: "The phrase makes a broad promotional claim without evidence.", suggestion: "use the actual numbers" },
129
+ { 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" },
130
+ { 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" },
131
+ { 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." },
132
+ { 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" },
133
+ { 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" },
134
+ { 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." },
135
+ { 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." },
136
+ { 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." },
137
+ { 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" },
138
+ { 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"' },
139
+ { 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"' },
140
+ { 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" },
141
+ { 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" },
142
+ { 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"' },
143
+ { 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"' },
144
+ { 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"' },
145
+ { 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" },
146
+ { 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" },
147
+ { id: "formula.performative-sincerity", severity: "red", expression: /\b(?:to be honest|in all honesty)\b/i, reason: "The phrase announces sincerity instead of making the claim directly.", suggestion: "cut the sincerity preamble and state the claim" },
148
+ { id: "hedge.performative-sincerity-adverb", severity: "yellow", expression: /\b(?:honestly|genuinely|truly|frankly|actually)\b/i, reason: "The adverb performs sincerity instead of adding evidence.", suggestion: "cut the adverb or replace it with the evidence" },
149
+ { 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." },
150
+ { 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." },
151
+ ];
package/dist/ai-editor.js CHANGED
@@ -1,9 +1,105 @@
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 = '3.2.0-reconciled.1';
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
+ const ruleIds = new Set(rules.map((rule) => rule.id));
9
+ const policyStates = new Set(['blocking', 'advisory', 'judgment-required', 'disabled']);
10
+ const suppressedDuplicateIds = new Set(['hedge.worth-noting', 'struct.in-other-words']);
11
+ const reconciledPolicies = {
12
+ 'struct.not-x-y': 'advisory',
13
+ 'struct.this-isnt-x-this-is-y': 'advisory',
14
+ 'struct.same-better': 'disabled',
15
+ 'struct.moment-becomes': 'disabled',
16
+ 'hedge.i-think': 'disabled',
17
+ 'ai.question-hook': 'advisory',
18
+ };
19
+ function defaultPolicy(id, severity) {
20
+ const reconciled = reconciledPolicies[id];
21
+ if (reconciled)
22
+ return reconciled;
23
+ if (severity === 'red' && (id.startsWith('ai.') || id.startsWith('ogilvy.')))
24
+ return 'judgment-required';
25
+ return severity === 'red' ? 'blocking' : 'advisory';
26
+ }
27
+ function policiesFor(profile) {
28
+ if (profile?.version === '3') {
29
+ for (const [id, state] of Object.entries(profile.rulePolicy)) {
30
+ if (!ruleIds.has(id))
31
+ throw new Error(`Profile rulePolicy contains unknown rule ID: ${id}`);
32
+ if (!policyStates.has(state))
33
+ throw new Error(`Profile rulePolicy contains invalid state for rule ID: ${id}`);
34
+ }
35
+ }
36
+ return new Map(rules.map((rule) => [rule.id, profile?.version === '3' && profile.rulePolicy[rule.id]
37
+ ? profile.rulePolicy[rule.id]
38
+ : defaultPolicy(rule.id, rule.severity)]));
39
+ }
40
+ export function serializedRules() {
41
+ return rules.map((rule) => ({
42
+ id: rule.id,
43
+ severity: rule.severity,
44
+ reason: rule.reason,
45
+ suggestion: rule.suggestion,
46
+ expression: { source: rule.expression.source, flags: rule.expression.flags },
47
+ scope: rule.scope ?? 'sentence',
48
+ }));
49
+ }
50
+ export function analyzeAiEditor(text, profile) {
51
+ const matched = [];
52
+ const mapped = sentences(text);
53
+ for (const sentence of mapped) {
54
+ for (const rule of sentenceRules) {
55
+ if (rule.expression.test(sentence.text)) {
56
+ matched.push({
57
+ engine: 'ai_editor',
58
+ id: rule.id,
59
+ severity: rule.severity,
60
+ sentence: sentence.index,
61
+ excerpt: sentence.text,
62
+ reason: rule.reason,
63
+ suggestion: rule.suggestion,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ let lineStart = 0;
69
+ for (const line of text.split('\n')) {
70
+ for (const rule of lineRules) {
71
+ const result = rule.expression.exec(line);
72
+ if (!result)
73
+ continue;
74
+ const matchStart = lineStart + result.index;
75
+ const sentence = mapped.find((candidate) => candidate.start <= matchStart && matchStart < candidate.end);
76
+ if (!sentence)
77
+ continue;
78
+ matched.push({
79
+ engine: 'ai_editor',
80
+ id: rule.id,
81
+ severity: rule.severity,
82
+ sentence: sentence.index,
83
+ excerpt: sentence.text,
84
+ reason: rule.reason,
85
+ suggestion: rule.suggestion,
86
+ });
87
+ }
88
+ lineStart += line.length + 1;
89
+ }
90
+ matched.sort((left, right) => left.sentence - right.sentence || (ruleOrder.get(left.id) ?? 0) - (ruleOrder.get(right.id) ?? 0));
91
+ const policies = policiesFor(profile);
92
+ const findings = matched.flatMap((finding) => {
93
+ if (suppressedDuplicateIds.has(finding.id))
94
+ return [];
95
+ if (finding.id === 'ai.question-hook' && finding.sentence !== 1)
96
+ return [];
97
+ const appliedPolicy = policies.get(finding.id);
98
+ if (!appliedPolicy || appliedPolicy === 'disabled')
99
+ return [];
100
+ return [{ ...finding, appliedPolicy, severity: appliedPolicy === 'blocking' ? 'red' : 'yellow' }];
101
+ });
102
+ const blocking = findings.reduce((count, finding) => count + Number(finding.appliedPolicy === 'blocking'), 0);
103
+ const score = Math.max(0, 100 - blocking * 18 - (findings.length - blocking) * 6);
104
+ return { engine: 'ai_editor', version: RULESET_VERSION, score, passed: blocking === 0, findings };
105
+ }