Skill: brandonwise/ai-humanizer (version 2.1.0)

--- name: humanizer description: > Humanize AI-generated text by detecting and removing patterns typical of LLM output. Rewrites text to sound natural, specific, and human. Uses 24 pattern detectors, 500+ AI vocabulary terms across 3 tiers, and statistical analysis (burstiness, type-token ratio, readability) for comprehensive detection. Use when asked to humanize text, de-AI writing, make content sound more natural/human, review writing for AI patterns, score text for AI detection, or improve AI-generated drafts. Covers content, language, style, communication, and filler categories. --- # Humanizer: remove AI writing patterns You are a writing editor that identifies and removes signs of AI-generated text. Your goal: make writing sound like a specific human wrote it, not like it was extruded from a language model. Based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), Copyleaks stylometric research, and real-world pattern analysis. ## Your task When given text to humanize: 1. Scan for the 24 patterns below 2. Check statistical indicators (burstiness, vocabulary diversity, sentence uniformity) 3. Rewrite problematic sections with natural alternatives 4. Preserve the core meaning 5. Match the intended tone (formal, casual, technical) 6. Add actual personality — sterile text is just as obvious as slop ## Quick reference: the 24 patterns | # | Pattern | Category | What to watch for | |---|---------|----------|-------------------| | 1 | Significance inflation | Content | "marking a pivotal moment in the evolution of..." | | 2 | Notability name-dropping | Content | Listing media outlets without specific claims | | 3 | Superficial -ing analyses | Content | "...showcasing... reflecting... highlighting..." | | 4 | Promotional language | Content | "nestled", "breathtaking", "stunning", "renowned" | | 5 | Vague attributions | Content | "Experts believe", "Studies show", "Industry reports" | | 6 | Formulaic challenges | Content | "Despite challenges... continues to thrive" | | 7 | AI vocabulary (500+ words) | Language | "delve", "tapestry", "landscape", "showcase", "seamless" | | 8 | Copula avoidance | Language | "serves as", "boasts", "features" instead of "is", "has" | | 9 | Negative parallelisms | Language | "It's not just X, it's Y" | | 10 | Rule of three | Language | "innovation, inspiration, and insights" | | 11 | Synonym cycling | Language | "protagonist... main character... central figure..." | | 12 | False ranges | Language | "from the Big Bang to dark matter" | | 13 | Em dash overuse | Style | Too many — dashes — everywhere | | 14 | Boldface overuse | Style | **Mechanical** **emphasis** **everywhere** | | 15 | Inline-header lists | Style | "- **Topic:** Topic is discussed here" | | 16 | Title Case headings | Style | Every Main Word Capitalized In Headings | | 17 | Emoji overuse | Style | 🚀💡✅ decorating professional text | | 18 | Curly quotes | Style | "smart quotes" instead of "straight quotes" | | 19 | Chatbot artifacts | Communication | "I hope this helps!", "Let me know if..." | | 20 | Cutoff disclaimers | Communication | "As of my last training...", "While details are limited..." | | 21 | Sycophantic tone | Communication | "Great question!", "You're absolutely right!" | | 22 | Filler phrases | Filler | "In order to", "Due to the fact that", "At this point in time" | | 23 | Excessive hedging | Filler | "could potentially possibly", "might arguably perhaps" | | 24 | Generic conclusions | Filler | "The future looks bright", "Exciting times lie ahead" | ## Statistical signals Beyond pattern matching, check for these AI statistical tells: | Signal | Human | AI | Why | |--------|-------|----|----| | Burstiness | High (0.5-1.0) | Low (0.1-0.3) | Humans write in bursts; AI is metronomic | | Type-token ratio | 0.5-0.7 | 0.3-0.5 | AI reuses the same vocabulary | | Sentence length variation | High CoV | Low CoV | AI sentences are all roughly the same length | | Trigram repetition | Low (<0.05) | High (>0.10) | AI reuses 3-word phrases | ## Vocabulary tiers - **Tier 1 (Dead giveaways):** delve, tapestry, vibrant, crucial, comprehensive, meticulous, embark, robust, seamless, groundbreaking, leverage, synergy, transformative, paramount, multifaceted, myriad, cornerstone, reimagine, empower, catalyst, invaluable, bustling, nestled, realm - **Tier 2 (Suspicious in density):** furthermore, moreover, paradigm, holistic, utilize, facilitate, nuanced, illuminate, encompasses, catalyze, proactive, ubiquitous, quintessential - **Phrases:** "In today's digital age", "It is worth noting", "plays a crucial role", "serves as a testament", "in the realm of", "delve into", "harness the power of", "embark on a journey", "without further ado" ## Core principles ### Write like a human, not a press release - Use "is" and "has" freely — "serves as" is pretentious - One qualifier per claim — don't stack hedges - Name your sources or drop the claim - End with something specific, not "the future looks bright" ### Add personality - Have opinions. React to facts, don't just report them - Vary sentence rhythm. Short. Then longer ones that meander. - Acknowledge complexity and mixed feelings - Let some mess in — perfect structure feels algorithmic ### Cut the fat - "In order to" → "to" - "Due to the fact that" → "because" - "It is important to note that" → (just say it) - Remove chatbot filler: "I hope this helps!", "Great question!" ## Before/after example **Before (AI-sounding):** > Great question! Here is an overview of sustainable energy. Sustainable energy serves as an enduring testament to humanity's commitment to environmental stewardship, marking a pivotal moment in the evolution of global energy policy. In today's rapidly evolving landscape, these groundbreaking technologies are reshaping how nations approach energy production, underscoring their vital role in combating climate change. The future looks bright. I hope this helps! **After (human):** > Solar panel costs dropped 90% between 2010 and 2023, according to IRENA data. That single fact explains why adoption took off — it stopped being an ideological choice and became an economic one. Germany gets 46% of its electricity from renewables now. The transition is happening, but it's messy and uneven, and the storage problem is still mostly unsolved. ## Using the analyzer ```bash # Score text (0-100, higher = more AI-like) echo "Your text here" | node src/cli.js score # Full analysis report node src/cli.js analyze -f draft.md # Markdown report node src/cli.js report article.txt > report.md # Suggestions grouped by priority node src/cli.js suggest essay.txt # Statistical analysis only node src/cli.js stats essay.txt # Humanization suggestions with auto-fixes node src/cli.js humanize --autofix -f article.txt # JSON output for programmatic use node src/cli.js analyze --json < input.txt ``` ## Always-on mode For agents that should ALWAYS write like a human (not just when asked to humanize), add the core rules to your personality/system prompt. See the README's "Always-On Mode" section for copy-paste templates for OpenClaw (SOUL.md), Claude, and ChatGPT. The key rules to internalize: - Ban Tier 1 vocabulary (delve, tapestry, vibrant, crucial, robust, seamless, etc.) - Kill filler phrases ("In order to" → "to", "Due to the fact that" → "because") - No sycophancy, chatbot artifacts, or generic conclusions - Vary sentence length, have opinions, use concrete specifics - If you wouldn't say it in conversation, don't write it ## Process 1. Read the input text 2. Run pattern detection (24 patterns, 500+ vocabulary terms) 3. Compute text statistics (burstiness, TTR, readability) 4. Identify all issues and generate suggestions 5. Rewrite problematic sections 6. Verify the result sounds natural when read aloud 7. Present the humanized version with a brief change summary

path: README.md
content: # humanizer

