@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.
- package/Readme.md +76 -17
- package/dist/ai-editor-rules.js +151 -0
- package/dist/ai-editor.js +104 -8
- package/dist/ai-editor.test.js +135 -22
- package/dist/approval-capability.js +111 -0
- package/dist/approval-capability.test.js +52 -0
- package/dist/approval-context.js +54 -0
- package/dist/approval-context.test.js +38 -0
- package/dist/benchmark.js +232 -0
- package/dist/benchmark.test.js +328 -0
- package/dist/canonical-json.js +123 -0
- package/dist/canonical-json.test.js +24 -0
- package/dist/cli.js +359 -21
- package/dist/cli.test.js +275 -7
- package/dist/copy-spec.js +35 -8
- package/dist/editorial-packs.js +25 -1
- package/dist/editorial-packs.test.js +45 -0
- package/dist/hygiene.js +91 -0
- package/dist/hygiene.test.js +73 -0
- package/dist/judgment-task.js +171 -0
- package/dist/judgment-task.test.js +162 -0
- package/dist/learning.js +240 -100
- package/dist/learning.test.js +203 -3
- package/dist/lifecycle-adapter.js +75 -0
- package/dist/lifecycle-adapter.test.js +56 -0
- package/dist/mcp-tools.js +110 -9
- package/dist/mcp-tools.test.js +188 -10
- package/dist/mcp.js +228 -9
- package/dist/mcp.test.js +248 -12
- package/dist/pipeline.js +81 -15
- package/dist/pipeline.test.js +94 -2
- package/dist/preservation.js +89 -0
- package/dist/preservation.test.js +22 -0
- package/dist/profile.js +87 -0
- package/dist/profile.test.js +114 -0
- package/dist/rebuild-task.js +226 -0
- package/dist/rebuild-task.test.js +179 -0
- package/dist/release-audit.test.js +144 -2
- package/dist/rewrite-task.js +136 -16
- package/dist/rewrite-task.test.js +72 -4
- package/dist/rule-reconciliation.test.js +50 -0
- package/dist/semantic-review.js +176 -7
- package/dist/semantic-review.test.js +98 -14
- package/dist/stage1-dry-run.test.js +39 -0
- package/dist/stage1-evaluation.js +579 -0
- package/dist/stage1-evaluation.test.js +184 -0
- package/dist/stage1-human-packet.test.js +102 -0
- package/dist/stage1-schema-contract.test.js +95 -0
- package/dist/stage2-human-packet.test.js +81 -0
- package/dist/version.js +1 -0
- package/dist/voice-dna.js +53 -1
- package/dist/voice-dna.test.js +79 -1
- 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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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).
|
|
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>
|
|
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.
|
|
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
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|