![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
![Tests](https://img.shields.io/badge/tests-128%20passing-brightgreen)
![Node >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)

Detect and remove signs of AI-generated writing. Makes text sound natural and human.

An [OpenClaw](https://github.com/nichochar/openclaw) skill and standalone CLI tool that scans text for **24 AI writing patterns** using **500+ vocabulary terms** and **statistical text analysis** (burstiness, type-token ratio, readability metrics) — then provides actionable suggestions to fix them.

Based on [Wikipedia:Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing), [Copyleaks stylistic fingerprint research](https://arxiv.org/abs/2503.01659), and [blader/humanizer](https://github.com/blader/humanizer).

## Install

### As an OpenClaw skill

```bash
git clone https://github.com/brandonwise/humanizer.git
cp humanizer/SKILL.md ~/.config/openclaw/skills/humanizer.md
```

### As a standalone CLI tool

```bash
git clone https://github.com/brandonwise/humanizer.git
cd humanizer
npm install

# Score some text
echo "This serves as a testament to innovation." | node src/cli.js score

# Full analysis
node src/cli.js analyze -f your-draft.md

# Humanize with auto-fixes
node src/cli.js humanize --autofix -f article.txt
```

### Global install

```bash
npm install -g .
humanizer score < draft.txt
humanizer analyze -f essay.md
humanizer humanize --autofix < article.txt
```

## Architecture

The scoring engine combines three signal types:

```
┌─────────────────────────────────────────────────┐
│              Composite Score (0-100)             │
├────────────────────┬────────────────────────────┤
│   Pattern Score    │    Uniformity Score         │
│   (70% weight)     │    (30% weight)             │
├────────────────────┼────────────────────────────┤
│ • 24 pattern       │ • Burstiness (sentence     │
│   detectors        │   length variation)         │
│ • 500+ vocabulary  │ • Type-token ratio          │
│   terms (3 tiers)  │ • Trigram repetition        │
│ • Density scoring  │ • Sentence length CoV       │
│ • Category breadth │ • Paragraph uniformity      │
└────────────────────┴────────────────────────────┘
```

**Pattern score** uses density-based detection: weighted hits per 100 words on a logarithmic curve, plus bonuses for breadth (unique patterns) and category diversity.

**Uniformity score** uses statistical analysis: human text has high burstiness (varied sentence lengths), diverse vocabulary, and low n-gram repetition. AI text is mechanically uniform.

## Statistical analysis

The stats engine computes metrics that differentiate AI from human writing:

| Metric | Human Writing | AI Writing | Why It Matters |
|--------|--------------|------------|----------------|
| **Burstiness** | 0.5–1.0 | 0.1–0.3 | Humans write in bursts — short sentences, then long ones. AI is metronomic. |
| **Type-token ratio** | 0.5–0.7 | 0.3–0.5 | Humans use more varied vocabulary. AI cycles through the same words. |
| **Sentence CoV** | 0.4–0.8 | 0.15–0.35 | Coefficient of variation in sentence length. Low = robotic uniformity. |
| **Trigram repetition** | < 0.05 | > 0.10 | AI reuses the same 3-word phrases more often. |
| **Readability (FK)** | Varies | 8–12 | AI tends to write at a consistent grade level. Humans vary. |

## CLI reference

### Commands

```bash
# Quick score (0-100, higher = more AI-like)
echo "text" | humanizer score

# Full analysis with pattern matches
humanizer analyze essay.txt

# Full markdown report (pipe to file)
humanizer report article.txt > report.md

# Suggestions grouped by priority
humanizer suggest draft.md

# Statistical analysis only
humanizer stats essay.txt

# Humanization suggestions with guidance
humanizer humanize -f article.txt

# Apply safe auto-fixes
humanizer humanize --autofix -f article.txt
```

### Options

```bash
-f, --file <path>       Read text from file
--json                  Output as JSON
--verbose, -v           Show all matches
--autofix               Apply safe fixes (humanize only)
--patterns <ids>        Check specific pattern IDs (comma-separated)
--threshold <n>         Only show patterns with weight above n
--config <file>         Custom config file (JSON)
--help, -h              Show help
```

### Score badges

```
🟢 0-25    Mostly human-sounding
🟡 26-50   Lightly AI-touched
🟠 51-75   Moderately AI-influenced
🔴 76-100  Heavily AI-generated
```

## API (programmatic use)

```javascript
const { analyze, score } = require('humanizer');

// Quick score
const s = score('Your text here...');
console.log(s); // 0-100

// Full analysis
const result = analyze(text, {
  verbose: true,          // Show all matches
  patternsToCheck: [7, 19, 22], // Only specific patterns
  includeStats: true,     // Include statistical analysis
});

console.log(result.score);           // 0-100 composite
console.log(result.patternScore);    // Pattern-only score
console.log(result.uniformityScore); // Stats-based uniformity score
console.log(result.stats);           // { burstiness, typeTokenRatio, ... }
console.log(result.findings);        // Detailed pattern matches
console.log(result.categories);      // Per-category breakdown

// Humanize
const { humanize, autoFix } = require('humanizer/src/humanizer');

const suggestions = humanize(text, { autofix: true });
console.log(suggestions.critical);   // Dead giveaway issues
console.log(suggestions.important);  // Noticeable patterns
console.log(suggestions.guidance);   // Writing tips
console.log(suggestions.styleTips);  // Statistical style advice
console.log(suggestions.autofix.text); // Auto-fixed text

// Stats only
const { computeStats } = require('humanizer/src/stats');
const stats = computeStats(text);
console.log(stats.burstiness);       // Sentence variation
console.log(stats.typeTokenRatio);   // Vocabulary diversity
```

## The 24 patterns

| # | Pattern | Category | Weight | Example |
|---|---------|----------|--------|---------|
| 1 | Significance inflation | Content | 4 | "marking a pivotal moment in the evolution of..." |
| 2 | Notability name-dropping | Content | 3 | "featured in NYT, BBC, CNN, and Forbes" |
| 3 | Superficial -ing analyses | Content | 4 | "...showcasing... reflecting... highlighting..." |
| 4 | Promotional language | Content | 3 | "nestled", "breathtaking", "stunning" |
| 5 | Vague attributions | Content | 4 | "Experts believe", "Studies show" |
| 6 | Formulaic challenges | Content | 3 | "Despite challenges... continues to thrive" |
| 7 | AI vocabulary | Language | 5 | "Additionally", "delve", "tapestry" (500+ words) |
| 8 | Copula avoidance | Language | 3 | "serves as" instead of "is" |
| 9 | Negative parallelisms | Language | 3 | "It's not just X, it's Y" |
| 10 | Rule of three | Language | 2 | "innovation, inspiration, and insights" |
| 11 | Synonym cycling | Language | 2 | "protagonist... main character... central figure" |
| 12 | False ranges | Language | 2 | "from the Big Bang to dark matter" |
| 13 | Em dash overuse | Style | 2 | Too many — em dashes — in one — piece |
| 14 | Boldface overuse | Style | 2 | **Every** **other** **word** bolded |
| 15 | Inline-header lists | Style | 3 | "- **Topic:** Topic is..." |
| 16 | Title Case headings | Style | 1 | "## Every Word Capitalized Here" |
| 17 | Emoji overuse | Style | 2 | 🚀💡✅ in professional text |
| 18 | Curly quotes | Style | 1 | \u201Csmart quotes\u201D instead of "straight" |
| 19 | Chatbot artifacts | Comms | 5 | "I hope this helps!", "Let me know if..." |
| 20 | Cutoff disclaimers | Comms | 4 | "As of my last training update..." |
| 21 | Sycophantic tone | Comms | 4 | "Great question!", "You're absolutely right!" |
| 22 | Filler phrases | Filler | 3 | "In order to", "Due to the fact that" |
| 23 | Excessive hedging | Filler | 3 | "could potentially possibly" |
| 24 | Generic conclusions | Filler | 3 | "The future looks bright" |

## Vocabulary tiers

- **Tier 1** (Dead giveaways): 50+ words that appear 5-20x more in AI text. Always flagged. Examples: *delve, tapestry, vibrant, crucial, meticulous, seamless, groundbreaking*
- **Tier 2** (Suspicious in density): 80+ words flagged when 2+ appear. Examples: *furthermore, paradigm, holistic, utilize, facilitate, nuanced*
- **Tier 3** (Context-dependent): 60+ words flagged only at >3% density. Examples: *significant, effective, unique, compelling, exceptional*
- **Phrases**: 80+ multi-word patterns. Examples: *"In today's digital age"*, *"plays a crucial role"*, *"serves as a testament"*

## How scoring works

1. **Pattern detection** — Each of 24 detectors scans for regex matches. Matches are weighted 1-5.
2. **Density calculation** — Weighted matches per 100 words, on a logarithmic curve (prevents runaway scores).
3. **Breadth bonus** — More unique pattern types = higher score (up to +20).
4. **Category diversity** — Hits across content/language/style/communication/filler = higher score (up to +15).
5. **Statistical uniformity** — Low burstiness, low vocabulary diversity, high repetition add up to 100 uniformity points.
6. **Composite blend** — Pattern score (70%) + uniformity score (30%) = final score.

This transparent methodology means you can see exactly why text scored the way it did.

## What makes this different

| Feature | humanizer | GPTZero | Copyleaks | ZeroGPT |
|---------|-----------|---------|-----------|---------|
| Open source | ✅ | ❌ | ❌ | ❌ |
| Transparent scoring | ✅ Fully explainable | ❌ Black box | ❌ Black box | ❌ Black box |
| Actionable suggestions | ✅ Per-pattern guidance | ❌ Score only | ❌ Score only | ❌ Score only |
| Auto-fix | ✅ Safe mechanical fixes | ❌ | ❌ | ❌ |
| Statistical analysis | ✅ Burstiness, TTR, FK | ✅ Perplexity | ✅ Stylometric | ❌ |
| No API key needed | ✅ | ❌ | ❌ | ❌ |
| Works offline | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies | ✅ | N/A | N/A | N/A |

## Before/after

**Before (AI score: 78):**
> Great question! Here is an overview of AI-assisted coding. AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows. The future looks bright. I hope this helps!

**After (AI score: 4):**
> AI coding tools speed up boilerplate. In a 2024 Google study, developers using Codex finished simple functions 55% faster, but showed no improvement on debugging or architecture. I've used Copilot for a year. It's good at config files and test scaffolding. It's bad at knowing when it's wrong.

## Always-On Mode: Bake Into Your Agent's Personality

The skill works great on-demand, but the real power is making your AI **always** write like a human. Here's how to bake the humanizer principles into your agent's system prompt or personality file.

### For OpenClaw (SOUL.md)

Add this to your `SOUL.md` (or equivalent personality file):

```markdown
## Writing Like a Human (Anti-AI Patterns)

Your writing should be indistinguishable from a sharp, opinionated human.
These rules are always on — not just when asked to "humanize."

### Never Use These Words
Dead giveaways of AI output — ban them from your vocabulary:

**Words:** delve, tapestry, vibrant, crucial, comprehensive, meticulous, embark,
robust, seamless, groundbreaking, leverage, synergy, transformative, paramount,
multifaceted, myriad, cornerstone, reimagine, empower, catalyst, bolster,
spearhead, invaluable, bustling, nestled, realm, showcase, foster, garner,
interplay, enduring, pivotal, intricate, harness, unleash, revolutionize,
elucidate, encompass, holistic, utilize, facilitate, nuanced, paradigm, poised

**Phrases:** "In today's [anything]", "It is worth noting", "plays a crucial role",
"serves as a testament", "in the realm of", "delve into", "harness the power of",
"embark on a journey", "the landscape of", "without further ado"

**Filler to cut:** "In order to" → "to". "Due to the fact that" → "because".
"It is important to note that" → just say it.

### Kill These Patterns
1. No significance inflation — state facts, skip "pivotal moment" claims
2. No vague attributions — name the source or drop the claim
3. No -ing tails — "highlighting the importance of..." gets its own sentence
4. No copula avoidance — use "is" and "has", not "serves as" or "boasts"
5. No sycophancy — skip "Great question!" and just answer
6. No chatbot artifacts — no "I hope this helps" or "Let me know if..."
7. No generic conclusions — end with specifics, not "the future looks bright"

### Write With Human Texture
- Vary sentence length (short, then long, then short)
- Have opinions and take stances
- Use concrete specifics (numbers, names, dates)
- Allow imperfection (contractions, fragments, "And" or "But" starters)
- Use simple verbs ("is", "has", "does" are fine)
- Read it aloud — if you wouldn't say it, don't write it
```

### For Claude (system prompt)

Add to your system prompt or `CLAUDE.md`:

```
You write like a human, not an AI. Never use words like "delve", "tapestry",
"crucial", "comprehensive", "robust", "seamless", or "groundbreaking". Never
write "In today's...", "It is worth noting...", or "Great question!". Use "is"
not "serves as". Vary your sentence lengths. Have opinions. Use specific numbers
and names instead of vague claims. End with specifics, not "the future looks
bright". If you'd never say it in conversation, don't write it.
```

### For ChatGPT (Custom Instructions)

Paste into your Custom Instructions → "How would you like ChatGPT to respond?":

```
Write like a specific human, not a generic AI. Rules:
- Never use: delve, tapestry, vibrant, crucial, robust, seamless, groundbreaking,
  transformative, leverage, synergy, paramount, multifaceted, myriad
- Never start with "In today's..." or end with "the future looks bright"
- Never write "Great question!" or "I hope this helps!"
- Use "is" not "serves as". Use "to" not "in order to"
- Vary sentence length. Short. Then longer. Have opinions.
- Use real numbers and names, not "experts say" or "studies show"
```

### Verification

After baking in, test your agent by asking it to write about any topic. Then scan it:

```bash
echo "Your agent's response here" | node src/cli.js score
```

Target: consistently **under 25** on the humanizer score.

## Project structure

```
humanizer/
├── SKILL.md          # OpenClaw skill definition
├── src/
│   ├── patterns.js   # 24 pattern detectors + pattern registry
│   ├── vocabulary.js  # 500+ AI words/phrases (3 tiers)
│   ├── stats.js       # Statistical analysis engine
│   ├── analyzer.js    # Composite scoring engine
│   ├── humanizer.js   # Suggestion engine + auto-fix
│   └── cli.js         # CLI with colored output
├── tests/             # Vitest test suite (128 tests)
│   ├── analyzer.test.js
│   ├── humanizer.test.js
│   ├── statistics.test.js
│   ├── calibration.test.js
│   ├── performance.test.js
│   └── edge-cases.test.js
├── references/        # Pattern catalogs, vocabulary lists
└── docs/              # Detailed documentation
```

## Contributing

1. Fork and create a branch
2. Add/improve pattern detection (see `src/patterns.js`)
3. Write tests for your changes
4. Run `npm test` — all tests must pass
5. Open a PR

## License

[MIT](LICENSE)


path: assets/banner.md
content: ```
 _                                _
| |__  _   _ _ __ ___   __ _ _ __ (_)_______ _ __
| '_ \| | | | '_ ` _ \ / _` | '_ \| |_  / _ \ '__|
| | | | |_| | | | | | | (_| | | | | |/ /  __/ |
|_| |_|\__,_|_| |_| |_|\__,_|_| |_|_/___\___|_|

  Detect and remove AI writing patterns.
  24 patterns. 5 categories. Zero dependencies.
```


path: docs/CONTRIBUTING.md
content: # Contributing to humanizer

Thanks for your interest in improving AI writing detection.

## Getting started

```bash
git clone https://github.com/brandonwise/humanizer.git
cd humanizer
npm install
npm test
```

## What to work on

- **New patterns** — Found an AI writing pattern not covered? Add it
- **Better detection** — Improve regex accuracy, reduce false positives
- **More vocabulary** — Expand the AI word/phrase lists
- **Test cases** — Add fixtures that expose detection gaps
- **Documentation** — Improve examples, add before/after pairs

## Adding a new pattern

1. Add the pattern definition to `src/patterns.js` (see `docs/PATTERNS.md` for the format)
2. Write tests in `tests/analyzer.test.js`
3. Add documentation to `references/patterns.md`
4. Run `npm test` — everything must pass

## Improving detection

The main source of false positives is overly broad regex. When tightening patterns:

- Add word boundary markers (`\b`)
- Use context-aware matching (surrounding words matter)
- Test against both AI samples and human samples
- Check that human text doesn't get flagged unfairly

## Code style

- Pure Node.js, no external runtime dependencies
- CommonJS modules (`require`/`module.exports`)
- No build step — the code runs directly
- Comments explain *why*, not *what*
- Functions are small and single-purpose

## Tests

- All tests use vitest
- Run with `npm test`
- Test each pattern detector individually
- Include both positive matches (should detect) and negative cases (should not detect)
- Test fixtures go in `tests/fixtures/`

## Pull request process

1. Fork the repo and create a branch (`git checkout -b add-pattern-25`)
2. Make your changes
3. Run `npm test` — all tests must pass
4. Open a PR with a clear description of what changed and why
5. Include before/after examples for any new patterns

## Reporting issues

Found a false positive? A missed pattern? Open an issue with:

- The text that was incorrectly flagged (or missed)
- Which pattern is involved
- What you expected to happen
- What actually happened


path: docs/EXAMPLES.md
content: # Real-world before/after examples

## Example 1: Tech product description

**Before (score: 72):**
> Great question! Here is a comprehensive overview of our groundbreaking platform.
>
> Our platform serves as a testament to the transformative power of AI, marking a pivotal moment in the evolution of enterprise software. In today's rapidly evolving landscape, businesses need cutting-edge solutions that streamline operations, enhance productivity, and foster collaboration.
>
> The platform boasts a robust suite of tools, showcasing our commitment to innovation. Additionally, the seamless integration capabilities highlight the intricate interplay between modern workflows and AI-driven automation, ensuring organizations can harness the full potential of their data.
>
> Industry experts believe our platform plays a crucial role in digital transformation. The future looks bright as exciting times lie ahead. I hope this helps! Let me know if you'd like more details.

**After (score: 2):**
> The platform connects to Salesforce, Slack, and Jira through pre-built integrations. Setup takes about 20 minutes.
>
> We added AI-powered search in March. It finds relevant documents across connected apps. In our beta with 50 companies, users said they spent 30% less time looking for files. That number came from self-reported surveys, so take it with some salt.
>
> The pricing starts at $15/user/month. There's a free tier for teams under 5.

**Patterns removed:** Chatbot artifacts (19), significance inflation (1), promotional language (4), AI vocabulary (7), copula avoidance (8), -ing analyses (3), vague attributions (5), filler phrases (22), generic conclusions (24)

---

## Example 2: City description

**Before (score: 65):**
> Nestled within the breathtaking Pacific Northwest, Portland stands as a vibrant city with a rich cultural heritage and stunning natural beauty. The city boasts a thriving arts scene, showcasing local talent and fostering community engagement.
>
> Portland's culinary landscape is equally impressive, featuring a diverse array of restaurants. Experts believe the city plays a crucial role in the regional economy, with industry reports suggesting continued growth. Despite its challenges, Portland continues to thrive.

**After (score: 6):**
> Portland has about 650,000 people and sits on the Willamette River, 60 miles from the Pacific coast. It rains a lot — around 155 days a year — which partly explains why there are so many coffee shops and bookstores.
>
> The food cart scene is the main draw for visitors. There are over 500 licensed food carts, more per capita than any other US city. The tech sector grew after Intel and Nike established headquarters nearby in the 1990s.

**Patterns removed:** Promotional language (4), copula avoidance (8), -ing analyses (3), AI vocabulary (7), vague attributions (5), formulaic challenges (6)

---

## Example 3: Email draft

**Before (score: 58):**
> Dear Team,
>
> I hope this email finds you well! I wanted to take a moment to delve into our Q3 results, which serve as a testament to our team's enduring commitment to excellence.
>
> It's not just about the numbers; it's about the journey. Our performance showcases the intricate interplay between strategy and execution, highlighting our ability to navigate challenges while fostering innovation, collaboration, and growth.
>
> In order to continue this momentum, it is important to note that we need to align our efforts with the evolving landscape. The future looks bright!
>
> Let me know if you have any questions. I hope this helps!

**After (score: 3):**
> Hi team,
>
> Q3 results are in. Revenue hit $2.4M, up 12% from Q2. The Harris account drove most of that — they expanded their contract in August.
>
> Two things to watch for Q4: the enterprise pipeline is thinner than I'd like, and we still haven't backfilled the two sales roles. I'd like to have candidates by mid-October.
>
> Full deck is attached. Let's discuss at Thursday's standup.

**Patterns removed:** Chatbot artifacts (19), AI vocabulary (7), significance inflation (1), copula avoidance (8), -ing analyses (3), negative parallelisms (9), filler phrases (22), generic conclusions (24)

---

## Example 4: Blog post intro

**Before (score: 45):**
> In today's rapidly evolving world of artificial intelligence, the landscape of machine learning continues to transform at an unprecedented pace. This groundbreaking technology has garnered significant attention from industry leaders and researchers alike, showcasing its potential to revolutionize various sectors.
>
> Furthermore, the multifaceted nature of deep learning encompasses a myriad of applications, from natural language processing to computer vision, highlighting the comprehensive scope of this transformative paradigm.

**After (score: 0):**
> Machine learning models got noticeably better in 2024. GPT-4o handles images and audio. Claude writes code that usually works on the first try. Stable Diffusion 3 makes hands that look right.
>
> The practical question isn't whether the technology works — it's whether it works well enough for your specific use case, and at what cost. A fine-tuned model for classifying support tickets costs a few hundred dollars and saves real time. A custom LLM to replace your writing staff probably doesn't.

**Patterns removed:** Filler (22), AI vocabulary (7), promotional language (4), -ing analyses (3), false ranges (12)


path: docs/PATTERNS.md
content: # Pattern documentation

Detailed technical documentation for all 24 AI writing patterns detected by humanizer.

## How detection works

Each pattern has a `detect(text)` function that returns an array of matches. Detection uses:

- **Regex matching** for vocabulary words, phrases, and structural patterns
- **Density analysis** for patterns that depend on frequency (em dashes, AI vocab)
- **Heuristic checks** for structural patterns (synonym cycling, rule of three)

## Pattern weights

Patterns are weighted 1-5 based on how strongly they signal AI-generated text:

| Weight | Meaning | Patterns |
|--------|---------|----------|
| 5 | Dead giveaway | AI vocabulary (7), Chatbot artifacts (19) |
| 4 | Strong signal | Significance inflation (1), -ing analyses (3), Vague attributions (5), Cutoff disclaimers (20), Sycophantic tone (21) |
| 3 | Moderate signal | Notability (2), Promotional language (4), Formulaic challenges (6), Copula avoidance (8), Negative parallelisms (9), Inline-header lists (15), Filler phrases (22), Hedging (23), Generic conclusions (24) |
| 2 | Weak signal | Rule of three (10), Synonym cycling (11), False ranges (12), Em dash overuse (13), Boldface overuse (14), Emoji overuse (17) |
| 1 | Minor tell | Title Case headings (16), Curly quotes (18) |

## Scoring algorithm

The AI score (0-100) combines three components:

1. **Density score** (up to 65 points): Weighted matches per 100 words, on a logarithmic scale
2. **Breadth bonus** (up to 20 points): 2 points per unique pattern type detected
3. **Category bonus** (up to 15 points): 3 points per category with hits

The logarithmic curve prevents long texts from getting inflated scores just by having more words to match against.

## Adding new patterns

To add a new pattern to `src/patterns.js`:

1. Define the pattern object with `id`, `name`, `category`, `description`, `weight`, and `detect(text)` function
2. The detect function must return `{ match, index, line, column, suggestion }`
3. Use the `findMatches()` helper for regex-based detection
4. Add tests in `tests/analyzer.test.js`
5. Document in `references/patterns.md`

Example:

```javascript
{
  id: 25,
  name: 'New pattern name',
  category: 'content', // or language, style, communication, filler
  description: 'What this pattern is and why it matters.',
  weight: 3,
  detect(text) {
    const regex = /your-pattern-here/gi;
    return findMatches(text, regex, 'Suggestion for fixing this pattern.');
  },
}
```


path: eslint.config.js
content: const js = require('@eslint/js');
const {
  baseRules,
  testRules,
  nodeGlobals,
  testGlobals,
  ignores,
} = require('../_config/eslint.base.js');

module.exports = [
  js.configs.recommended,
  {
    files: ['src/**/*.js'],
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: 'commonjs',
      globals: nodeGlobals,
    },
    rules: baseRules,
  },
  {
    files: ['tests/**/*.js'],
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: 'module',
      globals: testGlobals,
    },
    rules: testRules,
  },
  { ignores },
];


path: package.json
content: {
  "name": "humanizer",
  "version": "2.1.0",
  "description": "OpenClaw AI Humanizer - detect and remove AI writing patterns. Scans text for 24 common signs of AI-generated writing with statistical analysis (burstiness, TTR, readability) and provides actionable suggestions to make it sound natural and human.",
  "main": "src/analyzer.js",
  "bin": {
    "humanizer": "src/cli.js"
  },
  "scripts": {
    "start": "node src/cli.js",
    "test": "vitest run",
    "test:watch": "vitest",
    "lint": "eslint src/ tests/",
    "lint:fix": "eslint src/ tests/ --fix",
    "format": "prettier --write 'src/**/*.js' 'tests/**/*.js'",
    "format:check": "prettier --check 'src/**/*.js' 'tests/**/*.js'",
    "check": "npm run lint && npm run format:check && npm test",
    "build": "echo 'No build step needed — pure Node.js'"
  },
  "keywords": [
    "ai",
    "writing",
    "humanize",
    "llm",
    "detection",
    "text-analysis",
    "openclaw",
    "skill",
    "burstiness",
    "type-token-ratio",
    "flesch-kincaid",
    "ai-detection"
  ],
  "author": "Brandon Wise",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/brandonwise/humanizer"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.2",
    "eslint": "^9.0.0",
    "prettier": "^3.0.0",
    "vitest": "^3.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


path: references/ai-vocabulary.md
content: # AI vocabulary — words and phrases to avoid

Words and phrases that appear far more frequently in AI-generated text than in human writing. Organized by frequency and category.

## High-frequency AI words

These are the strongest signals. They appear in AI text at rates 5-20x higher than pre-2023 writing.

| Word | Why it's a signal | Better alternative |
|------|-------------------|-------------------|
| additionally | Overused transitional word | also, and, (just start the sentence) |
| delve | Almost never used in casual writing | explore, dig into, look at |
| tapestry (abstract) | "rich tapestry of culture" — meaningless | (describe what you actually mean) |
| testament | "a testament to X" — inflated significance | (remove, state the fact directly) |
| underscore | "underscores the importance of" | shows, makes clear, proves |
| pivotal | "pivotal moment/role" — everything is pivotal | important, key, (often removable) |
| landscape (abstract) | "the evolving landscape of" | field, area, industry, (be specific) |
| intricate/intricacies | Fake-depth modifier | complex, detailed, tricky |
| showcasing/showcase | "showcasing innovation" | showing, displaying, demonstrating |
| fostering/foster | "fostering collaboration" | encouraging, building, supporting |
| garner/garnered | "garnered attention" | got, received, attracted |
| interplay | "the interplay between X and Y" | relationship, tension, connection |
| enduring | "enduring legacy" — inflated | lasting, ongoing |
| vibrant | "vibrant community/culture" — generic | (describe what makes it vibrant) |
| crucial | "plays a crucial role" | important, key, essential |
| enhance/enhanced | "enhanced the experience" | improved, strengthened, helped |

## Medium-frequency AI words

Less individually damning, but suspicious when several appear together.

| Word | Better alternative |
|------|-------------------|
| furthermore, moreover | and, also |
| notably | (usually removable) |
| comprehensive | thorough, complete, full |
| multifaceted | complex, varied |
| nuanced | subtle, complicated |
| paradigm | model, approach, framework |
| transformative | (be specific about the change) |
| leveraging/leverage | using |
| synergy | cooperation, combined effect |
| holistic | complete, full, whole |
| robust | strong, solid, thorough |
| streamline/streamlined | simplify, speed up |
| utilize/utilizing | use |
| facilitate/facilitated | help, enable, make possible |
| elucidate/illuminate | explain, clarify |
| encompassing | including, covering |
| spearhead/spearheading | lead, drive |
| invaluable | valuable, essential |
| groundbreaking | new, novel, first |
| cutting-edge | new, latest, modern |
| bolster/bolstering | support, strengthen |
| catalyze/catalyst | trigger, cause, start |
| cornerstone | foundation, basis |
| reimagine/reimagining | rethink, redesign |
| empower/empowering | enable, help, give power to |
| harness/harnessing | use, take advantage of |
| navigate/navigating | handle, deal with, work through |
| realm | field, area, domain |
| poised | ready, set, positioned |
| myriad | many, numerous, lots of |

## Promotional phrases

These read like tourism brochures or press releases.

- "nestled in/within"
- "in the heart of"
- "breathtaking"
- "must-visit"
- "stunning"
- "renowned"
- "world-class"
- "state-of-the-art"
- "seamless"
- "game-changing"
- "unparalleled"
- "rich cultural heritage"
- "natural beauty"
- "commitment to excellence"

## Significance phrases

Claims of importance that add nothing.

- "marking a pivotal moment"
- "is a testament to"
- "serves as a reminder"
- "underscores the importance"
- "reflects broader trends"
- "setting the stage for"
- "key turning point"
- "evolving landscape"
- "indelible mark"
- "shaping the future of"
- "the evolution of"

## Filler phrases

Wordy constructions with simpler alternatives.

| Filler | Replacement |
|--------|-------------|
| in order to | to |
| due to the fact that | because |
| at this point in time | now |
| in the event that | if |
| has the ability to | can |
| it is important to note that | (remove) |
| it is worth noting that | (remove) |
| it should be noted that | (remove) |
| in today's rapidly evolving | (remove or be specific) |
| at the end of the day | (remove) |
| when it comes to | for |
| the fact of the matter is | (remove) |
| in terms of | for, about |
| for the purpose of | to, for |
| in light of the fact that | because, since |
| in the realm of | in |
| at its core | (remove or be specific) |
| first and foremost | first |
| last but not least | finally |

## Chatbot phrases

Leftover conversational filler from AI chat interactions.

- "I hope this helps!"
- "Let me know if you'd like..."
- "Would you like me to..."
- "Feel free to..."
- "Don't hesitate to..."
- "Happy to help!"
- "Here is an overview/summary/breakdown"
- "Of course!"
- "Certainly!"
- "Absolutely!"
- "I'd be happy to..."
- "Is there anything else..."
- "Great question!"
- "Excellent point!"
- "You're absolutely right!"

## Hedging stacks

Multiple qualifiers where one (or zero) would do.

- "could potentially"
- "might possibly"
- "could possibly"
- "perhaps potentially"
- "may potentially"
- "it could be argued"
- "one could argue"
- "it is possible that"

## Generic conclusion phrases

Vague upbeat endings.

- "the future looks bright"
- "exciting times lie ahead"
- "continue this journey"
- "journey toward excellence"
- "step in the right direction"
- "only time will tell"
- "the possibilities are endless"
- "poised for growth"
- "watch this space"


path: references/patterns.md
content: # AI writing patterns — full catalog

Comprehensive reference for all 24 AI writing patterns detected by humanizer. Each entry includes the pattern description, detection signals, and before/after examples.

## Content patterns (1-6)

### 1. Significance inflation

LLMs puff up the importance of mundane things with claims about legacy, evolution, and broader trends.

**Signals:** "pivotal moment", "testament to", "vital/crucial/significant role", "evolving landscape", "setting the stage for", "indelible mark", "deeply rooted", "shaping the future"

**Before:**
> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain.

**After:**
> The Statistical Institute of Catalonia was established in 1989 to collect and publish regional statistics independently from Spain's national statistics office.

---

### 2. Notability name-dropping

LLMs list media outlets and sources to claim notability without providing specific claims or context.

**Signals:** Comma-separated lists of publications, "active social media presence", "written by a leading expert"

**Before:**
> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu. She maintains an active social media presence with over 500,000 followers.

**After:**
> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods.

---

### 3. Superficial -ing analyses

AI tacks present participle phrases onto sentences to fake analytical depth.

**Signals:** Trailing clauses starting with: highlighting, underscoring, emphasizing, ensuring, reflecting, symbolizing, contributing to, cultivating, fostering, encompassing, showcasing

**Before:**
> The temple's color palette resonates with the region's natural beauty, symbolizing Texas bluebonnets, reflecting the community's deep connection to the land.

**After:**
> The temple uses blue, green, and gold. The architect said these reference local bluebonnets and the Gulf coast.

---

### 4. Promotional language

Tourism-brochure language that sounds like ad copy rather than neutral description.

**Signals:** "nestled", "breathtaking", "stunning", "renowned", "groundbreaking", "must-visit", "in the heart of", "rich cultural heritage", "world-class", "unparalleled"

**Before:**
> Nestled within the breathtaking region of Gonder, this vibrant town boasts a rich cultural heritage and stunning natural beauty.

**After:**
> Alamata Raya Kobo is a town in the Gonder region of Ethiopia, known for its weekly market and 18th-century church.

---

### 5. Vague attributions

Attributing claims to unnamed experts, studies, or reports instead of specific sources.

**Signals:** "Experts believe", "Industry reports suggest", "Studies show", "Observers have noted", "widely regarded"

**Before:**
> Experts believe it plays a crucial role in the regional ecosystem.

**After:**
> The river supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences.

---

### 6. Formulaic challenges

Boilerplate "despite challenges" sections that follow a predictable template.

**Signals:** "Despite its challenges", "continues to thrive", "future outlook remains", "challenges typical of"

**Before:**
> Despite its industrial prosperity, the area faces challenges typical of urban areas. Despite these challenges, it continues to thrive.

**After:**
> Traffic congestion increased after 2015 when three new IT parks opened. A stormwater drainage project began in 2022 to address recurring floods.

---

## Language patterns (7-12)

### 7. AI vocabulary

Words that appear far more frequently in AI-generated text than human writing.

**High-frequency words:** additionally, delve, tapestry, testament, underscore, pivotal, landscape (abstract), intricate/intricacies, showcasing, fostering, garner, interplay, enduring, vibrant, crucial, enhance

**Medium-frequency words:** furthermore, moreover, notably, comprehensive, multifaceted, nuanced, paradigm, transformative, leveraging, synergy, holistic, robust, streamline, utilize, facilitate, elucidate, encompassing, cornerstone, reimagine, empower, harness, navigate, realm, poised, myriad

**Before:**
> Additionally, a testament to Italian colonial influence is the widespread adoption of pasta in the local culinary landscape, showcasing integration into the traditional diet.

**After:**
> Pasta dishes, introduced during Italian colonization, remain common, especially in the south.

---

### 8. Copula avoidance

Using elaborate constructions where simple "is" or "has" works better.

**Signals:** "serves as", "stands as", "functions as", "boasts", "features" (as replacement for "has")

**Before:**
> Gallery 825 serves as LAAA's exhibition space. The gallery features four spaces and boasts over 3,000 square feet.

**After:**
> Gallery 825 is LAAA's exhibition space. The gallery has four rooms totaling 3,000 square feet.

---

### 9. Negative parallelisms

"Not just X, it's Y" and "Not only X but also Y" — overused rhetorical frame.

**Before:**
> It's not just about the beat; it's part of the aggression. It's not merely a song, it's a statement.

**After:**
> The heavy beat adds to the aggressive tone.

---

### 10. Rule of three

Forcing ideas into groups of three to sound comprehensive.

**Before:**
> Attendees can expect innovation, inspiration, and industry insights.

**After:**
> The event includes talks, panels, and informal networking.

---

### 11. Synonym cycling

Referring to the same thing by different names in consecutive sentences.

**Before:**
> The protagonist faces many challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home.

**After:**
> The protagonist faces many challenges but eventually triumphs and returns home.

---

### 12. False ranges

"From X to Y" where X and Y aren't on a meaningful scale.

**Before:**
> Our journey has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth of stars to the dance of dark matter.

**After:**
> The book covers the Big Bang, star formation, and current theories about dark matter.

---

## Style patterns (13-18)

### 13. Em dash overuse

LLMs use em dashes more than humans, mimicking punchy sales writing.

**Before:**
> The term is promoted by Dutch institutions — not by the people themselves. You don't say "Netherlands, Europe" — yet this continues — even in official documents.

**After:**
> The term is promoted by Dutch institutions, not by the people. This mislabeling continues in official documents.

---

### 14. Boldface overuse

Mechanical emphasis of phrases in bold throughout the text.

**Before:**
> It blends **OKRs**, **KPIs**, and tools like the **Business Model Canvas** and **Balanced Scorecard**.

**After:**
> It blends OKRs, KPIs, and tools like the Business Model Canvas and Balanced Scorecard.

---

### 15. Inline-header lists

List items starting with bolded headers and colons, often repeating the header word.

**Before:**
> - **User Experience:** The user experience has been improved.
> - **Performance:** Performance has been enhanced.
> - **Security:** Security has been strengthened.

**After:**
> The update improves the interface, speeds up load times, and adds end-to-end encryption.

---

### 16. Title Case headings

Capitalizing every main word in headings.

**Before:**
> ## Strategic Negotiations And Global Partnerships

**After:**
> ## Strategic negotiations and global partnerships

---

### 17. Emoji overuse

Decorating headings or bullet points with emojis in professional text.

**Before:**
> 🚀 **Launch Phase:** The product launches in Q3
> 💡 **Key Insight:** Users prefer simplicity

**After:**
> The product launches in Q3. User research showed a preference for simplicity.

---

### 18. Curly quotes

ChatGPT uses Unicode curly quotes instead of straight quotes.

**Before:**
> He said \u201Cthe project is on track\u201D

**After:**
> He said "the project is on track"

---

## Communication patterns (19-21)

### 19. Chatbot artifacts

Leftover phrases from chatbot conversations pasted into content.

**Signals:** "I hope this helps!", "Let me know if...", "Here is an overview", "Of course!", "Certainly!", "I'd be happy to"

**Before:**
> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand.

**After:**
> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest.

---

### 20. Cutoff disclaimers

AI knowledge-cutoff disclaimers left in text.

**Signals:** "As of my last training", "While specific details are limited", "Based on available information"

**Before:**
> While specific details are not extensively documented, it appears to have been established in the 1990s.

**After:**
> The company was founded in 1994, according to its registration documents.

---

### 21. Sycophantic tone

Overly positive, people-pleasing language.

**Before:**
> Great question! You're absolutely right! That's an excellent point!

**After:**
> The economic factors you mentioned are relevant here.

---

## Filler and hedging (22-24)

### 22. Filler phrases

Wordy phrases that can be shortened.

| Filler | Replacement |
|--------|-------------|
| In order to | to |
| Due to the fact that | because |
| At this point in time | now |
| In the event that | if |
| Has the ability to | can |
| It is important to note that | (remove) |
| When it comes to | for |
| For the purpose of | to |
| First and foremost | first |

---

### 23. Excessive hedging

Stacking qualifiers instead of committing to a claim.

**Before:**
> It could potentially possibly be argued that the policy might have some effect.

**After:**
> The policy may affect outcomes.

---

### 24. Generic conclusions

Vague upbeat endings that say nothing.

**Signals:** "The future looks bright", "Exciting times lie ahead", "journey toward excellence", "poised for growth", "the possibilities are endless"

**Before:**
> The future looks bright. Exciting times lie ahead as they continue their journey toward excellence.

**After:**
> The company plans to open two more locations next year.


path: references/style-guide.md
content: # Style guide — how to write like a human

Removing AI patterns is half the job. The other half is writing with personality. Sterile, voiceless text is just as obvious as slop.

## The human test

Read your text aloud. If it sounds like a press release, a Wikipedia article, or a customer service chatbot, it needs work. Good writing sounds like a specific person thinking on paper.

## Have opinions

Don't just report facts — react to them.

**Soulless:** The experiment produced interesting results. Some developers were impressed while others were skeptical.

**Human:** I genuinely don't know how to feel about this one. Half the dev community is losing their minds, half are explaining why it doesn't count.

## Vary your rhythm

Short punchy sentences. Then longer ones that take their time getting where they're going, maybe with a clause or two along the way. Mix it up.

**Monotone:** The tool works well. It processes data quickly. Users report satisfaction. The interface is clean.

**Varied:** The tool works well. It's fast — embarrassingly fast, actually, compared to the manual process we'd been limping along with for years. Users seem happy with it, though I suspect half of them haven't tried the advanced features yet.

## Acknowledge complexity

Real humans have mixed feelings. "This is impressive but also kind of unsettling" is more honest than either pure praise or pure criticism.

**AI-flat:** This is an innovative solution that addresses key challenges.

**Human:** It's clever, but it makes me nervous. The failure modes aren't well understood.

## Use first person when it fits

"I" isn't unprofessional — it's honest.

- "I keep coming back to..."
- "Here's what gets me..."
- "I've seen this go wrong when..."
- "What I don't understand is..."

## Let some mess in

Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human. Not every piece needs them, but they signal a real person behind the words.

## Be specific

"This is concerning" tells you nothing. "There's something unsettling about autonomous agents running at 3am while nobody watches" tells you everything.

Replace vague adjectives with concrete details:
- "Important city" → "Portland, population 650,000"
- "Recent study" → "A 2024 Stanford study of 500 developers"
- "Significant growth" → "Revenue grew 34% in Q3"

## Use simple verbs

| Instead of | Use |
|------------|-----|
| serves as | is |
| features | has |
| boasts | has |
| utilizes | uses |
| facilitates | helps |
| encompasses | includes |
| leverages | uses |
| garners | gets |

## Cut filler words

Every word should earn its place. If you can remove a word without changing the meaning, remove it.

| Remove | Keep |
|--------|------|
| In order to succeed | To succeed |
| Due to the fact that | Because |
| It is important to note that the data shows | The data shows |
| At this point in time | Now |
| Has the ability to process | Can process |

## One qualifier is enough

"Could potentially possibly" means the same as "could." Pick one qualifier and commit.

**Over-hedged:** It could potentially be argued that this might possibly have some positive effect.

**Direct:** The policy may help.

## End with something real

Don't reach for a platitude. End with a fact, a question, or nothing at all.

**Generic:** The future looks bright. Exciting times lie ahead.

**Specific:** The company plans to open two more locations next year. Whether that's ambitious or reckless depends on the Q4 numbers.

**Or just stop:** The company plans to open two more locations next year.

## Formatting guidelines

- Use sentence case for headings (capitalize first word and proper nouns only)
- Avoid bold emphasis in body text — let the writing carry the weight
- Don't decorate lists with emojis
- Use straight quotes, not curly quotes
- Prefer commas and periods over em dashes
- Don't use inline-header lists ("**Topic:** description of topic")
- If you need a list, keep items brief and parallel


path: scripts/analyze.sh
content: #!/usr/bin/env bash
# Quick wrapper for text analysis.
# Usage: ./scripts/analyze.sh [file] [--json] [--verbose]

set -euo pipefail
DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [[ $# -ge 1 && ! "$1" =~ ^-- ]]; then
  node "$DIR/src/cli.js" analyze -f "$@"
else
  node "$DIR/src/cli.js" analyze "$@"
fi


path: scripts/humanize.sh
content: #!/usr/bin/env bash
# Quick wrapper for humanization suggestions.
# Usage: ./scripts/humanize.sh [file] [--autofix] [--json]

set -euo pipefail
DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [[ $# -ge 1 && ! "$1" =~ ^-- ]]; then
  node "$DIR/src/cli.js" humanize -f "$@"
else
  node "$DIR/src/cli.js" humanize "$@"
fi


path: src/analyzer.js
content: /**
 * analyzer.js — Text analysis engine.
 *
 * Combines pattern detection with statistical analysis to produce a
 * comprehensive AI writing score. The score uses three signal types:
 *
 *   1. Pattern matches — vocabulary, phrases, structural patterns (24 detectors)
 *   2. Text statistics — burstiness, sentence variation, type-token ratio
 *   3. Category breadth — how many different AI signal types are present
 *
 * Based on research from:
 *   - Wikipedia:Signs of AI writing
 *   - Copyleaks stylistic fingerprint analysis (arxiv 2503.01659v1)
 *   - StyloAI 31-feature stylometric analysis
 */

const { patterns, wordCount } = require('./patterns');
const { computeStats, computeUniformityScore } = require('./stats');

// ─── Category Labels ────────────────────────────────────

const CATEGORY_LABELS = {
  content: 'Content patterns',
  language: 'Language & grammar',
  style: 'Style patterns',
  communication: 'Communication artifacts',
  filler: 'Filler & hedging',
};

// ─── Analysis Engine ─────────────────────────────────────

/**
 * Analyze text for AI writing patterns and compute statistics.
 *
 * @param {string} text  — The text to analyze
 * @param {object} opts  — Options:
 *   - verbose {boolean}     Show all matches (not just top 5 per pattern)
 *   - patternsToCheck {number[]}  Only run specific pattern IDs
 *   - includeStats {boolean}  Include full text statistics (default: true)
 *   - config {object}       Custom config overrides
 * @returns {object}     — Full analysis result
 */
function analyze(text, opts = {}) {
  const { verbose = false, patternsToCheck = null, includeStats = true } = opts;

  if (!text || typeof text !== 'string') {
    return emptyResult();
  }

  const trimmed = text.trim();
  if (trimmed.length === 0) return emptyResult();

  const words = wordCount(trimmed);

  // ── Compute text statistics ────────────────────────
  const stats = includeStats ? computeStats(trimmed) : null;
  // Only compute uniformity for text with enough structure to be meaningful
  const uniformityScore =
    stats && stats.wordCount >= 20 && stats.sentenceCount >= 3 ? computeUniformityScore(stats) : 0;

  // ── Run pattern detectors ──────────────────────────
  const findings = [];
  const categoryScores = {};
  for (const cat of Object.keys(CATEGORY_LABELS)) {
    categoryScores[cat] = { matches: 0, weightedScore: 0, patterns: [] };
  }

  const activePatterns = patternsToCheck
    ? patterns.filter((p) => patternsToCheck.includes(p.id))
    : patterns;

  for (const pattern of activePatterns) {
    const matches = pattern.detect(trimmed);
    if (matches.length > 0) {
      const finding = {
        patternId: pattern.id,
        patternName: pattern.name,
        category: pattern.category,
        description: pattern.description,
        weight: pattern.weight,
        matchCount: matches.length,
        matches: verbose ? matches : matches.slice(0, 5),
        truncated: !verbose && matches.length > 5,
      };

      findings.push(finding);
      categoryScores[pattern.category].matches += matches.length;
      categoryScores[pattern.category].weightedScore += matches.length * pattern.weight;
      categoryScores[pattern.category].patterns.push(pattern.name);
    }
  }

  // ── Calculate composite score ──────────────────────
  const patternScore = calculatePatternScore(findings, words);
  const compositeScore = calculateCompositeScore(patternScore, uniformityScore, findings);

  // ── Build category summary ─────────────────────────
  const categories = {};
  for (const [cat, label] of Object.entries(CATEGORY_LABELS)) {
    const data = categoryScores[cat];
    categories[cat] = {
      label,
      matches: data.matches,
      weightedScore: data.weightedScore,
      patternsDetected: data.patterns,
    };
  }

  const totalMatches = findings.reduce((sum, f) => sum + f.matchCount, 0);

  return {
    score: compositeScore,
    patternScore,
    uniformityScore,
    totalMatches,
    wordCount: words,
    stats,
    categories,
    findings,
    summary: buildSummary(compositeScore, totalMatches, findings, words, stats),
  };
}

// ─── Scoring ─────────────────────────────────────────────

/**
 * Pattern-based score component (0-100).
 * Uses density, breadth, and category diversity.
 */
function calculatePatternScore(findings, words) {
  if (words === 0 || findings.length === 0) return 0;

  let weightedTotal = 0;
  for (const f of findings) {
    weightedTotal += f.matchCount * f.weight;
  }

  // Density: weighted hits per 100 words (log scale)
  const density = (weightedTotal / words) * 100;
  const densityScore = Math.min(Math.log2(density + 1) * 13, 65);

  // Breadth: unique pattern types (max 20)
  const breadthBonus = Math.min(findings.length * 2, 20);

  // Category diversity (max 15)
  const categoriesHit = new Set(findings.map((f) => f.category)).size;
  const categoryBonus = Math.min(categoriesHit * 3, 15);

  return Math.min(Math.round(densityScore + breadthBonus + categoryBonus), 100);
}

/**
 * Composite score combining pattern detection and statistical analysis.
 *
 * Pattern score is the primary signal (70% weight).
 * Uniformity score adds statistical evidence (30% weight).
 * But only when both are present — stats alone aren't enough.
 */
function calculateCompositeScore(patternScore, uniformityScore, findings) {
  if (patternScore === 0 && uniformityScore === 0) return 0;

  // If no patterns detected, uniformity alone isn't enough to accuse
  if (findings.length === 0) return Math.min(Math.round(uniformityScore * 0.15), 15);

  // Weighted blend: patterns dominate, stats supplement
  const blended = patternScore * 0.7 + uniformityScore * 0.3;
  return Math.min(Math.round(blended), 100);
}

/**
 * Build human-readable summary.
 */
function buildSummary(finalScore, totalMatches, findings, words, stats) {
  if (totalMatches === 0 && finalScore < 10) {
    return 'No significant AI writing patterns detected. The text looks human-written.';
  }

  const level =
    finalScore >= 70
      ? 'heavily AI-generated'
      : finalScore >= 45
        ? 'moderately AI-influenced'
        : finalScore >= 20
          ? 'lightly AI-touched'
          : 'mostly human-sounding';

  const topPatterns = findings
    .sort((a, b) => b.matchCount * b.weight - a.matchCount * a.weight)
    .slice(0, 3)
    .map((f) => f.patternName);

  let summary = `Score: ${finalScore}/100 (${level}). Found ${totalMatches} matches across ${findings.length} pattern types in ${words} words.`;

  if (topPatterns.length > 0) {
    summary += ` Top issues: ${topPatterns.join(', ')}.`;
  }

  if (stats && stats.sentenceCount > 3) {
    if (stats.burstiness < 0.25) {
      summary += ' Sentence rhythm is very uniform (low burstiness) — typical of AI text.';
    }
    if (stats.typeTokenRatio < 0.4 && words > 100) {
      summary += ' Vocabulary diversity is low.';
    }
  }

  return summary;
}

// ─── Quick Score ─────────────────────────────────────────

/**
 * Quick score — returns just the number (0-100).
 */
function score(text) {
  return analyze(text).score;
}

// ─── Formatting ──────────────────────────────────────────

/**
 * Format analysis as human-readable terminal report.
 */
function formatReport(result) {
  const lines = [];

  lines.push('');
  lines.push('╔══════════════════════════════════════════════════╗');
  lines.push('║          AI WRITING PATTERN ANALYSIS             ║');
  lines.push('╚══════════════════════════════════════════════════╝');
  lines.push('');

  // Score bar
  const filled = Math.round(result.score / 5);
  const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
  lines.push(`  Score: ${result.score}/100  [${bar}]`);
  lines.push(
    `  Words: ${result.wordCount}  |  Matches: ${result.totalMatches}  |  Pattern: ${result.patternScore}  |  Uniformity: ${result.uniformityScore}`,
  );
  lines.push('');
  lines.push(`  ${result.summary}`);
  lines.push('');

  // Stats section
  if (result.stats) {
    const s = result.stats;
    lines.push('── Text Statistics ─────────────────────────────────');
    lines.push(`  Sentences: ${s.sentenceCount}  |  Paragraphs: ${s.paragraphCount}`);
    lines.push(`  Avg sentence length: ${s.avgSentenceLength} words (σ ${s.sentenceLengthStdDev})`);
    lines.push(`  Burstiness: ${s.burstiness} ${burstinessLabel(s.burstiness)}`);
    lines.push(
      `  Vocabulary diversity (TTR): ${s.typeTokenRatio} ${ttrLabel(s.typeTokenRatio, s.wordCount)}`,
    );
    lines.push(`  Function word ratio: ${s.functionWordRatio}`);
    lines.push(`  Trigram repetition: ${s.trigramRepetition}`);
    lines.push(`  Readability (FK grade): ${s.fleschKincaid}`);
    lines.push('');
  }

  // Category breakdown
  lines.push('── Categories ──────────────────────────────────────');
  for (const [, data] of Object.entries(result.categories)) {
    if (data.matches > 0) {
      lines.push(`  ${data.label}: ${data.matches} matches (${data.patternsDetected.join(', ')})`);
    }
  }
  lines.push('');

  // Findings detail
  if (result.findings.length > 0) {
    lines.push('── Findings ────────────────────────────────────────');
    for (const finding of result.findings) {
      lines.push('');
      lines.push(
        `  [${finding.patternId}] ${finding.patternName} (×${finding.matchCount}, weight: ${finding.weight})`,
      );
      lines.push(`      ${finding.description}`);
      for (const match of finding.matches) {
        const loc = match.line ? `L${match.line}:${match.column || ''}` : '';
        const preview =
          typeof match.match === 'string'
            ? match.match.substring(0, 80) + (match.match.length > 80 ? '...' : '')
            : '';
        const conf = match.confidence ? ` [${match.confidence}]` : '';
        lines.push(`      ${loc}: "${preview}"${conf}`);
        if (match.suggestion) {
          lines.push(`            → ${match.suggestion}`);
        }
      }
      if (finding.truncated) {
        lines.push(`      ... and ${finding.matchCount - finding.matches.length} more`);
      }
    }
  }

  lines.push('');
  lines.push('════════════════════════════════════════════════════');
  return lines.join('\n');
}

/**
 * Format analysis as markdown report.
 */
function formatMarkdown(result) {
  const lines = [];

  lines.push('# AI writing pattern analysis');
  lines.push('');
  lines.push(`**Score: ${result.score}/100** — ${scoreLabel(result.score)}`);
  lines.push('');
  lines.push(
    `Words: ${result.wordCount} | Matches: ${result.totalMatches} | Pattern score: ${result.patternScore} | Uniformity score: ${result.uniformityScore}`,
  );
  lines.push('');
  lines.push(result.summary);
  lines.push('');

  if (result.stats) {
    const s = result.stats;
    lines.push('## Text statistics');
    lines.push('');
    lines.push('| Metric | Value | Assessment |');
    lines.push('|--------|-------|------------|');
    lines.push(
      `| Avg sentence length | ${s.avgSentenceLength} words | ${s.avgSentenceLength > 25 ? 'Long' : s.avgSentenceLength < 12 ? 'Short' : 'Normal'} |`,
    );
    lines.push(
      `| Sentence variation | σ ${s.sentenceLengthStdDev} | ${s.sentenceLengthStdDev > 8 ? 'High (human-like)' : s.sentenceLengthStdDev < 4 ? 'Low (AI-like)' : 'Moderate'} |`,
    );
    lines.push(`| Burstiness | ${s.burstiness} | ${burstinessLabel(s.burstiness)} |`);
    lines.push(
      `| Vocabulary diversity | ${s.typeTokenRatio} | ${ttrLabel(s.typeTokenRatio, s.wordCount)} |`,
    );
    lines.push(
      `| Trigram repetition | ${s.trigramRepetition} | ${s.trigramRepetition > 0.1 ? 'High (AI-like)' : 'Normal'} |`,
    );
    lines.push(
      `| Readability | FK grade ${s.fleschKincaid} | ${s.fleschKincaid > 12 ? 'Academic' : s.fleschKincaid > 8 ? 'Standard' : 'Easy'} |`,
    );
    lines.push('');
  }

  if (result.findings.length > 0) {
    lines.push('## Findings');
    lines.push('');
    for (const finding of result.findings) {
      lines.push(`### ${finding.patternId}. ${finding.patternName} (×${finding.matchCount})`);
      lines.push(`*${finding.description}*`);
      lines.push('');
      for (const match of finding.matches) {
        const loc = match.line ? `Line ${match.line}` : '';
        lines.push(
          `- ${loc}: \`${typeof match.match === 'string' ? match.match.substring(0, 80) : ''}\``,
        );
        if (match.suggestion) lines.push(`  - ${match.suggestion}`);
      }
      lines.push('');
    }
  }

  return lines.join('\n');
}

/**
 * Format analysis as JSON.
 */
function formatJSON(result) {
  return JSON.stringify(result, null, 2);
}

// ─── Label Helpers ───────────────────────────────────────

function scoreLabel(s) {
  if (s >= 70) return 'Heavily AI-generated';
  if (s >= 45) return 'Moderately AI-influenced';
  if (s >= 20) return 'Lightly AI-touched';
  return 'Mostly human-sounding';
}

function burstinessLabel(b) {
  if (b >= 0.7) return '(high — human-like)';
  if (b >= 0.45) return '(moderate)';
  if (b >= 0.25) return '(low — somewhat uniform)';
  return '(very low — AI-like uniformity)';
}

function ttrLabel(ttr, wc) {
  if (wc < 100) return '(too short to assess)';
  if (ttr >= 0.6) return '(high — diverse vocabulary)';
  if (ttr >= 0.45) return '(moderate)';
  return '(low — repetitive vocabulary)';
}

function emptyResult() {
  return {
    score: 0,
    patternScore: 0,
    uniformityScore: 0,
    totalMatches: 0,
    wordCount: 0,
    stats: null,
    categories: {},
    findings: [],
    summary: 'No text provided.',
  };
}

// ─── Exports ─────────────────────────────────────────────

module.exports = {
  analyze,
  score,
  calculatePatternScore,
  calculateCompositeScore,
  formatReport,
  formatMarkdown,
  formatJSON,
  CATEGORY_LABELS,
};


path: src/cli.js
content: #!/usr/bin/env node

/**
 * cli.js — Command-line interface for the humanizer.
 *
 * Usage:
 *   humanizer analyze <file>                # Full analysis report
 *   humanizer score <file>                  # Just the score (0-100)
 *   humanizer humanize <file>               # Humanization suggestions
 *   humanizer report <file>                 # Full markdown report
 *   humanizer suggest <file>                # Suggestions grouped by priority
 *   humanizer stats <file>                  # Statistical analysis only
 *   humanizer analyze --json < input.txt    # JSON output
 *   humanizer analyze -f file.txt           # Read from file
 *   echo "text" | humanizer score           # Pipe text
 *
 * @module cli
 */

const fs = require('fs');
const { analyze, score, formatMarkdown, formatJSON } = require('./analyzer');
const { humanize, formatSuggestions } = require('./humanizer');
const { computeStats } = require('./stats');

// ─── Tiny Color Helper (no chalk dependency) ─────────────

/**
 * ANSI escape code helpers for terminal coloring.
 * Disables color when stdout is not a TTY or NO_COLOR is set.
 *
 * @namespace color
 */
const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;

const color = {
  /** @param {string} s */
  red: (s) => (supportsColor ? `\x1b[31m${s}\x1b[0m` : s),
  /** @param {string} s */
  green: (s) => (supportsColor ? `\x1b[32m${s}\x1b[0m` : s),
  /** @param {string} s */
  yellow: (s) => (supportsColor ? `\x1b[33m${s}\x1b[0m` : s),
  /** @param {string} s */
  blue: (s) => (supportsColor ? `\x1b[34m${s}\x1b[0m` : s),
  /** @param {string} s */
  magenta: (s) => (supportsColor ? `\x1b[35m${s}\x1b[0m` : s),
  /** @param {string} s */
  cyan: (s) => (supportsColor ? `\x1b[36m${s}\x1b[0m` : s),
  /** @param {string} s */
  gray: (s) => (supportsColor ? `\x1b[90m${s}\x1b[0m` : s),
  /** @param {string} s */
  bold: (s) => (supportsColor ? `\x1b[1m${s}\x1b[0m` : s),
  /** @param {string} s */
  dim: (s) => (supportsColor ? `\x1b[2m${s}\x1b[0m` : s),
};

/**
 * Get a colored score badge based on score value.
 *
 * @param {number} s - Score value 0-100
 * @returns {string} Colored badge string
 */
function scoreBadge(s) {
  if (s <= 25) return color.green(`🟢 ${s}/100`);
  if (s <= 50) return color.yellow(`🟡 ${s}/100`);
  if (s <= 75) return color.magenta(`🟠 ${s}/100`);
  return color.red(`🔴 ${s}/100`);
}

/**
 * Get a score label based on score value.
 *
 * @param {number} s - Score value 0-100
 * @returns {string} Human-readable label
 */
function scoreLabel(s) {
  if (s <= 19) return 'Mostly human-sounding';
  if (s <= 44) return 'Lightly AI-touched';
  if (s <= 69) return 'Moderately AI-influenced';
  return 'Heavily AI-generated';
}

// ─── CLI Arg Parsing ─────────────────────────────────────

const args = process.argv.slice(2);
const command = args[0];

const flags = {
  json: args.includes('--json'),
  verbose: args.includes('--verbose') || args.includes('-v'),
  autofix: args.includes('--autofix'),
  help: args.includes('--help') || args.includes('-h'),
  file: null,
  patterns: null,
  threshold: null,
  config: null,
};

// Parse -f / --file flag
const fileIdx = args.indexOf('-f') !== -1 ? args.indexOf('-f') : args.indexOf('--file');
if (fileIdx !== -1 && args[fileIdx + 1]) {
  flags.file = args[fileIdx + 1];
}

// Parse positional file argument (command <file>)
if (!flags.file && args[1] && !args[1].startsWith('-')) {
  const commands = ['analyze', 'score', 'humanize', 'report', 'suggest', 'stats'];
  if (!commands.includes(args[1])) {
    flags.file = args[1];
  }
}

// Parse --patterns flag (comma-separated pattern IDs)
const patIdx = args.indexOf('--patterns');
if (patIdx !== -1 && args[patIdx + 1]) {
  flags.patterns = args[patIdx + 1]
    .split(',')
    .map(Number)
    .filter((n) => n > 0);
}

// Parse --threshold flag
const threshIdx = args.indexOf('--threshold');
if (threshIdx !== -1 && args[threshIdx + 1]) {
  flags.threshold = parseInt(args[threshIdx + 1], 10);
}

// Parse --config flag
const configIdx = args.indexOf('--config');
if (configIdx !== -1 && args[configIdx + 1]) {
  flags.config = args[configIdx + 1];
}

// ─── Help ────────────────────────────────────────────────

/**
 * Display CLI help text.
 */
function showHelp() {
  console.log(`
${color.bold('humanizer')} — Detect and remove AI writing patterns

${color.bold('Usage:')}
  humanizer <command> [file] [options]

${color.bold('Commands:')}
  ${color.cyan('analyze')}      Full analysis report with pattern matches
  ${color.cyan('score')}        Quick score (0-100, higher = more AI-like)
  ${color.cyan('humanize')}     Humanization suggestions with guidance
  ${color.cyan('report')}       Full markdown report (for piping to files)
  ${color.cyan('suggest')}      Show only suggestions, grouped by priority
  ${color.cyan('stats')}        Show statistical text analysis only

${color.bold('Options:')}
  -f, --file <path>       Read text from file (otherwise reads stdin)
  --json                  Output as JSON
  --verbose, -v           Show all matches (not just top 5 per pattern)
  --autofix               Apply safe mechanical fixes (humanize only)
  --patterns <ids>        Only check specific pattern IDs (comma-separated)
  --threshold <n>         Only show patterns with weight above threshold
  --config <file>         Custom config file (JSON)
  --help, -h              Show this help

${color.bold('Examples:')}
  ${color.gray('# Quick score')}
  echo "This is a testament to..." | humanizer score

  ${color.gray('# Analyze a file')}
  humanizer analyze essay.txt

  ${color.gray('# Full markdown report')}
  humanizer report article.txt > report.md

  ${color.gray('# Just suggestions')}
  humanizer suggest article.txt

  ${color.gray('# Statistical analysis')}
  humanizer stats essay.txt

  ${color.gray('# Humanize with auto-fixes')}
  humanizer humanize --autofix -f article.txt

${color.bold('Score badges:')}
  🟢 0-25    Mostly human-sounding
  🟡 26-50   Lightly AI-touched
  🟠 51-75   Moderately AI-influenced
  🔴 76-100  Heavily AI-generated
`);
}

// ─── Read Input ──────────────────────────────────────────

/**
 * Read input text from file or stdin.
 *
 * @returns {Promise<string>} The input text
 */
function readInput() {
  return new Promise((resolve, reject) => {
    if (flags.file) {
      try {
        const text = fs.readFileSync(flags.file, 'utf-8');
        resolve(text);
      } catch (err) {
        reject(new Error(`Could not read file: ${flags.file} (${err.message})`));
      }
      return;
    }

    if (process.stdin.isTTY) {
      reject(new Error('No input. Pipe text or use -f <file>. Run with --help for usage.'));
      return;
    }

    let data = '';
    process.stdin.setEncoding('utf-8');
    process.stdin.on('data', (chunk) => {
      data += chunk;
    });
    process.stdin.on('end', () => resolve(data));
    process.stdin.on('error', reject);
  });
}

// ─── Stats Formatter ─────────────────────────────────────

/**
 * Format text statistics as a terminal report.
 *
 * @param {object} stats - Stats object from computeStats()
 * @returns {string} Formatted report
 */
function formatStatsReport(stats) {
  const lines = [];

  lines.push('');
  lines.push(color.bold('  ┌──────────────────────────────────────────────┐'));
  lines.push(color.bold('  │          TEXT STATISTICS ANALYSIS             │'));
  lines.push(color.bold('  └──────────────────────────────────────────────┘'));
  lines.push('');

  lines.push(color.bold('  ── Sentences ──────────────────────────────────'));
  lines.push(`    Count:            ${stats.sentenceCount}`);
  lines.push(`    Avg length:       ${stats.avgSentenceLength} words`);
  lines.push(`    Std deviation:    ${stats.sentenceLengthStdDev}`);
  lines.push(`    Burstiness:       ${stats.burstiness}  ${burstLabel(stats.burstiness)}`);
  lines.push('');

  lines.push(color.bold('  ── Vocabulary ─────────────────────────────────'));
  lines.push(`    Total words:      ${stats.wordCount}`);
  lines.push(`    Unique words:     ${stats.uniqueWordCount}`);
  lines.push(
    `    Type-token ratio: ${stats.typeTokenRatio}  ${ttrLabel(stats.typeTokenRatio, stats.wordCount)}`,
  );
  lines.push(`    Avg word length:  ${stats.avgWordLength}`);
  lines.push('');

  lines.push(color.bold('  ── Structure ──────────────────────────────────'));
  lines.push(`    Paragraphs:       ${stats.paragraphCount}`);
  lines.push(`    Avg para length:  ${stats.avgParagraphLength} words`);
  lines.push(`    Trigram repeat:   ${stats.trigramRepetition}`);
  lines.push('');

  lines.push(color.bold('  ── Readability ────────────────────────────────'));
  lines.push(`    Flesch-Kincaid:   ${stats.fleschKincaid} grade level`);
  lines.push(
    `    Function words:   ${stats.functionWordRatio} (${(stats.functionWordRatio * 100).toFixed(1)}%)`,
  );
  lines.push('');

  return lines.join('\n');
}

/**
 * Get burstiness label.
 *
 * @param {number} b
 * @returns {string}
 */
function burstLabel(b) {
  if (b >= 0.7) return color.green('(high — human-like)');
  if (b >= 0.45) return color.yellow('(moderate)');
  if (b >= 0.25) return color.yellow('(low — somewhat uniform)');
  return color.red('(very low — AI-like)');
}

/**
 * Get type-token ratio label.
 *
 * @param {number} ttr
 * @param {number} wc
 * @returns {string}
 */
function ttrLabel(ttr, wc) {
  if (wc < 100) return color.gray('(too short to assess)');
  if (ttr >= 0.6) return color.green('(high — diverse)');
  if (ttr >= 0.45) return color.yellow('(moderate)');
  return color.red('(low — repetitive)');
}

// ─── Colored Report Formatter ────────────────────────────

/**
 * Format analysis with enhanced terminal formatting and colors.
 *
 * @param {object} result - Analysis result from analyze()
 * @returns {string} Colored terminal report
 */
function formatColoredReport(result) {
  const lines = [];

  lines.push('');
  lines.push(color.bold('  ┌──────────────────────────────────────────────┐'));
  lines.push(color.bold('  │        AI WRITING PATTERN ANALYSIS           │'));
  lines.push(color.bold('  └──────────────────────────────────────────────┘'));
  lines.push('');

  // Score bar with color
  const filled = Math.round(result.score / 5);
  const barColor =
    result.score <= 25
      ? color.green
      : result.score <= 50
        ? color.yellow
        : result.score <= 75
          ? color.magenta
          : color.red;
  const bar = barColor('█'.repeat(filled)) + color.dim('░'.repeat(20 - filled));
  lines.push(`  Score: ${scoreBadge(result.score)}  [${bar}]`);
  lines.push(
    `  ${color.dim(`Words: ${result.wordCount}  |  Matches: ${result.totalMatches}  |  Pattern: ${result.patternScore}  |  Uniformity: ${result.uniformityScore}`)}`,
  );
  lines.push('');
  lines.push(`  ${result.summary}`);
  lines.push('');

  // Statistics
  if (result.stats) {
    const s = result.stats;
    lines.push(color.bold('  ── Statistics ──────────────────────────────────'));
    lines.push(`  Burstiness: ${s.burstiness}  ${burstLabel(s.burstiness)}`);
    lines.push(
      `  Type-token ratio: ${s.typeTokenRatio}  ${ttrLabel(s.typeTokenRatio, s.wordCount)}`,
    );
    lines.push(`  Trigram repetition: ${s.trigramRepetition}`);
    lines.push(`  Readability: ${s.fleschKincaid} grade level`);
    lines.push('');
  }

  // Category breakdown
  lines.push(color.bold('  ── Categories ──────────────────────────────────'));
  for (const [, data] of Object.entries(result.categories)) {
    if (data.matches > 0) {
      lines.push(
        `  ${color.cyan(data.label)}: ${data.matches} matches ${color.dim(`(${data.patternsDetected.join(', ')})`)}`,
      );
    }
  }
  lines.push('');

  // Findings detail
  if (result.findings.length > 0) {
    lines.push(color.bold('  ── Findings ──────────────────────────────────'));
    for (const finding of result.findings) {
      if (flags.threshold && finding.weight < flags.threshold) continue;

      lines.push('');
      const weightColor =
        finding.weight >= 4 ? color.red : finding.weight >= 2 ? color.yellow : color.blue;
      lines.push(
        `  ${weightColor(`[${finding.patternId}]`)} ${color.bold(finding.patternName)} ${color.dim(`(×${finding.matchCount}, weight: ${finding.weight})`)}`,
      );
      lines.push(`      ${color.dim(finding.description)}`);
      for (const match of finding.matches) {
        const loc = match.line ? `L${match.line}` : '';
        const preview =
          typeof match.match === 'string'
            ? match.match.substring(0, 80) + (match.match.length > 80 ? '...' : '')
            : '';
        lines.push(`      ${color.dim(loc)}: "${preview}"`);
        if (match.suggestion) {
          lines.push(`            ${color.green('→')} ${match.suggestion}`);
        }
      }
      if (finding.truncated) {
        lines.push(
          `      ${color.dim(`... and ${finding.matchCount - finding.matches.length} more`)}`,
        );
      }
    }
  }

  lines.push('');
  lines.push(color.dim('  ──────────────────────────────────────────────'));
  return lines.join('\n');
}

// ─── Grouped Suggestions Formatter ───────────────────────

/**
 * Format suggestions grouped by priority with color.
 *
 * @param {object} result - Humanization result from humanize()
 * @returns {string} Formatted suggestion report
 */
function formatGroupedSuggestions(result) {
  const lines = [];

  lines.push('');
  lines.push(color.bold(`  Score: ${scoreBadge(result.score)}  (${scoreLabel(result.score)})`));
  lines.push(`  ${color.dim(`${result.totalIssues} issues found in ${result.wordCount} words`)}`);
  lines.push('');

  if (result.critical.length > 0) {
    lines.push(color.red(color.bold('  ━━ CRITICAL (remove these first) ━━━━━━━━━━━━')));
    for (const s of result.critical) {
      lines.push(`  ${color.red('●')} L${s.line}: ${color.bold(s.pattern)}`);
      lines.push(`    ${color.dim(truncate(s.text, 60))}`);
      lines.push(`    ${color.green('→')} ${s.suggestion}`);
    }
    lines.push('');
  }

  if (result.important.length > 0) {
    lines.push(color.yellow(color.bold('  ━━ IMPORTANT (noticeable AI patterns) ━━━━━━━')));
    for (const s of result.important) {
      lines.push(`  ${color.yellow('●')} L${s.line}: ${color.bold(s.pattern)}`);
      lines.push(`    ${color.dim(truncate(s.text, 60))}`);
      lines.push(`    ${color.green('→')} ${s.suggestion}`);
    }
    lines.push('');
  }

  if (result.minor.length > 0) {
    lines.push(color.blue(color.bold('  ━━ MINOR (subtle tells) ━━━━━━━━━━━━━━━━━━━━')));
    for (const s of result.minor) {
      lines.push(`  ${color.blue('●')} L${s.line}: ${color.bold(s.pattern)}`);
      lines.push(`    ${color.dim(truncate(s.text, 60))}`);
      lines.push(`    ${color.green('→')} ${s.suggestion}`);
    }
    lines.push('');
  }

  if (result.guidance.length > 0) {
    lines.push(color.cyan(color.bold('  ━━ GUIDANCE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
    for (const tip of result.guidance) {
      lines.push(`  ${color.cyan('•')} ${tip}`);
    }
    lines.push('');
  }

  if (result.styleTips && result.styleTips.length > 0) {
    lines.push(color.magenta(color.bold('  ━━ STYLE TIPS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
    for (const t of result.styleTips) {
      lines.push(`  ${color.magenta('◦')} ${t.tip}`);
    }
    lines.push('');
  }

  return lines.join('\n');
}

/**
 * Truncate a string to a max length.
 *
 * @param {string} str
 * @param {number} len
 * @returns {string}
 */
function truncate(str, len) {
  if (typeof str !== 'string') return '';
  return str.length > len ? `${str.substring(0, len)}...` : str;
}

// ─── Main ────────────────────────────────────────────────

/**
 * Main CLI entry point.
 */
async function main() {
  if (flags.help || !command) {
    showHelp();
    process.exit(command ? 0 : 1);
  }

  let text;
  try {
    text = await readInput();
  } catch (err) {
    console.error(color.red(`Error: ${err.message}`));
    process.exit(1);
  }

  if (!text.trim()) {
    console.error(color.red('Error: Empty input.'));
    process.exit(1);
  }

  const opts = {
    verbose: flags.verbose,
    patternsToCheck: flags.patterns,
  };

  switch (command) {
    case 'analyze': {
      const result = analyze(text, opts);
      if (flags.json) {
        console.log(formatJSON(result));
      } else {
        console.log(formatColoredReport(result));
      }
      break;
    }

    case 'score': {
      const s = score(text);
      if (flags.json) {
        console.log(JSON.stringify({ score: s }));
      } else {
        console.log(scoreBadge(s));
      }
      break;
    }

    case 'humanize': {
      const result = humanize(text, { autofix: flags.autofix, verbose: flags.verbose });
      if (flags.json) {
        console.log(JSON.stringify(result, null, 2));
      } else {
        console.log(formatSuggestions(result));
        if (flags.autofix && result.autofix) {
          console.log(`\n${color.bold('── AUTO-FIXED TEXT ──────────────────────────────')}\n`);
          console.log(result.autofix.text);
          console.log(`\n${color.dim('════════════════════════════════════════════════')}`);
        }
      }
      break;
    }

    case 'report': {
      const result = analyze(text, { ...opts, verbose: true });
      console.log(formatMarkdown(result));
      break;
    }

    case 'suggest': {
      const result = humanize(text, { verbose: flags.verbose });
      if (flags.json) {
        console.log(JSON.stringify(result, null, 2));
      } else {
        console.log(formatGroupedSuggestions(result));
      }
      break;
    }

    case 'stats': {
      const stats = computeStats(text);
      if (flags.json) {
        console.log(JSON.stringify(stats, null, 2));
      } else {
        console.log(formatStatsReport(stats));
      }
      break;
    }

    default:
      console.error(color.red(`Unknown command: ${command}. Run with --help for usage.`));
      process.exit(1);
  }
}

main().catch((err) => {
  console.error(color.red(`Fatal: ${err.message}`));
  process.exit(1);
});


path: src/humanizer.js
content: /**
 * humanizer.js — Humanization engine.
 *
 * Takes analysis results and produces actionable rewrite suggestions.
 * Includes both:
 *   - autoFix: safe mechanical transforms (curly quotes, filler phrases, chatbot artifacts)
 *   - humanize: full suggestion report with prioritized guidance
 *
 * Humanization techniques based on 2025 research:
 *   - Sentence length variation (mix short with long)
 *   - Burstiness injection (fragments, questions, varied rhythm)
 *   - Concrete specificity (replace vague with numbers/names/dates)
 *   - First-person injection (where appropriate)
 *   - Opinion injection (humans have preferences, AI is neutral)
 */

const { analyze } = require('./analyzer');

// ─── Automatic Fixes ─────────────────────────────────────

/**
 * Apply safe, mechanical fixes that don't require judgment.
 * Only transforms where the "right" answer is unambiguous.
 *
 * @param {string} text — Input text
 * @returns {{ text: string, fixes: string[] }}
 */
function autoFix(text) {
  let result = text;
  const fixes = [];

  // Curly quotes → straight quotes
  if (/[\u201C\u201D]/.test(result)) {
    result = result.replace(/[\u201C\u201D]/g, '"');
    fixes.push('Replaced curly double quotes with straight quotes');
  }
  if (/[\u2018\u2019]/.test(result)) {
    result = result.replace(/[\u2018\u2019]/g, "'");
    fixes.push('Replaced curly single quotes with straight quotes');
  }

  // Filler phrase replacements (unambiguous)
  const safeFills = [
    { from: /\bin order to\b/gi, to: 'to', label: '"in order to" → "to"' },
    {
      from: /\bdue to the fact that\b/gi,
      to: 'because',
      label: '"due to the fact that" → "because"',
    },
    { from: /\bat this point in time\b/gi, to: 'now', label: '"at this point in time" → "now"' },
    { from: /\bin the event that\b/gi, to: 'if', label: '"in the event that" → "if"' },
    { from: /\bhas the ability to\b/gi, to: 'can', label: '"has the ability to" → "can"' },
    { from: /\bfor the purpose of\b/gi, to: 'to', label: '"for the purpose of" → "to"' },
    { from: /\bfirst and foremost\b/gi, to: 'first', label: '"first and foremost" → "first"' },
    {
      from: /\bin light of the fact that\b/gi,
      to: 'because',
      label: '"in light of the fact that" → "because"',
    },
    { from: /\bin the realm of\b/gi, to: 'in', label: '"in the realm of" → "in"' },
    { from: /\butilize\b/gi, to: 'use', label: '"utilize" → "use"' },
    { from: /\butilizing\b/gi, to: 'using', label: '"utilizing" → "using"' },
    { from: /\butilization\b/gi, to: 'use', label: '"utilization" → "use"' },
  ];

  for (const { from, to, label } of safeFills) {
    if (from.test(result)) {
      result = result.replace(from, to);
      fixes.push(label);
    }
  }

  // Chatbot artifact removal (start/end of text)
  const chatbotStart = [
    /^(Here is|Here's) (a |an |the )?(comprehensive |brief |quick )?(overview|summary|breakdown|list|guide|explanation|look)[^.]*\.\s*/i,
    /^(Of course|Certainly|Absolutely|Sure)!\s*/i,
    /^(Great|Excellent|Good|Wonderful|Fantastic) question!\s*/i,
    /^(That's|That is) a (great|excellent|good|wonderful|fantastic) (question|point)!\s*/i,
  ];
  for (const regex of chatbotStart) {
    if (regex.test(result)) {
      result = result.replace(regex, '');
      fixes.push('Removed chatbot opening artifact');
    }
  }

  const chatbotEnd = [
    /\s*(I hope this helps|Let me know if you('d| would) like|Feel free to|Don't hesitate to|Is there anything else)[^.]*[.!]\s*$/i,
    /\s*Happy to help[.!]?\s*$/i,
  ];
  for (const regex of chatbotEnd) {
    if (regex.test(result)) {
      result = result.replace(regex, '');
      fixes.push('Removed chatbot closing artifact');
    }
  }

  result = result.trim();
  return { text: result, fixes };
}

// ─── Suggestion Engine ───────────────────────────────────

/**
 * Generate humanization suggestions.
 *
 * @param {string} text    — Input text
 * @param {object} opts    — Options:
 *   - autofix {boolean}   Apply safe auto-fixes
 *   - verbose {boolean}   Show all matches
 *   - includeStats {boolean}  Include statistical suggestions
 * @returns {object}       — Suggestions report
 */
function humanize(text, opts = {}) {
  const { autofix = false, includeStats = true } = opts;

  const analysis = analyze(text, { verbose: true, includeStats });

  // Group by priority
  const critical = []; // weight 4-5: dead giveaways
  const important = []; // weight 2-3: noticeable
  const minor = []; // weight 1: subtle

  for (const finding of analysis.findings) {
    const suggestions = finding.matches.map((m) => ({
      pattern: finding.patternName,
      patternId: finding.patternId,
      category: finding.category,
      weight: finding.weight,
      text: m.match,
      line: m.line,
      column: m.column,
      suggestion: m.suggestion,
      confidence: m.confidence || 'high',
    }));

    if (finding.weight >= 4) critical.push(...suggestions);
    else if (finding.weight >= 2) important.push(...suggestions);
    else minor.push(...suggestions);
  }

  // Auto-fix
  let fixedText = null;
  let appliedFixes = [];
  if (autofix) {
    const result = autoFix(text);
    fixedText = result.text;
    appliedFixes = result.fixes;
  }

  // Build guidance (pattern-based + statistical)
  const guidance = buildGuidance(analysis);
  const styleTips = includeStats && analysis.stats ? buildStyleTips(analysis.stats) : [];

  return {
    score: analysis.score,
    patternScore: analysis.patternScore,
    uniformityScore: analysis.uniformityScore,
    wordCount: analysis.wordCount,
    totalIssues: analysis.totalMatches,
    stats: analysis.stats,
    critical,
    important,
    minor,
    autofix: autofix ? { text: fixedText, fixes: appliedFixes } : null,
    guidance,
    styleTips,
  };
}

/**
 * Build pattern-based guidance.
 */
function buildGuidance(analysis) {
  const tips = [];
  const ids = new Set(analysis.findings.map((f) => f.patternId));

  if (ids.has(1) || ids.has(4)) {
    tips.push(
      'Replace inflated/promotional language with concrete facts. What specifically happened? Give dates, numbers, names.',
    );
  }
  if (ids.has(3)) {
    tips.push(
      'Cut trailing -ing phrases. If the point matters enough to mention, give it its own sentence.',
    );
  }
  if (ids.has(5)) {
    tips.push('Name your sources. "Experts say" means nothing — who said it, when, and where?');
  }
  if (ids.has(6)) {
    tips.push(
      'Replace formulaic "despite challenges" sections with specific problems and concrete outcomes.',
    );
  }
  if (ids.has(7)) {
    tips.push(
      'Swap AI vocabulary for plainer words. "Delve" → "look at". "Tapestry" → (be specific). "Showcase" → "show".',
    );
  }
  if (ids.has(8)) {
    tips.push('Use "is" and "has" freely. "Serves as" and "boasts" are needlessly fancy.');
  }
  if (ids.has(9)) {
    tips.push('Drop "not just X, it\'s Y" frames. Just say what the thing is.');
  }
  if (ids.has(10)) {
    tips.push("Break up triads. You don't always need three of everything.");
  }
  if (ids.has(13)) {
    tips.push('Ease up on em dashes. Use commas, periods, or parentheses for variety.');
  }
  if (ids.has(14) || ids.has(15)) {
    tips.push('Strip mechanical bold formatting and inline-header lists. Let prose do the work.');
  }
  if (ids.has(17)) {
    tips.push('Remove emojis from professional text. They signal chatbot output.');
  }
  if (ids.has(19) || ids.has(21)) {
    tips.push(
      'Remove chatbot filler ("I hope this helps!", "Great question!"). Just deliver the content.',
    );
  }
  if (ids.has(20)) {
    tips.push('Delete knowledge-cutoff disclaimers. Either research it or leave it out.');
  }
  if (ids.has(22) || ids.has(23)) {
    tips.push('Trim filler and hedging. "In order to" → "to". One qualifier per claim is enough.');
  }
  if (ids.has(24)) {
    tips.push(
      'Cut generic conclusions. End with a specific fact instead of "the future looks bright".',
    );
  }

  if (analysis.score >= 50) {
    tips.push(
      "Consider rewriting from scratch. When AI patterns are this dense, patching individual phrases isn't enough — the structure itself needs rethinking.",
    );
  }

  return tips;
}

/**
 * Build statistical style tips based on text metrics.
 * These suggest structural improvements beyond word choice.
 */
function buildStyleTips(stats) {
  const tips = [];

  // Burstiness
  if (stats.burstiness < 0.25 && stats.sentenceCount > 4) {
    tips.push({
      metric: 'burstiness',
      value: stats.burstiness,
      tip: 'Sentence rhythm is very uniform. Mix short punchy sentences (3-8 words) with longer flowing ones (20+). Fragments work too. Like this.',
    });
  }

  // Sentence length variation
  if (stats.sentenceLengthVariation < 0.3 && stats.sentenceCount > 4) {
    tips.push({
      metric: 'sentenceLengthVariation',
      value: stats.sentenceLengthVariation,
      tip: `Sentences are all roughly ${Math.round(stats.avgSentenceLength)} words. Vary your rhythm — alternate between short and long.`,
    });
  }

  // Very long average sentences
  if (stats.avgSentenceLength > 28) {
    tips.push({
      metric: 'avgSentenceLength',
      value: stats.avgSentenceLength,
      tip: 'Average sentence is quite long. Break some into shorter ones. Not every thought needs a subordinate clause.',
    });
  }

  // Low vocabulary diversity
  if (stats.typeTokenRatio < 0.4 && stats.wordCount > 100) {
    tips.push({
      metric: 'typeTokenRatio',
      value: stats.typeTokenRatio,
      tip: "Vocabulary is repetitive. Try using more varied word choices — but don't synonym-cycle (that's also an AI tell).",
    });
  }

  // High trigram repetition
  if (stats.trigramRepetition > 0.1 && stats.wordCount > 100) {
    tips.push({
      metric: 'trigramRepetition',
      value: stats.trigramRepetition,
      tip: 'Repeated 3-word phrases detected. Vary your sentence structures.',
    });
  }

  // Add humanization techniques if text scores poorly
  if (tips.length >= 2) {
    tips.push({
      metric: 'general',
      value: null,
      tip: "Try the read-aloud test: read the text out loud. If it sounds weird or robotic, rewrite those parts until they sound like something you'd actually say.",
    });
    tips.push({
      metric: 'general',
      value: null,
      tip: 'Add first-person perspective where it fits: "I found", "We noticed", "In my experience". Real humans write from a point of view.',
    });
  }

  return tips;
}

// ─── Report Formatting ──────────────────────────────────

/**
 * Format humanization suggestions as readable terminal output.
 */
function formatSuggestions(result) {
  const lines = [];

  lines.push('');
  lines.push('╔══════════════════════════════════════════════════╗');
  lines.push('║           HUMANIZATION SUGGESTIONS               ║');
  lines.push('╚══════════════════════════════════════════════════╝');
  lines.push('');

  const filled = Math.round(result.score / 5);
  const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
  lines.push(`  AI Score: ${result.score}/100  [${bar}]`);
  lines.push(
    `  Issues: ${result.totalIssues}  |  Pattern: ${result.patternScore}  |  Uniformity: ${result.uniformityScore}`,
  );
  lines.push('');

  if (result.critical.length > 0) {
    lines.push('── CRITICAL (dead giveaways) ───────────────────────');
    for (const s of result.critical) {
      lines.push(`  L${s.line}: [${s.pattern}] "${truncate(s.text, 60)}" [${s.confidence}]`);
      lines.push(`       → ${s.suggestion}`);
    }
    lines.push('');
  }

  if (result.important.length > 0) {
    lines.push('── IMPORTANT (noticeable patterns) ─────────────────');
    for (const s of result.important.slice(0, 15)) {
      lines.push(`  L${s.line}: [${s.pattern}] "${truncate(s.text, 60)}"`);
      lines.push(`       → ${s.suggestion}`);
    }
    if (result.important.length > 15) {
      lines.push(`  ... and ${result.important.length - 15} more`);
    }
    lines.push('');
  }

  if (result.minor.length > 0) {
    lines.push('── MINOR (subtle tells) ────────────────────────────');
    for (const s of result.minor.slice(0, 10)) {
      lines.push(`  L${s.line}: [${s.pattern}] "${truncate(s.text, 60)}"`);
      lines.push(`       → ${s.suggestion}`);
    }
    if (result.minor.length > 10) {
      lines.push(`  ... and ${result.minor.length - 10} more`);
    }
    lines.push('');
  }

  if (result.autofix) {
    lines.push('── AUTO-FIXES APPLIED ──────────────────────────────');
    for (const fix of result.autofix.fixes) {
      lines.push(`  ✓ ${fix}`);
    }
    lines.push('');
  }

  if (result.guidance.length > 0) {
    lines.push('── GUIDANCE ────────────────────────────────────────');
    for (const tip of result.guidance) {
      lines.push(`  • ${tip}`);
    }
    lines.push('');
  }

  if (result.styleTips.length > 0) {
    lines.push('── STYLE TIPS (statistical) ────────────────────────');
    for (const t of result.styleTips) {
      const metric = t.value !== null ? ` [${t.metric}: ${t.value}]` : '';
      lines.push(`  ◦ ${t.tip}${metric}`);
    }
    lines.push('');
  }

  lines.push('════════════════════════════════════════════════════');
  return lines.join('\n');
}

function truncate(str, len) {
  if (typeof str !== 'string') return '';
  return str.length > len ? `${str.substring(0, len)}...` : str;
}

// ─── Exports ─────────────────────────────────────────────

module.exports = {
  humanize,
  autoFix,
  formatSuggestions,
  buildGuidance,
  buildStyleTips,
};


path: src/patterns.js
content: /**
 * patterns.js — AI writing pattern detection engine.
 *
 * 24 pattern detectors organized into 5 categories, with a registry
 * that supports dynamic add/remove and custom word lists.
 *
 * Architecture:
 *   - Each pattern is an object with id, name, category, description,
 *     weight (1-5), and a detect(text) function
 *   - detect() returns [{ match, index, line, column, suggestion, confidence }]
 *   - The registry holds all patterns and provides query methods
 *   - Vocabulary is sourced from vocabulary.js (500+ words/phrases)
 */

const { TIER_1, TIER_2, TIER_3, AI_PHRASES } = require('./vocabulary');
// Stats imported for cross-module analysis when needed
// const { tokenize } = require('./stats');

// ─── Helpers ─────────────────────────────────────────────

/**
 * Find all regex matches with line numbers and columns.
 * Returns [{ match, index, line, column, suggestion, confidence }]
 */
function findMatches(text, regex, suggestion, confidence = 'high') {
  const results = [];
  const lines = text.split('\n');
  let offset = 0;

  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
    const line = lines[lineNum];
    const lineRegex = new RegExp(
      regex.source,
      regex.flags.includes('g') ? regex.flags : `${regex.flags}g`,
    );
    let m;
    while ((m = lineRegex.exec(line)) !== null) {
      results.push({
        match: m[0],
        index: offset + m.index,
        line: lineNum + 1,
        column: m.index + 1,
        suggestion: typeof suggestion === 'function' ? suggestion(m[0]) : suggestion,
        confidence,
      });
    }
    offset += line.length + 1;
  }
  return results;
}

/** Count regex occurrences. */
function countMatches(text, regex) {
  const m = text.match(regex);
  return m ? m.length : 0;
}

/** Word count. */
function wordCount(text) {
  return text.trim().split(/\s+/).filter(Boolean).length;
}

// ─── Vocabulary Detection Helpers ────────────────────────

/**
 * Build a case-insensitive word-boundary regex for a word.
 * Escapes special regex chars in the word.
 */
function wordRegex(word) {
  const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  // For multi-word phrases, don't use word boundaries on internal spaces
  if (word.includes(' ')) {
    return new RegExp(`\\b${escaped}\\b`, 'gi');
  }
  return new RegExp(`\\b${escaped}\\b`, 'gi');
}

/**
 * Scan text for words from a tier list. Returns matches with word-specific suggestions.
 */
function scanWordList(text, wordList, suggestionPrefix, confidence = 'high') {
  const results = [];
  for (const word of wordList) {
    const regex = wordRegex(word);
    const matches = findMatches(
      text,
      regex,
      `${suggestionPrefix}: "${word}". Use a simpler, more specific alternative.`,
      confidence,
    );
    results.push(...matches);
  }
  return results;
}

/**
 * Scan text for AI phrases. Returns matches with phrase-specific fixes.
 */
function scanPhrases(text, phrases, tierFilter = null) {
  const results = [];
  for (const { pattern, tier, fix } of phrases) {
    if (tierFilter !== null && tier !== tierFilter) continue;
    const matches = findMatches(
      text,
      pattern,
      fix.startsWith('(') ? fix : `Replace with: ${fix}`,
      tier === 1 ? 'high' : tier === 2 ? 'medium' : 'low',
    );
    results.push(...matches);
  }
  return results;
}

// ─── Significance / Promotional Phrase Lists ─────────────
// (Kept here for patterns that need inline regex arrays)

const SIGNIFICANCE_PHRASES = [
  /marking a pivotal/gi,
  /pivotal moment/gi,
  /pivotal role/gi,
  /key role/gi,
  /crucial role/gi,
  /vital role/gi,
  /significant role/gi,
  /is a testament/gi,
  /stands as a testament/gi,
  /serves as a testament/gi,
  /serves as a reminder/gi,
  /reflects broader/gi,
  /broader trends/gi,
  /broader movement/gi,
  /evolving landscape/gi,
  /evolving world/gi,
  /setting the stage for/gi,
  /marking a shift/gi,
  /key turning point/gi,
  /indelible mark/gi,
  /deeply rooted/gi,
  /focal point/gi,
  /symbolizing its ongoing/gi,
  /enduring legacy/gi,
  /lasting impact/gi,
  /contributing to the/gi,
  /underscores the importance/gi,
  /highlights the significance/gi,
  /represents a shift/gi,
  /shaping the future/gi,
  /the evolution of/gi,
  /rich tapestry/gi,
  /rich heritage/gi,
  /stands as a beacon/gi,
  /marks a milestone/gi,
  /paving the way/gi,
  /charting a course/gi,
];

const PROMOTIONAL_WORDS = [
  /\bnestled\b/gi,
  /\bin the heart of\b/gi,
  /\bbreathtaking\b/gi,
  /\bmust-visit\b/gi,
  /\bstunning\b/gi,
  /\brenowned\b/gi,
  /\bnatural beauty\b/gi,
  /\brich cultural heritage\b/gi,
  /\brich history\b/gi,
  /\bcommitment to\b/gi,
  /\bexemplifies\b/gi,
  /\bworld-class\b/gi,
  /\bstate-of-the-art\b/gi,
  /\bgame-changing\b/gi,
  /\bgame changer\b/gi,
  /\bunparalleled\b/gi,
  /\bprofound\b/gi,
  /\bbest-in-class\b/gi,
  /\btrailblazing\b/gi,
  /\bvisionary\b/gi,
  /\bcutting-edge\b/gi,
  /\bworldwide recognition\b/gi,
];

const VAGUE_ATTRIBUTION_PHRASES = [
  /\bexperts (believe|argue|say|suggest|note|agree|contend|have noted)\b/gi,
  /\bindustry (reports|observers|experts|analysts|leaders|insiders)\b/gi,
  /\bobservers have (cited|noted|pointed out)\b/gi,
  /\bsome critics argue\b/gi,
  /\bsome experts (say|believe|suggest)\b/gi,
  /\bseveral sources\b/gi,
  /\baccording to reports\b/gi,
  /\bwidely (regarded|considered|recognized|acknowledged)\b/gi,
  /\bit is widely (known|believed|accepted)\b/gi,
  /\bmany (experts|scholars|researchers|analysts) (believe|argue|suggest)\b/gi,
  /\bstudies (show|suggest|indicate|have shown)\b/gi,
  /\bresearch (shows|suggests|indicates|has shown)\b/gi,
  /\bsources close to\b/gi,
  /\bpeople familiar with\b/gi,
];

const CHALLENGES_PHRASES = [
  /despite (its|these|the|their) (challenges|setbacks|obstacles|difficulties|limitations)/gi,
  /faces (several|many|numerous|various) challenges/gi,
  /continues to thrive/gi,
  /continues to grow/gi,
  /future (outlook|prospects) (remain|look|appear)/gi,
  /challenges and (future|legacy|opportunities)/gi,
  /despite these (challenges|hurdles|obstacles)/gi,
  /overcoming (obstacles|challenges|adversity)/gi,
  /weather(ing|ed) the storm/gi,
];

const COPULA_AVOIDANCE = [
  /\bserves as( a)?\b/gi,
  /\bstands as( a)?\b/gi,
  /\bmarks a\b/gi,
  /\brepresents a\b/gi,
  /\bboasts (a|an|over|more)\b/gi,
  /\bfeatures (a|an|over|more)\b/gi,
  /\boffers (a|an)\b/gi,
  /\bfunctions as\b/gi,
  /\bacts as( a)?\b/gi,
  /\boperates as( a)?\b/gi,
];

// ─── Pattern Definitions ─────────────────────────────────

const patterns = [
  // ── CONTENT PATTERNS (1-6) ──────────────────────────────

  {
    id: 1,
    name: 'Significance inflation',
    category: 'content',
    description:
      'Inflated claims about significance, legacy, or broader trends. LLMs puff up importance of mundane things.',
    weight: 4,
    detect(text) {
      const results = [];
      for (const regex of SIGNIFICANCE_PHRASES) {
        results.push(
          ...findMatches(
            text,
            regex,
            'Remove inflated significance claim. State concrete facts instead.',
            'high',
          ),
        );
      }
      return results;
    },
  },

  {
    id: 2,
    name: 'Notability name-dropping',
    category: 'content',
    description:
      'Listing media outlets or sources to claim notability without providing context or specific claims.',
    weight: 3,
    detect(text) {
      const mediaList =
        /\b(cited|featured|covered|mentioned|reported|published|recognized|highlighted) (in|by) .{0,20}(The New York Times|BBC|CNN|The Washington Post|The Guardian|Wired|Forbes|Reuters|Bloomberg|Financial Times|The Verge|TechCrunch|The Hindu|Al Jazeera|Time|Newsweek|The Economist|Nature|Science).{0,100}(,\s*(and\s+)?(The New York Times|BBC|CNN|The Washington Post|The Guardian|Wired|Forbes|Reuters|Bloomberg|Financial Times|The Verge|TechCrunch|The Hindu|Al Jazeera|Time|Newsweek|The Economist|Nature|Science))+/gi;
      const results = findMatches(
        text,
        mediaList,
        'Instead of listing outlets, cite one specific claim from one source.',
        'high',
      );
      results.push(
        ...findMatches(
          text,
          /\bactive social media presence\b/gi,
          'Remove — not meaningful without specific context.',
          'high',
        ),
      );
      results.push(
        ...findMatches(
          text,
          /\bwritten by a leading expert\b/gi,
          'Name the expert and their specific credential.',
          'medium',
        ),
      );
      results.push(
        ...findMatches(
          text,
          /\bhas been (featured|recognized|acknowledged) (by|in)\b/gi,
          'Cite the specific feature with a concrete claim.',
          'medium',
        ),
      );
      return results;
    },
  },

  {
    id: 3,
    name: 'Superficial -ing analyses',
    category: 'content',
    description: 'Tacking "-ing" participial phrases onto sentences to fake depth.',
    weight: 4,
    detect(text) {
      const ingPhrases =
        /,\s*(highlighting|underscoring|emphasizing|ensuring|reflecting|symbolizing|contributing to|cultivating|fostering|encompassing|showcasing|demonstrating|illustrating|representing|signaling|indicating|solidifying|reinforcing|cementing|underscoring|bolstering|reaffirming|illuminating|epitomizing)\b[^.]{5,}/gi;
      return findMatches(
        text,
        ingPhrases,
        'Remove trailing -ing phrase. If the point matters, give it its own sentence with specifics.',
        'high',
      );
    },
  },

  {
    id: 4,
    name: 'Promotional language',
    category: 'content',
    description: 'Ad-copy language that sounds like a tourism brochure or press release.',
    weight: 3,
    detect(text) {
      const results = [];
      for (const regex of PROMOTIONAL_WORDS) {
        results.push(
          ...findMatches(
            text,
            regex,
            'Replace promotional language with neutral, factual description.',
            'high',
          ),
        );
      }
      return results;
    },
  },

  {
    id: 5,
    name: 'Vague attributions',
    category: 'content',
    description: 'Attributing claims to unnamed experts, industry reports, or vague authorities.',
    weight: 4,
    detect(text) {
      const results = [];
      for (const regex of VAGUE_ATTRIBUTION_PHRASES) {
        results.push(
          ...findMatches(
            text,
            regex,
            "Name the specific source, study, or person. If you can't, remove the claim.",
            'high',
          ),
        );
      }
      return results;
    },
  },

  {
    id: 6,
    name: 'Formulaic challenges',
    category: 'content',
    description: 'Boilerplate "Despite challenges... continues to thrive" sections.',
    weight: 3,
    detect(text) {
      const results = [];
      for (const regex of CHALLENGES_PHRASES) {
        results.push(
          ...findMatches(
            text,
            regex,
            'Replace with specific challenges and concrete outcomes.',
            'high',
          ),
        );
      }
      return results;
    },
  },

  // ── LANGUAGE PATTERNS (7-12) ────────────────────────────

  {
    id: 7,
    name: 'AI vocabulary',
    category: 'language',
    description:
      'Words and phrases that appear far more frequently in AI-generated text. 500+ words tracked across 3 tiers.',
    weight: 5,
    detect(text) {
      const results = [];
      const words = wordCount(text);

      // Tier 1: always flag
      results.push(...scanWordList(text, TIER_1, 'Tier 1 AI word', 'high'));

      // Tier 2: flag if 2+ tier-2 words appear
      const tier2Matches = scanWordList(text, TIER_2, 'Tier 2 AI word', 'medium');
      if (tier2Matches.length >= 2) {
        results.push(...tier2Matches);
      }

      // Tier 3: flag only at high density (>3% of words are tier-3)
      if (words > 50) {
        const tier3Count = TIER_3.reduce((count, word) => {
          const regex = wordRegex(word);
          return count + countMatches(text, regex);
        }, 0);
        const density = tier3Count / words;
        if (density > 0.03) {
          results.push(...scanWordList(text, TIER_3, 'Tier 3 AI word (high density)', 'low'));
        }
      }

      // AI phrases (from vocabulary.js)
      results.push(
        ...scanPhrases(
          text,
          AI_PHRASES.filter(
            (p) =>
              p.fix &&
              !p.fix.startsWith('(remove') &&
              !['to', 'because', 'now', 'if', 'can', 'first', 'finally'].includes(p.fix),
          ),
        ),
      );

      return results;
    },
  },

  {
    id: 8,
    name: 'Copula avoidance',
    category: 'language',
    description:
      'Using "serves as", "functions as", "boasts" instead of simple "is", "has", "are".',
    weight: 3,
    detect(text) {
      const results = [];
      for (const regex of COPULA_AVOIDANCE) {
        results.push(
          ...findMatches(text, regex, 'Use simple "is", "are", or "has" instead.', 'high'),
        );
      }
      return results;
    },
  },

  {
    id: 9,
    name: 'Negative parallelisms',
    category: 'language',
    description:
      '"It\'s not just X, it\'s Y" or "Not only X but Y" constructions — overused by LLMs.',
    weight: 3,
    detect(text) {
      const negParallel =
        /\b(it'?s|this is) not (just|merely|only|simply) .{3,60}(,|;|—)\s*(it'?s|this is|but)\b/gi;
      const notOnly = /\bnot only .{3,60} but (also )?\b/gi;
      return [
        ...findMatches(
          text,
          negParallel,
          'Rewrite directly. State what the thing IS, not what it "isn\'t just".',
          'high',
        ),
        ...findMatches(
          text,
          notOnly,
          'Simplify. Remove the "not only...but also" frame.',
          'medium',
        ),
      ];
    },
  },

  {
    id: 10,
    name: 'Rule of three',
    category: 'language',
    description: 'Forcing ideas into groups of three. LLMs love triads that sound "comprehensive".',
    weight: 2,
    detect(text) {
      // Abstract noun triads
      const buzzyTriad =
        /\b(\w+tion|\w+ity|\w+ment|\w+ness|\w+ance|\w+ence),\s+(\w+tion|\w+ity|\w+ment|\w+ness|\w+ance|\w+ence),\s+and\s+(\w+tion|\w+ity|\w+ment|\w+ness|\w+ance|\w+ence)\b/gi;
      const results = findMatches(
        text,
        buzzyTriad,
        'Rule of three with abstract nouns. Pick the one or two that actually matter.',
        'medium',
      );

      // Buzzy adjective triads
      const buzzAdj = [
        'seamless',
        'intuitive',
        'powerful',
        'innovative',
        'dynamic',
        'robust',
        'comprehensive',
        'cutting-edge',
        'scalable',
        'agile',
        'efficient',
        'effective',
        'engaging',
        'impactful',
        'meaningful',
        'transformative',
        'sustainable',
        'resilient',
        'inclusive',
        'accessible',
      ];
      const adjPattern = buzzAdj.join('|');
      const adjTriad = new RegExp(
        `\\b(${adjPattern}),\\s+(${adjPattern}),\\s+and\\s+(${adjPattern})\\b`,
        'gi',
      );
      results.push(
        ...findMatches(
          text,
          adjTriad,
          'Buzzy adjective triad. Pick one and make it specific.',
          'medium',
        ),
      );

      return results;
    },
  },

  {
    id: 11,
    name: 'Synonym cycling',
    category: 'language',
    description:
      'Referring to the same thing by different names in consecutive sentences to avoid repetition.',
    weight: 2,
    detect(text) {
      const synonymSets = [
        ['protagonist', 'main character', 'central figure', 'hero', 'lead character', 'lead'],
        ['company', 'firm', 'organization', 'enterprise', 'corporation', 'establishment', 'entity'],
        ['city', 'metropolis', 'urban center', 'municipality', 'locale', 'township'],
        ['building', 'structure', 'edifice', 'facility', 'complex', 'establishment'],
        ['tool', 'instrument', 'mechanism', 'apparatus', 'device', 'utility'],
        ['country', 'nation', 'state', 'republic', 'sovereign state'],
        ['problem', 'challenge', 'issue', 'obstacle', 'hurdle', 'difficulty'],
        ['solution', 'approach', 'methodology', 'framework', 'strategy', 'paradigm'],
      ];

      const results = [];
      const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);

      for (const synonyms of synonymSets) {
        for (let i = 0; i < sentences.length - 1; i++) {
          const found = [];
          for (let j = i; j < Math.min(i + 4, sentences.length); j++) {
            const lower = sentences[j].toLowerCase();
            for (const syn of synonyms) {
              if (lower.includes(syn) && !found.includes(syn)) {
                found.push(syn);
              }
            }
          }
          if (found.length >= 3) {
            results.push({
              match: `Synonym cycling: ${found.join(' → ')}`,
              index: text.indexOf(sentences[i]),
              line: text.substring(0, text.indexOf(sentences[i])).split('\n').length,
              column: 1,
              suggestion: `Pick one term and stick with it. Found "${found.join('", "')}" used as synonyms in nearby sentences.`,
              confidence: 'medium',
            });
            break;
          }
        }
      }
      return results;
    },
  },

  {
    id: 12,
    name: 'False ranges',
    category: 'language',
    description: '"From X to Y" where X and Y aren\'t on a meaningful scale.',
    weight: 2,
    detect(text) {
      const doubleRange = /\bfrom .{3,40} to .{3,40},\s*from .{3,40} to .{3,40}/gi;
      const results = findMatches(
        text,
        doubleRange,
        "False range — X and Y probably aren't on a meaningful scale. Just list the topics.",
        'high',
      );

      const abstractRange =
        /\bfrom (the )?(dawn|birth|inception|beginning|advent|emergence|rise|earliest) .{3,60} to (the )?(modern|current|present|contemporary|latest|cutting-edge|digital|future)/gi;
      results.push(
        ...findMatches(
          text,
          abstractRange,
          "Unnecessarily broad range. Be specific about what you're actually covering.",
          'medium',
        ),
      );

      return results;
    },
  },

  // ── STYLE PATTERNS (13-18) ──────────────────────────────

  {
    id: 13,
    name: 'Em dash overuse',
    category: 'style',
    description: 'LLMs overuse em dashes (—) as a crutch for punchy writing.',
    weight: 2,
    detect(text) {
      const emDashes = text.match(/—/g) || [];
      const words = wordCount(text);
      const ratio = words > 0 ? emDashes.length / (words / 100) : 0;

      if (ratio > 1.0 && emDashes.length >= 2) {
        return findMatches(
          text,
          /—/g,
          `High em dash density (${emDashes.length} in ${words} words). Replace most with commas, periods, or parentheses.`,
          'medium',
        );
      }
      return [];
    },
  },

  {
    id: 14,
    name: 'Boldface overuse',
    category: 'style',
    description:
      'Mechanical emphasis of phrases in bold. AI uses **bold** as a highlighting crutch.',
    weight: 2,
    detect(text) {
      const boldMatches = text.match(/\*\*[^*]+\*\*/g) || [];
      if (boldMatches.length >= 3) {
        return findMatches(
          text,
          /\*\*[^*]+\*\*/g,
          'Excessive boldface. Remove emphasis — let the writing carry the weight.',
          'medium',
        );
      }
      return [];
    },
  },

  {
    id: 15,
    name: 'Inline-header lists',
    category: 'style',
    description: 'Lists where each item starts with a bolded header followed by a colon.',
    weight: 3,
    detect(text) {
      const inlineHeaders = /^[*-]\s+\*\*[^*]+:\*\*\s/gm;
      const matches = text.match(inlineHeaders) || [];
      if (matches.length >= 2) {
        return findMatches(
          text,
          inlineHeaders,
          'Inline-header list pattern. Convert to a paragraph or use a simpler list.',
          'high',
        );
      }
      return [];
    },
  },

  {
    id: 16,
    name: 'Title Case headings',
    category: 'style',
    description: 'Capitalizing Every Main Word In Headings. AI chatbots default to this.',
    weight: 1,
    detect(text) {
      const headingRegex = /^#{1,6}\s+(.+)$/gm;
      const results = [];
      let m;
      while ((m = headingRegex.exec(text)) !== null) {
        const heading = m[1].trim();
        const words = heading.split(/\s+/);
        if (words.length >= 3) {
          const skipWords =
            /^(I|AI|API|CLI|URL|HTML|CSS|JS|TS|NPM|NYC|USA|UK|EU|LLM|GPT|SaaS|IoT|CEO|CTO|VP|PR|HR|IT|UI|UX)\b/;
          const capitalizedCount = words.filter(
            (w) => /^[A-Z]/.test(w) && !skipWords.test(w),
          ).length;
          if (capitalizedCount / words.length > 0.7) {
            const lineNum = text.substring(0, m.index).split('\n').length;
            results.push({
              match: m[0],
              index: m.index,
              line: lineNum,
              column: 1,
              suggestion:
                'Use sentence case for headings (only capitalize first word and proper nouns).',
              confidence: 'medium',
            });
          }
        }
      }
      return results;
    },
  },

  {
    id: 17,
    name: 'Emoji overuse',
    category: 'style',
    description: 'Decorating headings or bullet points with emojis in professional/technical text.',
    weight: 2,
    detect(text) {
      const emojiCount = countMatches(text, /[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}]/gu);
      if (emojiCount >= 3) {
        return findMatches(
          text,
          /[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}]/gu,
          'Remove emoji decoration from professional text.',
          'high',
        );
      }
      return [];
    },
  },

  {
    id: 18,
    name: 'Curly quotes',
    category: 'style',
    description:
      'ChatGPT uses Unicode curly quotes (\u201C\u201D\u2018\u2019) instead of straight quotes.',
    weight: 1,
    detect(text) {
      return findMatches(
        text,
        /[\u201C\u201D\u2018\u2019]/g,
        'Replace curly quotes with straight quotes.',
        'high',
      );
    },
  },

  // ── COMMUNICATION PATTERNS (19-21) ─────────────────────

  {
    id: 19,
    name: 'Chatbot artifacts',
    category: 'communication',
    description:
      'Leftover chatbot phrases: "I hope this helps!", "Let me know if...", "Here is an overview".',
    weight: 5,
    detect(text) {
      // Use the phrase-level detection from vocabulary.js
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) => p.fix === '(remove)' || p.fix === '(remove — start with the content)',
        ),
      );
    },
  },

  {
    id: 20,
    name: 'Cutoff disclaimers',
    category: 'communication',
    description: 'AI knowledge-cutoff disclaimers left in text.',
    weight: 4,
    detect(text) {
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) =>
            p.fix === '(remove)' &&
            (p.pattern.source.includes('training') ||
              p.pattern.source.includes('details are') ||
              p.pattern.source.includes('available')),
        ),
      );
    },
  },

  {
    id: 21,
    name: 'Sycophantic tone',
    category: 'communication',
    description:
      'Overly positive, people-pleasing language: "Great question!", "You\'re absolutely right!".',
    weight: 4,
    detect(text) {
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) =>
            p.fix &&
            (p.fix.includes('(remove)') || p.fix.includes('address the substance')) &&
            (p.pattern.source.includes('question') ||
              p.pattern.source.includes('point') ||
              p.pattern.source.includes('right') ||
              p.pattern.source.includes('observation')),
        ),
      );
    },
  },

  // ── FILLER & HEDGING (22-24) ────────────────────────────

  {
    id: 22,
    name: 'Filler phrases',
    category: 'filler',
    description:
      'Wordy filler that can be shortened: "in order to" → "to", "due to the fact that" → "because".',
    weight: 3,
    detect(text) {
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) =>
            p.fix &&
            !p.fix.startsWith('(') &&
            [
              'to',
              'because',
              'now',
              'if',
              'can',
              'to / for',
              'first',
              'finally',
              'for / regarding',
              'because / since',
            ].includes(p.fix),
        ),
      );
    },
  },

  {
    id: 23,
    name: 'Excessive hedging',
    category: 'filler',
    description: 'Stacking qualifiers: "could potentially possibly", "might arguably perhaps".',
    weight: 3,
    detect(text) {
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) =>
            p.fix &&
            (p.fix.includes('could') ||
              p.fix.includes('might') ||
              p.fix.includes('may') ||
              p.fix.includes('perhaps') ||
              p.fix.includes('maybe')),
        ),
      );
    },
  },

  {
    id: 24,
    name: 'Generic conclusions',
    category: 'filler',
    description: 'Vague upbeat endings: "The future looks bright", "Exciting times lie ahead".',
    weight: 3,
    detect(text) {
      return scanPhrases(
        text,
        AI_PHRASES.filter(
          (p) =>
            p.fix &&
            (p.fix.includes('specific fact') ||
              p.fix.includes('concrete') ||
              p.fix.includes('cite evidence') ||
              p.fix.includes('what you do know') ||
              p.fix.includes('what happens next')),
        ),
      );
    },
  },
];

// ─── Pattern Registry ────────────────────────────────────

class PatternRegistry {
  constructor() {
    this._patterns = [...patterns];
    this._customWords = { tier1: [], tier2: [], tier3: [] };
  }

  /** Get all patterns. */
  all() {
    return this._patterns;
  }

  /** Get pattern by ID. */
  get(id) {
    return this._patterns.find((p) => p.id === id);
  }

  /** Get patterns by category. */
  byCategory(category) {
    return this._patterns.filter((p) => p.category === category);
  }

  /** Add a custom pattern. */
  add(pattern) {
    if (!pattern.id || !pattern.name || !pattern.detect) {
      throw new Error('Pattern must have id, name, and detect function');
    }
    this._patterns.push(pattern);
  }

  /** Remove a pattern by ID. */
  remove(id) {
    this._patterns = this._patterns.filter((p) => p.id !== id);
  }

  /** Add custom words to a tier. */
  addWords(tier, words) {
    const key = `tier${tier}`;
    if (!this._customWords[key]) throw new Error(`Invalid tier: ${tier}`);
    this._customWords[key].push(...words);
  }

  /** Get full vocabulary for a tier (built-in + custom). */
  getVocabulary(tier) {
    const builtIn = tier === 1 ? TIER_1 : tier === 2 ? TIER_2 : TIER_3;
    return [...builtIn, ...(this._customWords[`tier${tier}`] || [])];
  }

  /** List all pattern IDs and names. */
  list() {
    return this._patterns.map((p) => ({
      id: p.id,
      name: p.name,
      category: p.category,
      weight: p.weight,
    }));
  }

  /** Get categories. */
  categories() {
    return [...new Set(this._patterns.map((p) => p.category))];
  }
}

// Singleton registry
const registry = new PatternRegistry();

// ─── Exports ─────────────────────────────────────────────

module.exports = {
  patterns,
  registry,
  PatternRegistry,
  findMatches,
  countMatches,
  wordCount,
  scanWordList,
  scanPhrases,
  // Re-export vocabulary for backward compat
  TIER_1,
  TIER_2,
  TIER_3,
  AI_PHRASES,
  SIGNIFICANCE_PHRASES,
  PROMOTIONAL_WORDS,
  VAGUE_ATTRIBUTION_PHRASES,
  CHALLENGES_PHRASES,
  COPULA_AVOIDANCE,
};


path: src/stats.js
content: /**
 * stats.js — Text statistics engine.
 *
 * Computes stylometric features that distinguish AI from human writing.
 * Based on academic research (Copyleaks arxiv 2503.01659v1, StyloAI):
 *
 *   - Sentence length statistics (mean, std dev, variation coefficient)
 *   - Burstiness score (humans write in bursts/lulls; AI is uniform)
 *   - Vocabulary diversity (type-token ratio)
 *   - Function word ratio
 *   - N-gram repetition density
 *   - Readability metrics (Flesch-Kincaid)
 *   - Paragraph structure statistics
 */

const { FUNCTION_WORDS } = require('./vocabulary');

// ─── Sentence Splitting ─────────────────────────────────

/**
 * Split text into sentences. Handles abbreviations and edge cases better
 * than a naive split on period.
 */
function splitSentences(text) {
  // Handle common abbreviations that shouldn't split
  const cleaned = text
    .replace(/\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|etc|vs|approx|dept|est|vol)\./gi, '$1\u2024') // temp replace
    .replace(/\b([A-Z])\./g, '$1\u2024') // initials: "J. K. Rowling"
    .replace(/\b(\d+)\./g, '$1\u2024'); // numbered lists

  const sentences = cleaned
    .split(/(?<=[.!?])\s+(?=[A-Z"'\u201C])|(?<=[.!?])$/)
    .map((s) => s.replace(/\u2024/g, '.').trim())
    .filter((s) => s.length > 0);

  return sentences;
}

// ─── Core Statistics ─────────────────────────────────────

/**
 * Tokenize text into words (lowercase, stripped of punctuation).
 */
function tokenize(text) {
  return text
    .toLowerCase()
    .replace(/[^\w\s'-]/g, ' ')
    .split(/\s+/)
    .filter((w) => w.length > 0);
}

/**
 * Compute all text statistics.
 *
 * @param {string} text — Input text
 * @returns {object}    — Statistics object
 */
function computeStats(text) {
  if (!text || typeof text !== 'string' || text.trim().length === 0) {
    return emptyStats();
  }

  const words = tokenize(text);
  const sentences = splitSentences(text);
  const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);

  if (words.length === 0) return emptyStats();

  // ── Word-level stats ────────────────────────────────
  const wordCount = words.length;
  const uniqueWords = new Set(words);
  const typeTokenRatio = uniqueWords.size / wordCount;

  // Average word length
  const avgWordLength = words.reduce((sum, w) => sum + w.length, 0) / wordCount;

  // ── Sentence-level stats ────────────────────────────
  const sentenceLengths = sentences.map((s) => tokenize(s).length).filter((n) => n > 0);
  const sentenceCount = sentenceLengths.length;

  let avgSentenceLength = 0;
  let sentenceLengthStdDev = 0;
  let sentenceLengthVariation = 0;
  let burstiness = 0;

  if (sentenceCount > 1) {
    avgSentenceLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceCount;

    // Standard deviation
    const variance =
      sentenceLengths.reduce((sum, len) => sum + Math.pow(len - avgSentenceLength, 2), 0) /
      sentenceCount;
    sentenceLengthStdDev = Math.sqrt(variance);

    // Coefficient of variation (std dev / mean) — our burstiness proxy
    sentenceLengthVariation = avgSentenceLength > 0 ? sentenceLengthStdDev / avgSentenceLength : 0;

    // Burstiness: based on consecutive sentence length differences
    // High burstiness = human (lots of variation between consecutive sentences)
    // Low burstiness = AI (uniform sentence length throughout)
    let consecutiveDiffSum = 0;
    for (let i = 1; i < sentenceLengths.length; i++) {
      consecutiveDiffSum += Math.abs(sentenceLengths[i] - sentenceLengths[i - 1]);
    }
    const avgConsecutiveDiff = consecutiveDiffSum / (sentenceLengths.length - 1);
    burstiness = avgSentenceLength > 0 ? avgConsecutiveDiff / avgSentenceLength : 0;
  } else if (sentenceCount === 1) {
    avgSentenceLength = sentenceLengths[0];
  }

  // ── Function word ratio ─────────────────────────────
  const functionWordSet = new Set(FUNCTION_WORDS);
  const functionWordCount = words.filter((w) => functionWordSet.has(w)).length;
  const functionWordRatio = functionWordCount / wordCount;

  // ── N-gram repetition ───────────────────────────────
  const trigramRepetition = computeNgramRepetition(words, 3);

  // ── Paragraph stats ─────────────────────────────────
  const paragraphCount = paragraphs.length;
  const avgParagraphLength =
    paragraphCount > 0
      ? paragraphs.reduce((sum, p) => sum + tokenize(p).length, 0) / paragraphCount
      : 0;

  // ── Readability (Flesch-Kincaid Grade Level approximation) ──
  const syllableCount = words.reduce((sum, w) => sum + estimateSyllables(w), 0);
  const fleschKincaid =
    sentenceCount > 0
      ? 0.39 * (wordCount / sentenceCount) + 11.8 * (syllableCount / wordCount) - 15.59
      : 0;

  return {
    wordCount,
    uniqueWordCount: uniqueWords.size,
    sentenceCount,
    paragraphCount,
    avgWordLength: round(avgWordLength),
    avgSentenceLength: round(avgSentenceLength),
    sentenceLengthStdDev: round(sentenceLengthStdDev),
    sentenceLengthVariation: round(sentenceLengthVariation), // coefficient of variation
    burstiness: round(burstiness),
    typeTokenRatio: round(typeTokenRatio),
    functionWordRatio: round(functionWordRatio),
    trigramRepetition: round(trigramRepetition),
    avgParagraphLength: round(avgParagraphLength),
    fleschKincaid: round(fleschKincaid),
    sentenceLengths,
  };
}

/**
 * Compute n-gram repetition rate.
 * Returns the fraction of n-grams that appear more than once.
 * AI text tends to reuse similar n-grams more than human text.
 */
function computeNgramRepetition(words, n) {
  if (words.length < n) return 0;

  const ngrams = {};
  for (let i = 0; i <= words.length - n; i++) {
    const gram = words.slice(i, i + n).join(' ');
    ngrams[gram] = (ngrams[gram] || 0) + 1;
  }

  const totalNgrams = Object.keys(ngrams).length;
  if (totalNgrams === 0) return 0;

  const repeated = Object.values(ngrams).filter((c) => c > 1).length;
  return repeated / totalNgrams;
}

/**
 * Estimate syllable count for a word (English heuristic).
 */
function estimateSyllables(word) {
  word = word.toLowerCase().replace(/[^a-z]/g, '');
  if (word.length <= 3) return 1;

  // Count vowel groups
  const vowelGroups = word.match(/[aeiouy]+/g);
  let count = vowelGroups ? vowelGroups.length : 1;

  // Subtract silent e
  if (word.endsWith('e') && !word.endsWith('le')) count--;
  // Add for -ed that creates syllable
  if (word.endsWith('ed') && word.length > 3 && !/[aeiouy]ed$/.test(word)) count--;

  return Math.max(count, 1);
}

/**
 * Compute a "uniformity score" from text stats.
 * Higher = more uniform/AI-like. Lower = more varied/human-like.
 * Range: 0-100.
 */
function computeUniformityScore(stats) {
  if (stats.wordCount === 0) return 0;

  let score = 0;

  // Low burstiness = more AI-like (max 25 points)
  // Human burstiness is typically 0.5-1.0, AI is 0.1-0.3
  if (stats.burstiness < 0.2) score += 25;
  else if (stats.burstiness < 0.35) score += 18;
  else if (stats.burstiness < 0.5) score += 10;
  else if (stats.burstiness < 0.65) score += 5;

  // Low sentence length variation = more AI-like (max 25 points)
  // Human CoV is typically 0.4-0.8, AI is 0.15-0.35
  if (stats.sentenceLengthVariation < 0.2) score += 25;
  else if (stats.sentenceLengthVariation < 0.35) score += 18;
  else if (stats.sentenceLengthVariation < 0.5) score += 10;
  else if (stats.sentenceLengthVariation < 0.65) score += 5;

  // Low type-token ratio = more repetitive/AI-like (max 20 points)
  // But very short texts naturally have high TTR, so only penalize for longer texts
  if (stats.wordCount > 100) {
    if (stats.typeTokenRatio < 0.35) score += 20;
    else if (stats.typeTokenRatio < 0.45) score += 12;
    else if (stats.typeTokenRatio < 0.55) score += 5;
  }

  // High trigram repetition = more AI-like (max 15 points)
  if (stats.trigramRepetition > 0.15) score += 15;
  else if (stats.trigramRepetition > 0.1) score += 10;
  else if (stats.trigramRepetition > 0.05) score += 5;

  // Abnormally uniform paragraph lengths (max 15 points)
  // Only check if we have multiple paragraphs
  if (stats.paragraphCount >= 3 && stats.sentenceCount > 5) {
    // Check if all paragraphs are similar length
    // Use sentence length uniformity as a proxy for paragraph uniformity
    if (stats.sentenceLengthStdDev < 3 && stats.avgSentenceLength > 10) {
      score += 15; // Very uniform sentence lengths with moderate length = AI
    }
  }

  return Math.min(score, 100);
}

function emptyStats() {
  return {
    wordCount: 0,
    uniqueWordCount: 0,
    sentenceCount: 0,
    paragraphCount: 0,
    avgWordLength: 0,
    avgSentenceLength: 0,
    sentenceLengthStdDev: 0,
    sentenceLengthVariation: 0,
    burstiness: 0,
    typeTokenRatio: 0,
    functionWordRatio: 0,
    trigramRepetition: 0,
    avgParagraphLength: 0,
    fleschKincaid: 0,
    sentenceLengths: [],
  };
}

function round(n) {
  return Math.round(n * 1000) / 1000;
}

// ─── Exports ─────────────────────────────────────────────

module.exports = {
  computeStats,
  computeUniformityScore,
  computeNgramRepetition,
  splitSentences,
  tokenize,
  estimateSyllables,
};


path: src/vocabulary.js
content: /**
 * vocabulary.js — Comprehensive AI vocabulary database.
 *
 * 500+ words and phrases organized into detection tiers based on how strongly
 * they signal AI-generated text. Sourced from:
 *   - Wikipedia:Signs of AI writing (WikiProject AI Cleanup)
 *   - Copyleaks stylistic fingerprint research (arxiv 2503.01659v1)
 *   - godofprompt.ai comprehensive AI word analysis
 *   - Real-world pattern observation across ChatGPT, Claude, Gemini, Llama
 *
 * Tiers:
 *   1 — Dead giveaways. Almost never appear in natural human writing at these frequencies.
 *   2 — Suspicious when clustered. Fine alone, damning in groups.
 *   3 — Context-dependent. Only flagged when density exceeds threshold.
 */

// ─── Tier 1: Dead Giveaways ─────────────────────────────
// Words that appear 5-20x more often in AI text than human text.

const TIER_1 = [
  'delve',
  'delving',
  'delved',
  'delves',
  'tapestry',
  'vibrant',
  'crucial',
  'comprehensive',
  'intricate',
  'intricacies',
  'pivotal',
  'testament',
  'landscape', // abstract usage: "the evolving landscape of"
  'bustling',
  'nestled',
  'realm',
  'meticulous',
  'meticulously',
  'complexities',
  'embark',
  'embarking',
  'embarked',
  'robust',
  'showcasing',
  'showcase',
  'showcased',
  'showcases',
  'underscores',
  'underscoring',
  'underscored',
  'fostering',
  'foster',
  'fostered',
  'fosters',
  'seamless',
  'seamlessly',
  'groundbreaking',
  'renowned',
  'synergy',
  'synergies',
  'leverage',
  'leveraging',
  'leveraged',
  'garner',
  'garnered',
  'garnering',
  'interplay',
  'enduring',
  'enhance',
  'enhanced',
  'enhancing',
  'enhancement',
  'tapestry',
  'testament',
  'additionally',
  'daunting',
  'ever-evolving',
  'game changer',
  'game-changing',
  'game-changer',
  'underscore',
];

// ─── Tier 2: Suspicious in Density ──────────────────────
// Normal in isolation, but multiple occurrences signal AI authorship.

const TIER_2 = [
  'furthermore',
  'moreover',
  'notably',
  'consequently',
  'subsequently',
  'accordingly',
  'nonetheless',
  'henceforth',
  'indeed',
  'specifically',
  'essentially',
  'ultimately',
  'arguably',
  'fundamentally',
  'inherently',
  'profoundly',

  'encompassing',
  'encompasses',
  'encompassed',
  'endeavour',
  'endeavor',
  'endeavoring',
  'elevate',
  'elevated',
  'elevating',
  'alleviate',
  'alleviating',
  'streamline',
  'streamlined',
  'streamlining',
  'harness',
  'harnessing',
  'harnessed',
  'unleash',
  'unleashing',
  'unleashed',
  'revolutionize',
  'revolutionizing',
  'revolutionized',
  'transformative',
  'transformation',
  'paramount',
  'multifaceted',
  'spearhead',
  'spearheading',
  'spearheaded',
  'bolster',
  'bolstering',
  'bolstered',
  'catalyze',
  'catalyst',
  'catalyzed',
  'cornerstone',
  'reimagine',
  'reimagining',
  'reimagined',
  'empower',
  'empowering',
  'empowerment',
  'empowered',
  'navigate',
  'navigating',
  'navigated',
  'poised',
  'myriad',
  'nuanced',
  'nuance',
  'nuances',
  'paradigm',
  'paradigms',
  'paradigm-shifting',
  'holistic',
  'holistically',
  'utilize',
  'utilizing',
  'utilization',
  'utilized',
  'facilitate',
  'facilitated',
  'facilitating',
  'facilitation',
  'elucidate',
  'elucidating',
  'illuminate',
  'illuminating',
  'illuminated',
  'invaluable',
  'cutting-edge',
  'innovative',
  'innovation',
  'align',
  'aligns',
  'aligning',
  'alignment',
  'dynamic',
  'dynamics',
  'impactful',
  'agile',
  'scalable',
  'scalability',
  'proactive',
  'proactively',
  'synergistic',
  'optimize',
  'optimizing',
  'optimization',
  'resonate',
  'resonating',
  'resonated',
  'resonates',
  'underscore',
  'underscored',
  'cultivate',
  'cultivating',
  'cultivated',
  'galvanize',
  'galvanizing',
  'invigorate',
  'invigorating',
  'juxtapose',
  'juxtaposing',
  'juxtaposition',
  'underscore',
  'bolster',
  'augment',
  'augmenting',
  'augmented',
  'proliferate',
  'proliferating',
  'proliferation',
  'burgeoning',
  'nascent',
  'ubiquitous',
  'plethora',
  'myriad',
  'quintessential',
  'eclectic',
  'indelible',
  'overarching',
  'underpinning',
  'underpinnings',
];

// ─── Tier 3: Context-Dependent ──────────────────────────
// Common words that only become AI signals at high density or in
// combination with other AI patterns. Flagged when density > 3%.

const TIER_3 = [
  'significant',
  'significantly',
  'important',
  'importantly',
  'effective',
  'effectively',
  'efficient',
  'efficiently',
  'diverse',
  'diversity',
  'unique',
  'uniquely',
  'key', // as adjective: "key role", "key factor"
  'vital',
  'vitally',
  'critical',
  'critically',
  'essential',
  'essentially',
  'valuable',
  'notable',
  'remarkable',
  'remarkably',
  'substantial',
  'substantially',
  'considerable',
  'considerably',
  'noteworthy',
  'prominent',
  'prominently',
  'influential',
  'thoughtful',
  'thoughtfully',
  'insightful',
  'insightfully',
  'meaningful',
  'meaningfully',
  'purposeful',
  'purposefully',
  'deliberate',
  'deliberately',
  'strategic',
  'strategically',
  'integral',
  'indispensable',
  'instrumental',
  'imperative',
  'exemplary',
  'commendable',
  'praiseworthy',
  'sophisticated',
  'profound',
  'compelling',
  'captivating',
  'exquisite',
  'impeccable',
  'formidable',
  'stellar',
  'exceptional',
  'exceptionally',
  'extraordinary',
  'unparalleled',
  'unprecedented',
  'monumental',
  'groundbreaking',
  'trailblazing',
  'visionary',
  'world-class',
  'state-of-the-art',
  'best-in-class',
];

// ─── AI Phrases (Tier 3+) ───────────────────────────────
// Multi-word phrases that strongly signal AI authorship.
// Each has a regex pattern and a severity weight.

const AI_PHRASES = [
  // "In today's..." openers
  {
    pattern:
      /\bin today'?s (digital age|fast-paced world|rapidly evolving|ever-changing|modern|interconnected)\b/gi,
    tier: 1,
    fix: '(remove or be specific about what changed)',
  },
  { pattern: /\bin today'?s world\b/gi, tier: 2, fix: '(remove or be specific)' },

  // "It is [worth/important] to note"
  {
    pattern: /\bit is (worth|important to|essential to|crucial to) not(e|ing) that\b/gi,
    tier: 1,
    fix: '(remove — just state the fact)',
  },
  { pattern: /\bit should be noted that\b/gi, tier: 1, fix: '(remove — just state the fact)' },
  { pattern: /\bit bears mentioning that\b/gi, tier: 1, fix: '(remove — just state the fact)' },

  // "Pave the way" and journey metaphors
  { pattern: /\bpave the way (for|to)\b/gi, tier: 1, fix: 'enable / allow / lead to' },
  { pattern: /\bat the forefront of\b/gi, tier: 1, fix: 'leading / first in' },
  {
    pattern: /\bnavigate the (complexities|challenges|landscape)\b/gi,
    tier: 1,
    fix: 'handle / deal with / work through',
  },
  { pattern: /\bharness the (power|potential|capabilities) of\b/gi, tier: 1, fix: 'use' },
  { pattern: /\bembark on a journey\b/gi, tier: 1, fix: 'start / begin' },
  { pattern: /\bpush the boundaries\b/gi, tier: 1, fix: '(be specific about what changed)' },
  {
    pattern: /\bfoster a (culture|environment|atmosphere|sense) of\b/gi,
    tier: 1,
    fix: 'build / create / encourage',
  },
  {
    pattern: /\bunlock the (potential|power|full|true)\b/gi,
    tier: 1,
    fix: 'enable / use / improve',
  },
  { pattern: /\bserves as a testament\b/gi, tier: 1, fix: 'shows / proves / demonstrates' },
  {
    pattern: /\bplays a (crucial|pivotal|vital|key|significant|important|critical) role\b/gi,
    tier: 1,
    fix: 'matters for / helps / is important to',
  },
  { pattern: /\bin the realm of\b/gi, tier: 1, fix: 'in' },
  { pattern: /\bdelve into\b/gi, tier: 1, fix: 'explore / examine / look at' },
  { pattern: /\bthe landscape of\b/gi, tier: 1, fix: '(be specific — what part of the field?)' },
  { pattern: /\bnestled (in|within|among)\b/gi, tier: 1, fix: 'located in / in / near' },

  // Abstract verb phrases
  { pattern: /\brise to the (occasion|challenge)\b/gi, tier: 2, fix: 'handle / face / tackle' },
  {
    pattern: /\bstand at the (crossroads|intersection)\b/gi,
    tier: 2,
    fix: '(be specific about the choice)',
  },
  {
    pattern: /\bshape the (future|trajectory|direction)\b/gi,
    tier: 2,
    fix: '(be specific about how)',
  },
  { pattern: /\btip of the iceberg\b/gi, tier: 2, fix: 'one example / a small part' },
  { pattern: /\bdouble-edged sword\b/gi, tier: 2, fix: 'has tradeoffs / cuts both ways' },
  { pattern: /\ba testament to\b/gi, tier: 1, fix: 'shows / proves' },
  { pattern: /\bthe dawn of\b/gi, tier: 2, fix: 'the start of / the beginning of' },
  { pattern: /\bthe fabric of\b/gi, tier: 1, fix: '(be concrete)' },
  { pattern: /\bthe tapestry of\b/gi, tier: 1, fix: '(be concrete)' },

  // Hedging stacks
  { pattern: /\bcould potentially\b/gi, tier: 1, fix: 'could / might' },
  { pattern: /\bmight possibly\b/gi, tier: 1, fix: 'might' },
  { pattern: /\bcould possibly\b/gi, tier: 1, fix: 'could' },
  { pattern: /\bperhaps potentially\b/gi, tier: 1, fix: 'perhaps / maybe' },
  { pattern: /\bmay potentially\b/gi, tier: 1, fix: 'may' },
  { pattern: /\bcould conceivably\b/gi, tier: 1, fix: 'could' },

  // Chatbot filler
  { pattern: /\bI hope this helps\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\blet me know if (you|there)\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bwould you like me to\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bfeel free to\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bdon'?t hesitate to\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bhappy to help\b/gi, tier: 1, fix: '(remove)' },
  {
    pattern:
      /\bhere is (a |an |the )?(comprehensive |brief |quick )?(overview|summary|breakdown|list|guide|explanation|look)\b/gi,
    tier: 1,
    fix: '(remove — start with the content)',
  },
  { pattern: /\bI'?d be happy to\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bis there anything else\b/gi, tier: 1, fix: '(remove)' },

  // Sycophantic
  { pattern: /\bgreat question\b/gi, tier: 1, fix: '(remove)' },
  { pattern: /\bexcellent (question|point|observation)\b/gi, tier: 1, fix: '(remove)' },
  {
    pattern:
      /\bthat'?s a (great|excellent|wonderful|fantastic|good|insightful|thoughtful) (question|point|observation)\b/gi,
    tier: 1,
    fix: '(remove)',
  },
  { pattern: /\byou'?re absolutely right\b/gi, tier: 1, fix: '(remove or address the substance)' },
  {
    pattern: /\byou raise a (great|good|excellent|valid|important) point\b/gi,
    tier: 1,
    fix: '(remove or address the substance)',
  },

  // Cutoff disclaimers
  {
    pattern: /\bas of (my|this) (last|latest|most recent) (training|update|knowledge)\b/gi,
    tier: 1,
    fix: '(remove)',
  },
  {
    pattern: /\bwhile (specific )?details are (limited|scarce|not available)\b/gi,
    tier: 1,
    fix: '(remove — research it or omit the claim)',
  },
  {
    pattern: /\bbased on (available|my|current) (information|knowledge|understanding|data)\b/gi,
    tier: 1,
    fix: '(remove)',
  },
  { pattern: /\bup to my (last )?training\b/gi, tier: 1, fix: '(remove)' },

  // Generic conclusions
  {
    pattern: /\bthe future (looks|is|remains) bright\b/gi,
    tier: 1,
    fix: '(end with a specific fact or plan)',
  },
  {
    pattern: /\bexciting times (lie|lay|are) ahead\b/gi,
    tier: 1,
    fix: '(end with a specific fact or plan)',
  },
  {
    pattern: /\bcontinue (this|their|our|the) journey\b/gi,
    tier: 1,
    fix: '(be specific about what happens next)',
  },
  {
    pattern: /\bjourney toward(s)? (excellence|success|greatness)\b/gi,
    tier: 1,
    fix: '(be specific)',
  },
  { pattern: /\bstep in the right direction\b/gi, tier: 1, fix: '(be specific about the outcome)' },
  { pattern: /\bonly time will tell\b/gi, tier: 1, fix: '(end with what you actually know)' },
  {
    pattern: /\bthe possibilities are (endless|limitless|infinite)\b/gi,
    tier: 1,
    fix: "(be specific about what's possible)",
  },
  {
    pattern: /\bpoised for (growth|success|greatness|expansion)\b/gi,
    tier: 1,
    fix: '(cite evidence or remove)',
  },
  { pattern: /\bwatch this space\b/gi, tier: 2, fix: '(end with something concrete)' },
  { pattern: /\bstay tuned\b/gi, tier: 2, fix: '(end with something concrete)' },
  { pattern: /\bremains to be seen\b/gi, tier: 2, fix: '(state what you do know)' },

  // Formulaic filler
  { pattern: /\bin order to\b/gi, tier: 2, fix: 'to' },
  { pattern: /\bdue to the fact that\b/gi, tier: 1, fix: 'because' },
  { pattern: /\bat this point in time\b/gi, tier: 1, fix: 'now' },
  { pattern: /\bin the event that\b/gi, tier: 1, fix: 'if' },
  { pattern: /\bhas the ability to\b/gi, tier: 1, fix: 'can' },
  { pattern: /\bfor the purpose of\b/gi, tier: 1, fix: 'to / for' },
  { pattern: /\bin light of the fact that\b/gi, tier: 1, fix: 'because / since' },
  { pattern: /\bfirst and foremost\b/gi, tier: 2, fix: 'first' },
  { pattern: /\blast but not least\b/gi, tier: 2, fix: 'finally' },
  { pattern: /\bat the end of the day\b/gi, tier: 2, fix: '(remove or be specific)' },
  { pattern: /\bwhen it comes to\b/gi, tier: 2, fix: 'for / regarding' },
  { pattern: /\bthe fact of the matter is\b/gi, tier: 1, fix: '(remove — just state it)' },
  { pattern: /\bin terms of\b/gi, tier: 3, fix: 'for / about / regarding' },
  { pattern: /\bat its core\b/gi, tier: 2, fix: '(remove or be specific)' },
  {
    pattern: /\bit goes without saying\b/gi,
    tier: 2,
    fix: "(if it goes without saying, don't say it)",
  },
  { pattern: /\bneedless to say\b/gi, tier: 2, fix: "(if needless to say, don't say it)" },
];

// ─── Function Words ─────────────────────────────────────
// Function words make up ~0.04% of vocabulary but 50%+ of usage.
// Their distribution differs measurably between AI and human text.
// These are the function words tracked for stylometric analysis.

const FUNCTION_WORDS = [
  'the',
  'be',
  'to',
  'of',
  'and',
  'a',
  'in',
  'that',
  'have',
  'I',
  'it',
  'for',
  'not',
  'on',
  'with',
  'he',
  'as',
  'you',
  'do',
  'at',
  'this',
  'but',
  'his',
  'by',
  'from',
  'they',
  'we',
  'say',
  'her',
  'she',
  'or',
  'an',
  'will',
  'my',
  'one',
  'all',
  'would',
  'there',
  'their',
  'what',
  'so',
  'up',
  'out',
  'if',
  'about',
  'who',
  'get',
  'which',
  'go',
  'me',
  'when',
  'make',
  'can',
  'like',
  'time',
  'no',
  'just',
  'him',
  'know',
  'take',
  'people',
  'into',
  'year',
  'your',
  'good',
  'some',
  'could',
  'them',
  'see',
  'other',
  'than',
  'then',
  'now',
  'look',
  'only',
  'come',
  'its',
  'over',
  'think',
  'also',
  'back',
  'after',
  'use',
  'two',
  'how',
  'our',
  'work',
  'first',
  'well',
  'way',
  'even',
  'new',
  'want',
  'because',
  'any',
  'these',
  'give',
  'day',
  'most',
  'us',
];

// ─── Exports ─────────────────────────────────────────────

module.exports = {
  TIER_1,
  TIER_2,
  TIER_3,
  AI_PHRASES,
  FUNCTION_WORDS,
};


path: tests/analyzer.test.js
content: /**
 * analyzer.test.js — Tests for the text analysis engine.
 */

import { describe, it, expect } from 'vitest';
import { analyze, score, formatReport, formatJSON, formatMarkdown } from '../src/analyzer.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function loadFixture(name) {
  return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf-8');
}

// ─── Basic Functionality ─────────────────────────────────

describe('analyze', () => {
  it('returns a valid result object', () => {
    const result = analyze('Hello world.');
    expect(result).toHaveProperty('score');
    expect(result).toHaveProperty('patternScore');
    expect(result).toHaveProperty('uniformityScore');
    expect(result).toHaveProperty('totalMatches');
    expect(result).toHaveProperty('wordCount');
    expect(result).toHaveProperty('categories');
    expect(result).toHaveProperty('findings');
    expect(result).toHaveProperty('summary');
    expect(result).toHaveProperty('stats');
  });

  it('handles empty input gracefully', () => {
    const result = analyze('');
    expect(result.score).toBe(0);
    expect(result.totalMatches).toBe(0);
  });

  it('handles null/undefined input', () => {
    expect(analyze(null).score).toBe(0);
    expect(analyze(undefined).score).toBe(0);
  });

  it('scores clean human text low', () => {
    const text = loadFixture('human-sample-1.txt');
    const result = analyze(text);
    expect(result.score).toBeLessThan(25);
  });

  it('scores obvious AI text high', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = analyze(text);
    expect(result.score).toBeGreaterThan(50);
  });

  it('detects multiple categories in AI text', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = analyze(text);
    const hitCategories = Object.entries(result.categories)
      .filter(([, v]) => v.matches > 0)
      .map(([k]) => k);
    expect(hitCategories.length).toBeGreaterThanOrEqual(3);
  });

  it('includes stats in result', () => {
    const text = 'The cat sat on the mat. The dog ran fast. The bird flew away.';
    const result = analyze(text);
    expect(result.stats).not.toBeNull();
    expect(result.stats).toHaveProperty('burstiness');
    expect(result.stats).toHaveProperty('typeTokenRatio');
  });
});

// ─── Score Function ──────────────────────────────────────

describe('score', () => {
  it('returns a number between 0 and 100', () => {
    const s = score('This is a simple sentence.');
    expect(s).toBeGreaterThanOrEqual(0);
    expect(s).toBeLessThanOrEqual(100);
  });

  it('scores AI sample higher than human sample', () => {
    const aiScore = score(loadFixture('ai-sample-1.txt'));
    const humanScore = score(loadFixture('human-sample-1.txt'));
    expect(aiScore).toBeGreaterThan(humanScore);
  });
});

// ─── Pattern Filtering ──────────────────────────────────

describe('pattern filtering', () => {
  it('can check only specific patterns', () => {
    const text = 'Additionally, this serves as a testament to excellence.';
    const full = analyze(text);
    const filtered = analyze(text, { patternsToCheck: [7] }); // Only AI vocab
    expect(filtered.findings.length).toBeLessThanOrEqual(full.findings.length);
    expect(filtered.findings.every((f) => f.patternId === 7)).toBe(true);
  });
});

// ─── Formatting ──────────────────────────────────────────

describe('formatting', () => {
  it('formatReport produces a string', () => {
    const result = analyze('This is a testament to great things.');
    const report = formatReport(result);
    expect(typeof report).toBe('string');
    expect(report).toContain('AI WRITING PATTERN ANALYSIS');
    expect(report).toContain('Score:');
  });

  it('formatJSON produces valid JSON', () => {
    const result = analyze('This is a testament to great things.');
    const json = formatJSON(result);
    const parsed = JSON.parse(json);
    expect(parsed).toHaveProperty('score');
  });

  it('formatMarkdown produces markdown', () => {
    const result = analyze('This is a testament to great things.');
    const md = formatMarkdown(result);
    expect(typeof md).toBe('string');
    expect(md).toContain('# AI writing pattern analysis');
    expect(md).toContain('**Score:');
  });
});

// ─── Individual Pattern Detection ────────────────────────

describe('pattern detection', () => {
  // 1. Significance inflation
  it('detects significance inflation', () => {
    const text =
      'This moment marks a pivotal shift in the evolution of technology, setting the stage for a key turning point.';
    const result = analyze(text, { patternsToCheck: [1] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.findings[0].patternId).toBe(1);
  });

  // 2. Notability name-dropping
  it('detects notability name-dropping', () => {
    const text = 'She maintains an active social media presence with millions of followers.';
    const result = analyze(text, { patternsToCheck: [2] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 3. Superficial -ing analyses
  it('detects superficial -ing analyses', () => {
    const text =
      "The building uses modern materials, showcasing the architect's vision and reflecting the community's values.";
    const result = analyze(text, { patternsToCheck: [3] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 4. Promotional language
  it('detects promotional language', () => {
    const text =
      'Nestled in the heart of downtown, this stunning venue boasts breathtaking views and renowned cuisine.';
    const result = analyze(text, { patternsToCheck: [4] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(3);
  });

  // 5. Vague attributions
  it('detects vague attributions', () => {
    const text =
      'Experts believe this is important. Industry reports suggest continued growth. Studies show improvement.';
    const result = analyze(text, { patternsToCheck: [5] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });

  // 6. Formulaic challenges
  it('detects formulaic challenges', () => {
    const text =
      'Despite its challenges, the city continues to thrive. Despite these obstacles, the future outlook remains positive.';
    const result = analyze(text, { patternsToCheck: [6] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 7. AI vocabulary
  it('detects AI vocabulary words', () => {
    const text =
      'Additionally, this showcases the vibrant tapestry of the evolving landscape, a testament to enduring innovation.';
    const result = analyze(text, { patternsToCheck: [7] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(4);
  });

  // 8. Copula avoidance
  it('detects copula avoidance', () => {
    const text =
      'The gallery serves as a space for art. The building boasts over 3000 square feet. It functions as a hub.';
    const result = analyze(text, { patternsToCheck: [8] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });

  // 9. Negative parallelisms
  it('detects negative parallelisms', () => {
    const text =
      "It's not just a tool, it's a revolution. Not only does it save time but also transforms workflows.";
    const result = analyze(text, { patternsToCheck: [9] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 10. Rule of three
  it('detects rule of three with abstract nouns', () => {
    const text =
      'The event promotes innovation, inspiration, and collaboration for increased motivation, dedication, and education.';
    const result = analyze(text, { patternsToCheck: [10] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 13. Em dash overuse
  it('detects em dash overuse', () => {
    const text =
      'The project — which started last year — has grown significantly — reaching new heights — and the team — a dedicated group — continues to push forward.';
    const result = analyze(text, { patternsToCheck: [13] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 14. Boldface overuse
  it('detects boldface overuse', () => {
    const text =
      'The **team** worked on **three** key **projects** using **modern** tools for **better** results.';
    const result = analyze(text, { patternsToCheck: [14] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 15. Inline-header lists
  it('detects inline-header lists', () => {
    const text =
      '- **Speed:** Loading is faster now.\n- **Quality:** Output quality improved.\n- **Adoption:** More users joined.';
    const result = analyze(text, { patternsToCheck: [15] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 16. Title Case headings
  it('detects Title Case headings', () => {
    const text =
      '## Strategic Negotiations And Global Partnerships\n\nSome content here.\n\n## Building A Better Tomorrow Today';
    const result = analyze(text, { patternsToCheck: [16] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 17. Emoji overuse
  it('detects emoji overuse in professional text', () => {
    const text =
      '🚀 Launch phase complete\n💡 Key insights discovered\n✅ Next steps defined\n🎯 Goals aligned';
    const result = analyze(text, { patternsToCheck: [17] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 18. Curly quotes
  it('detects curly quotes', () => {
    const text =
      'He said \u201Cthe project is on track\u201D but she replied \u201CI\u2019m not so sure.\u201D';
    const result = analyze(text, { patternsToCheck: [18] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(3);
  });

  // 19. Chatbot artifacts
  it('detects chatbot artifacts', () => {
    const text =
      'Here is an overview of the topic. I hope this helps! Let me know if you would like me to expand on any section.';
    const result = analyze(text, { patternsToCheck: [19] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });

  // 20. Cutoff disclaimers
  it('detects cutoff disclaimers', () => {
    const text =
      'While specific details are limited, based on available information the company was founded in the 1990s. As of my last training update, this was accurate.';
    const result = analyze(text, { patternsToCheck: [20] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 21. Sycophantic tone
  it('detects sycophantic tone', () => {
    const text =
      "Great question! You're absolutely right that this is complex. That's an excellent point about the economy.";
    const result = analyze(text, { patternsToCheck: [21] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });

  // 22. Filler phrases
  it('detects filler phrases', () => {
    const text =
      'In order to achieve this goal, due to the fact that resources are limited, the team has the ability to adapt.';
    const result = analyze(text, { patternsToCheck: [22] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });

  // 23. Excessive hedging
  it('detects excessive hedging', () => {
    const text =
      'It could potentially be true. One might possibly agree that things could conceivably improve.';
    const result = analyze(text, { patternsToCheck: [23] });
    expect(result.findings.length).toBeGreaterThan(0);
  });

  // 24. Generic conclusions
  it('detects generic conclusions', () => {
    const text =
      'The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence.';
    const result = analyze(text, { patternsToCheck: [24] });
    expect(result.findings.length).toBeGreaterThan(0);
    expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  });
});

// ─── AI Sample Full Analysis ─────────────────────────────

describe('full AI sample analysis', () => {
  it('detects many patterns in ai-sample-1.txt', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = analyze(text, { verbose: true });
    const categories = Object.entries(result.categories).filter(([, v]) => v.matches > 0);
    expect(categories.length).toBeGreaterThanOrEqual(4);
    expect(result.score).toBeGreaterThan(50);
    expect(result.totalMatches).toBeGreaterThan(15);
  });

  it('detects many patterns in ai-sample-2.txt', () => {
    const text = loadFixture('ai-sample-2.txt');
    const result = analyze(text);
    expect(result.score).toBeGreaterThan(30);
    expect(result.totalMatches).toBeGreaterThan(5);
  });
});


path: tests/calibration.test.js
content: /**
 * calibration.test.js — Calibration tests for the scoring engine.
 *
 * Known AI samples should score high, known human samples should score low.
 */

import { describe, it, expect } from 'vitest';
import { score } from '../src/analyzer.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function loadFixture(name) {
  return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf-8');
}

// ─── AI Samples — Should Score High ─────────────────────

describe('AI sample calibration', () => {
  it('ai-sample-1.txt scores 55+', () => {
    const text = loadFixture('ai-sample-1.txt');
    const s = score(text);
    expect(s).toBeGreaterThanOrEqual(55);
  });

  it('ai-sample-2.txt scores 30+ (moderate AI)', () => {
    const text = loadFixture('ai-sample-2.txt');
    const s = score(text);
    expect(s).toBeGreaterThanOrEqual(30);
  });

  it('classic chatbot output scores very high', () => {
    const text = `Great question! Here is a comprehensive overview of machine learning.

Machine learning serves as a transformative cornerstone of modern technology, marking a pivotal moment in the evolution of artificial intelligence. In today's rapidly evolving digital age, these groundbreaking tools are reshaping how organizations navigate the complexities of data-driven decision making.

It is worth noting that the landscape of AI continues to evolve at a breathtaking pace. Experts believe that machine learning plays a crucial role in fostering innovation and unleashing the potential of big data.

- **Speed:** Processing has been revolutionized, empowering teams to harness the power of real-time analytics.
- **Quality:** Output quality has been enhanced through multifaceted approaches to model training.
- **Adoption:** Industry reports suggest continued growth, underscoring the paramount importance of this technology.

Despite challenges, the future looks bright. Exciting times lie ahead as we embark on this journey toward excellence. I hope this helps! Let me know if you'd like me to delve into any section further.`;
    const s = score(text);
    expect(s).toBeGreaterThanOrEqual(60);
  });

  it('promotional AI text scores high', () => {
    const text = `Nestled in the heart of downtown, this stunning venue serves as a testament to architectural innovation. The breathtaking facility boasts world-class amenities and seamless integration of cutting-edge technology.

Renowned for its commitment to excellence, the establishment showcases a vibrant tapestry of cultural experiences. Industry observers have noted its pivotal role in reshaping the landscape of urban entertainment.

The comprehensive approach encompasses state-of-the-art design, fostering a culture of innovation while leveraging synergy between form and function. The future looks bright as exciting times lie ahead.`;
    const s = score(text);
    expect(s).toBeGreaterThanOrEqual(55);
  });

  it('hedging + filler AI text scores high', () => {
    const text = `It could potentially be argued that in order to navigate the complexities of modern software development, it is important to note that one must harness the power of robust frameworks. Due to the fact that the landscape is ever-evolving, teams need to embark on a journey of continuous improvement.

As we move forward, it goes without saying that leveraging cutting-edge tools plays a pivotal role. Needless to say, this comprehensive guide will help you unlock the potential of these transformative technologies.

In conclusion, the multifaceted challenges of today's digital age require a seamless approach. Without further ado, let us delve into the realm of best practices.`;
    const s = score(text);
    expect(s).toBeGreaterThanOrEqual(50);
  });
});

// ─── Human Samples — Should Score Low ───────────────────

describe('human sample calibration', () => {
  it('human-sample-1.txt scores under 30', () => {
    const text = loadFixture('human-sample-1.txt');
    const s = score(text);
    expect(s).toBeLessThan(30);
  });

  it('casual human writing scores low', () => {
    const text = `I tried three different coffee shops this week. The one on 5th Ave had the best espresso but terrible wifi. The place near the park was quiet enough to work but their cold brew tasted like it had been sitting out since Tuesday.

Ended up going back to my usual spot. Nothing fancy. The barista knows my order. Sometimes that matters more than fancy latte art.`;
    const s = score(text);
    expect(s).toBeLessThan(25);
  });

  it('technical human writing scores low', () => {
    const text = `The bug was in the connection pooling code. When you hit exactly 256 concurrent connections, the pool silently dropped new requests instead of queuing them. No error, no log, just a hung request.

Found it by adding a counter to the pool checkout method. Took about 3 hours of staring at tcpdump output before I thought to look there.

Fixed it with a bounded semaphore. PR is up. The test covers the edge case now.`;
    const s = score(text);
    expect(s).toBeLessThan(25);
  });

  it('opinionated human writing scores low', () => {
    const text = `Look, I get why people like TypeScript. It catches some real bugs at compile time. But the productivity tax is real, and nobody wants to talk about it.

Last week I spent 45 minutes trying to satisfy the type checker on a function that was obviously correct. The types were right, the logic was right, but some intersection type was confusing the compiler.

I still use it for big projects. But for scripts and prototypes? Just give me plain JavaScript.`;
    const s = score(text);
    expect(s).toBeLessThan(25);
  });

  it('narrative human writing scores low', () => {
    const text = `My grandfather built his own house in 1962. Took him two years, working weekends. The foundation is slightly off-level — you can tell if you put a marble on the kitchen floor. It rolls toward the east wall every time.

He never fixed it. Said it gave the house character. I think he just didn't want to jack up a house he'd already put a roof on.

The house is still standing. My aunt lives there now.`;
    const s = score(text);
    expect(s).toBeLessThan(25);
  });
});

// ─── Relative Ordering ──────────────────────────────────

describe('relative scoring', () => {
  it('AI text always scores higher than human text', () => {
    const aiText = loadFixture('ai-sample-1.txt');
    const humanText = loadFixture('human-sample-1.txt');

    const aiScore = score(aiText);
    const humanScore = score(humanText);

    expect(aiScore).toBeGreaterThan(humanScore);
    expect(aiScore - humanScore).toBeGreaterThan(20);
  });

  it('more AI patterns → higher score', () => {
    const light = 'The project has been interesting in scope. We worked hard on it last year.';
    const heavy =
      "Additionally, this groundbreaking project serves as a testament to innovation. In today's rapidly evolving landscape, it showcases the vibrant tapestry of modern technology, fostering seamless synergy. I hope this helps!";

    expect(score(heavy)).toBeGreaterThan(score(light));
  });
});


path: tests/edge-cases.test.js
content: /**
 * edge-cases.test.js — Edge case tests.
 *
 * Empty text, single word, unicode, non-English, very long text.
 */

import { describe, it, expect } from 'vitest';
import { analyze, score } from '../src/analyzer.js';
import { computeStats } from '../src/stats.js';

// ─── Empty / Minimal Input ───────────────────────────────

describe('empty and minimal input', () => {
  it('handles empty string', () => {
    const result = analyze('');
    expect(result.score).toBe(0);
    expect(result.totalMatches).toBe(0);
    expect(result.wordCount).toBe(0);
  });

  it('handles whitespace-only string', () => {
    const result = analyze('   \n\n\t  ');
    expect(result.score).toBe(0);
  });

  it('handles null', () => {
    const result = analyze(null);
    expect(result.score).toBe(0);
  });

  it('handles undefined', () => {
    const result = analyze(undefined);
    expect(result.score).toBe(0);
  });

  it('handles single word — score is low', () => {
    const result = analyze('hello');
    expect(result.score).toBeLessThanOrEqual(15);
    expect(result.wordCount).toBe(1);
  });

  it('handles single character — score is low', () => {
    const result = analyze('x');
    expect(result.score).toBeLessThanOrEqual(15);
  });

  it('handles number-only input — score is low', () => {
    const result = analyze('12345');
    expect(result.score).toBeLessThanOrEqual(15);
  });

  it('statistics handles empty string', () => {
    const stats = computeStats('');
    expect(stats.sentenceCount).toBe(0);
    expect(stats.wordCount).toBe(0);
  });

  it('statistics handles single word', () => {
    const stats = computeStats('hello');
    expect(stats.wordCount).toBe(1);
    expect(stats.typeTokenRatio).toBe(1);
  });
});

// ─── Unicode & Special Characters ────────────────────────

describe('unicode and special characters', () => {
  it('handles emoji text', () => {
    const result = analyze('🎉 Hello world! 🚀 Great day! ✅ Done!');
    expect(result.score).toBeGreaterThanOrEqual(0);
    expect(result.score).toBeLessThanOrEqual(100);
  });

  it('handles Chinese text', () => {
    const result = analyze('这是一个测试。人工智能正在改变世界。');
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles Japanese text', () => {
    const result = analyze('これはテストです。AIは世界を変えています。');
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles Arabic text', () => {
    const result = analyze('هذا اختبار. الذكاء الاصطناعي يغير العالم.');
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles mixed unicode and ASCII', () => {
    const text = 'The café is très bien. Über cool. Naïve approach.';
    const result = analyze(text);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles HTML entities', () => {
    const result = analyze('This &amp; that &lt;tag&gt; content.');
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles special whitespace characters', () => {
    const result = analyze('Hello\u00A0world\u2003test\u200Bhidden');
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('statistics handles non-English gracefully', () => {
    const stats = computeStats('这是一个测试。人工智能正在改变世界。');
    expect(stats.sentenceCount).toBeGreaterThanOrEqual(0);
  });
});

// ─── Very Long Text ──────────────────────────────────────

describe('very long text', () => {
  it('handles 1000 identical sentences', () => {
    const text = Array(1000).fill('The cat sat on the mat.').join(' ');
    const result = analyze(text);
    expect(result.score).toBeGreaterThanOrEqual(0);
    expect(result.score).toBeLessThanOrEqual(100);
  });

  it('handles text with thousands of newlines', () => {
    const text = Array(500).fill('Line of text.\n').join('');
    const result = analyze(text);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('statistics handles very long text', () => {
    const text = Array(500).fill('The cat sat on the mat.').join(' ');
    const stats = computeStats(text);
    expect(stats.wordCount).toBeGreaterThan(100);
    expect(stats.typeTokenRatio).toBeLessThan(0.1);
  });
});

// ─── Malformed Input ─────────────────────────────────────

describe('malformed input', () => {
  it('handles text with only punctuation — score is low', () => {
    const result = analyze('...!!!???---');
    expect(result.score).toBeLessThanOrEqual(15);
  });

  it('handles extremely long single word', () => {
    const word = 'a'.repeat(10000);
    const result = analyze(word);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles text with excessive whitespace', () => {
    const result = analyze('Hello     world     this     is     spaced');
    expect(result.wordCount).toBeGreaterThanOrEqual(4);
  });

  it('handles markdown-heavy text', () => {
    const text =
      '# Heading\n\n**bold** _italic_ ~~strike~~ `code`\n\n- item 1\n- item 2\n- item 3\n\n> blockquote\n\n```\ncode block\n```';
    const result = analyze(text);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('handles text with URLs', () => {
    const text =
      'Check out https://example.com and http://test.org/path?query=1&foo=bar for more info.';
    const result = analyze(text);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });
});

// ─── Score Bounds ────────────────────────────────────────

describe('score bounds', () => {
  it('score is always 0-100', () => {
    const inputs = [
      '',
      'hello',
      'The cat sat.',
      'Additionally, this serves as a testament.',
      'Great question! I hope this helps! Let me know!',
    ];

    for (const input of inputs) {
      const s = score(input);
      expect(s).toBeGreaterThanOrEqual(0);
      expect(s).toBeLessThanOrEqual(100);
    }
  });

  it('maximum AI text does not exceed 100', () => {
    const text = `Great question! Here is a comprehensive overview.

Additionally, this serves as a testament to the transformative tapestry of the evolving landscape. In today's rapidly evolving digital age, these groundbreaking tools — nestled at the forefront of innovation — are showcasing the vibrant interplay of technology, highlighting its pivotal role and underscoring the crucial importance of seamless synergy.

Experts believe it plays a crucial role. Studies show improvement. Industry reports suggest growth. Despite challenges, the ecosystem continues to thrive. It's not just a tool, it's a revolution.

In order to help, due to the fact that you asked, at this point in time, it is important to note that the future looks bright. Exciting times lie ahead. I hope this helps! Let me know if you'd like me to expand.`;

    const s = score(text);
    expect(s).toBeLessThanOrEqual(100);
    expect(s).toBeGreaterThanOrEqual(60);
  });
});


path: tests/fixtures/ai-sample-1.txt
content: Great question! Here is an overview of AI-assisted coding.

AI-assisted coding serves as an enduring testament to the transformative potential of large language models, marking a pivotal moment in the evolution of software development. In today's rapidly evolving technological landscape, these groundbreaking tools — nestled at the intersection of research and practice — are reshaping how engineers ideate, iterate, and deliver, underscoring their vital role in modern workflows.

At its core, the value proposition is clear: streamlining processes, enhancing collaboration, and fostering alignment. It's not just about autocomplete; it's about unlocking creativity at scale, ensuring that organizations can remain agile while delivering seamless, intuitive, and powerful experiences to users. The tool serves as a catalyst. The assistant functions as a partner. The system stands as a foundation for innovation.

Industry observers have noted that adoption has accelerated from hobbyist experiments to enterprise-wide rollouts, from solo developers to cross-functional teams. The technology has been featured in The New York Times, BBC, and The Verge. Additionally, the ability to generate documentation, tests, and refactors showcases how AI can contribute to better outcomes, highlighting the intricate interplay between automation and human judgment.

- 💡 **Speed:** Code generation is significantly faster, reducing friction and empowering developers.
- 🚀 **Quality:** Output quality has been enhanced through improved training, contributing to higher standards.
- ✅ **Adoption:** Usage continues to grow, reflecting broader industry trends.

While specific details are limited based on available information, it could potentially be argued that these tools might have some positive effect. Despite challenges typical of emerging technologies — including hallucinations, bias, and accountability — the ecosystem continues to thrive. In order to fully realize this potential, teams must align with best practices.

In conclusion, the future looks bright. Exciting times lie ahead as we continue this journey toward excellence. I hope this helps! Let me know if you'd like me to expand on any section.


path: tests/fixtures/ai-sample-2.txt
content: Nestled within the breathtaking region of the Pacific Northwest, Portland stands as a vibrant city with a rich cultural heritage and stunning natural beauty. The city boasts a thriving arts scene, showcasing local talent and fostering community engagement.

Portland's culinary landscape is equally impressive, featuring a diverse array of restaurants and food carts. The city's commitment to sustainability exemplifies its dedication to environmental stewardship, highlighting the intricate interplay between urban development and ecological preservation.

Experts believe the city plays a crucial role in the regional economy. Several sources have noted that Portland's tech sector has garnered significant attention, with industry reports suggesting continued growth. The city serves as a hub for innovation, enhancing its reputation as a must-visit destination for entrepreneurs.

Despite its challenges, including housing affordability and traffic congestion, Portland continues to thrive as an integral part of the Pacific Northwest's growth. The future looks bright for this remarkable city, and only time will tell what exciting developments lie ahead.


path: tests/fixtures/human-sample-1.txt
content: I keep going back and forth on AI coding tools. Last week I mass-accepted a bunch of Copilot suggestions in a config file and didn't notice until two days later that one of them referenced a package we'd already ripped out.

The thing is, they're genuinely useful for boilerplate. Test scaffolding, repetitive CRUD endpoints, config files you've written a hundred times before. I wrote a React form component last month that took maybe ten minutes with Copilot where it would've taken thirty without. Fine.

But I've also watched a junior dev on my team accept suggestions for three weeks straight and end up with code that worked, passed CI, and was completely unmaintainable. Nobody caught it until code review, and by then it was a 2000-line PR.

The productivity numbers are squishy. GitHub says 30% of suggestions get accepted, but that's not the same as 30% improvement. Some of those accepted suggestions still need editing. The Uplevel study from last year found basically no difference in PR throughput between teams with and without Copilot.

I still use it. I just don't trust it.


path: tests/humanizer.test.js
content: /**
 * humanizer.test.js — Tests for the humanization engine.
 */

import { describe, it, expect } from 'vitest';
import { humanize, autoFix, formatSuggestions } from '../src/humanizer.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function loadFixture(name) {
  return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf-8');
}

// ─── autoFix ─────────────────────────────────────────────

describe('autoFix', () => {
  it('replaces curly double quotes with straight quotes', () => {
    const { text, fixes } = autoFix('He said \u201Chello\u201D to her.');
    expect(text).toBe('He said "hello" to her.');
    expect(fixes.length).toBeGreaterThan(0);
  });

  it('replaces curly single quotes with straight quotes', () => {
    const { text } = autoFix('It\u2019s a fine day.');
    expect(text).toBe("It's a fine day.");
  });

  it('replaces "in order to" with "to"', () => {
    const { text } = autoFix('In order to succeed, we must work hard.');
    expect(text).toContain('to succeed');
    expect(text).not.toContain('In order to');
  });

  it('replaces "due to the fact that" with "because"', () => {
    const { text } = autoFix('We stopped due to the fact that it was raining.');
    expect(text).toContain('because');
    expect(text).not.toContain('due to the fact that');
  });

  it('replaces "at this point in time" with "now"', () => {
    const { text } = autoFix('At this point in time, we are ready.');
    expect(text).toContain('now');
  });

  it('replaces "in the event that" with "if"', () => {
    const { text } = autoFix('In the event that you need help, call us.');
    expect(text).toContain('if');
    expect(text).not.toContain('In the event that');
  });

  it('replaces "has the ability to" with "can"', () => {
    const { text } = autoFix('The system has the ability to process data.');
    expect(text).toContain('can');
  });

  it('removes chatbot opening artifacts', () => {
    const { text, fixes } = autoFix('Great question! Here is the answer to your question.');
    expect(text).not.toContain('Great question!');
    expect(fixes.some((f) => f.includes('chatbot'))).toBe(true);
  });

  it('removes chatbot closing artifacts', () => {
    const { text, fixes } = autoFix('The answer is 42. I hope this helps!');
    expect(text).not.toContain('I hope this helps');
    expect(fixes.some((f) => f.includes('chatbot'))).toBe(true);
  });

  it('handles text with no fixable issues', () => {
    const { text, fixes } = autoFix('The cat sat on the mat.');
    expect(text).toBe('The cat sat on the mat.');
    expect(fixes.length).toBe(0);
  });

  it('applies multiple fixes in one pass', () => {
    const input =
      'Great question! In order to help, due to the fact that you asked, here\u2019s the answer. I hope this helps!';
    const { text, fixes } = autoFix(input);
    expect(fixes.length).toBeGreaterThanOrEqual(3);
    expect(text).not.toContain('In order to');
    expect(text).not.toContain('\u2019');
  });
});

// ─── humanize ────────────────────────────────────────────

describe('humanize', () => {
  it('returns a valid suggestion object', () => {
    const result = humanize('This is a testament to great things.');
    expect(result).toHaveProperty('score');
    expect(result).toHaveProperty('critical');
    expect(result).toHaveProperty('important');
    expect(result).toHaveProperty('minor');
    expect(result).toHaveProperty('guidance');
    expect(result).toHaveProperty('totalIssues');
    expect(result).toHaveProperty('styleTips');
  });

  it('categorizes issues by severity', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    expect(result.critical.length).toBeGreaterThan(0);
    expect(result.important.length).toBeGreaterThan(0);
  });

  it('provides guidance tips', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    expect(result.guidance.length).toBeGreaterThan(0);
    expect(result.guidance.some((g) => typeof g === 'string' && g.length > 10)).toBe(true);
  });

  it('returns autofix results when requested', () => {
    const text = 'In order to help, I hope this helps!';
    const result = humanize(text, { autofix: true });
    expect(result.autofix).not.toBeNull();
    expect(result.autofix.text).not.toContain('In order to');
    expect(result.autofix.fixes.length).toBeGreaterThan(0);
  });

  it('returns null autofix when not requested', () => {
    const result = humanize('Some text here.', { autofix: false });
    expect(result.autofix).toBeNull();
  });

  it('scores human text low', () => {
    const text = loadFixture('human-sample-1.txt');
    const result = humanize(text);
    expect(result.score).toBeLessThan(30);
  });

  it('each suggestion has required fields', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    const allSuggestions = [...result.critical, ...result.important, ...result.minor];
    for (const s of allSuggestions) {
      expect(s).toHaveProperty('pattern');
      expect(s).toHaveProperty('patternId');
      expect(s).toHaveProperty('category');
      expect(s).toHaveProperty('suggestion');
      expect(s).toHaveProperty('line');
    }
  });

  it('includes style tips for AI-like text', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    expect(result.styleTips).toBeDefined();
    expect(Array.isArray(result.styleTips)).toBe(true);
  });
});

// ─── formatSuggestions ───────────────────────────────────

describe('formatSuggestions', () => {
  it('produces readable output', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    const output = formatSuggestions(result);
    expect(typeof output).toBe('string');
    expect(output).toContain('HUMANIZATION SUGGESTIONS');
    expect(output).toContain('AI Score:');
  });

  it('includes guidance section', () => {
    const text = loadFixture('ai-sample-1.txt');
    const result = humanize(text);
    const output = formatSuggestions(result);
    expect(output).toContain('GUIDANCE');
  });
});


path: tests/performance.test.js
content: /**
 * performance.test.js — Benchmark tests.
 *
 * Analyze 10K words in under 1 second.
 */

import { describe, it, expect } from 'vitest';
import { analyze } from '../src/analyzer.js';
import { computeStats } from '../src/stats.js';

/**
 * Generate a large text block of approximately the target word count.
 */
function generateLargeText(targetWords) {
  const paragraphs = [
    "The project serves as a testament to innovation and transformative technology. In today's rapidly evolving landscape, these groundbreaking tools are reshaping how organizations navigate complexities.",
    'I tried three different approaches last week. The first one worked but was slow. The second broke in production. Third time was the charm — simple solution, no fancy tricks.',
    'Additionally, the comprehensive framework showcases seamless integration with cutting-edge platforms. Experts believe this plays a crucial role in fostering synergy across multifaceted teams.',
    'Found the bug at 2am. It was a race condition in the connection pool. Added a mutex, wrote a test, went to bed. PR got merged the next morning.',
    'It is worth noting that the landscape of modern software continues to evolve at a breathtaking pace. Despite challenges, the future looks bright as exciting times lie ahead.',
    "The API returns JSON. You POST to /users with a name and email. It gives you back an ID. That's it. No magic, just a REST endpoint.",
  ];

  let text = '';
  let words = 0;
  let i = 0;
  while (words < targetWords) {
    text += paragraphs[i % paragraphs.length] + '\n\n';
    words += paragraphs[i % paragraphs.length].split(/\s+/).length;
    i++;
  }
  return text;
}

describe('performance', () => {
  it('analyzes 10K words in under 1 second', () => {
    const text = generateLargeText(10000);
    const wordCount = text.split(/\s+/).filter(Boolean).length;
    expect(wordCount).toBeGreaterThanOrEqual(9000);

    const start = performance.now();
    const result = analyze(text);
    const elapsed = performance.now() - start;

    expect(elapsed).toBeLessThan(1000);
    expect(result.score).toBeGreaterThanOrEqual(0);
    expect(result.score).toBeLessThanOrEqual(100);
  });

  it('computes statistics on 10K words in under 500ms', () => {
    const text = generateLargeText(10000);

    const start = performance.now();
    const stats = computeStats(text);
    const elapsed = performance.now() - start;

    expect(elapsed).toBeLessThan(500);
    expect(stats.wordCount).toBeGreaterThan(5000);
  });

  it('handles 50K words without crashing', () => {
    const text = generateLargeText(50000);

    const start = performance.now();
    const result = analyze(text);
    const elapsed = performance.now() - start;

    expect(elapsed).toBeLessThan(5000);
    expect(result.score).toBeGreaterThanOrEqual(0);
  });

  it('many short analyses complete quickly (batch)', () => {
    const texts = Array.from(
      { length: 100 },
      (_, i) => `This is test text number ${i}. It has a few sentences. Nothing special here.`,
    );

    const start = performance.now();
    for (const text of texts) {
      analyze(text);
    }
    const elapsed = performance.now() - start;

    expect(elapsed).toBeLessThan(2000);
  });
});


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector:  ()