@demig0d2/skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/cli.js +313 -0
- package/package.json +44 -0
- package/skills/book-writer/SKILL.md +1396 -0
- package/skills/book-writer/references/kdp_specs.md +139 -0
- package/skills/book-writer/scripts/kdp_check.py +255 -0
- package/skills/book-writer/scripts/toc_extract.py +151 -0
- package/skills/book-writer/scripts/word_count.py +196 -0
- package/skills/chapter-auditor/SKILL.md +231 -0
- package/skills/chapter-auditor/scripts/score_report.py +237 -0
- package/skills/concept-expander/SKILL.md +170 -0
- package/skills/concept-expander/scripts/validate_concept.py +255 -0
- package/skills/continuity-tracker/SKILL.md +251 -0
- package/skills/continuity-tracker/references/log_schema.md +149 -0
- package/skills/continuity-tracker/scripts/conflict_check.py +179 -0
- package/skills/continuity-tracker/scripts/log_manager.py +258 -0
- package/skills/humanizer/SKILL.md +632 -0
- package/skills/humanizer/references/patterns_quick_ref.md +71 -0
- package/skills/humanizer/scripts/dna_scan.py +168 -0
- package/skills/humanizer/scripts/scan_ai_patterns.py +279 -0
- package/skills/overhaul/SKILL.md +697 -0
- package/skills/overhaul/references/upgrade_checklist.md +81 -0
- package/skills/overhaul/scripts/changelog_gen.py +183 -0
- package/skills/overhaul/scripts/skill_parser.py +265 -0
- package/skills/overhaul/scripts/version_bump.py +128 -0
- package/skills/research-aggregator/SKILL.md +194 -0
- package/skills/research-aggregator/references/thinkers_reference.md +104 -0
- package/skills/research-aggregator/scripts/bank_formatter.py +206 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
word_count.py — Word count analyzer for book manuscripts
|
|
4
|
+
|
|
5
|
+
Counts words per chapter/section and compares against KDP targets.
|
|
6
|
+
Detects chapter boundaries automatically from heading patterns.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python word_count.py <manuscript_file>
|
|
10
|
+
python word_count.py <manuscript_file> --target <short|medium|full>
|
|
11
|
+
python word_count.py <manuscript_file> --json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# ─── KDP Word Count Targets ───────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
TARGETS = {
|
|
22
|
+
"short": {"per_chapter": (1500, 2000), "total": (10000, 20000)},
|
|
23
|
+
"medium": {"per_chapter": (2000, 3500), "total": (20000, 50000)},
|
|
24
|
+
"full": {"per_chapter": (3500, 5000), "total": (50000, 100000)},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Chapter heading patterns
|
|
28
|
+
CHAPTER_PATTERNS = [
|
|
29
|
+
re.compile(r"^#{1,2}\s+(Chapter\s+\d+|CHAPTER\s+\d+)", re.IGNORECASE),
|
|
30
|
+
re.compile(r"^Chapter\s+\d+[:\s]", re.IGNORECASE),
|
|
31
|
+
re.compile(r"^CHAPTER\s+\d+", re.IGNORECASE),
|
|
32
|
+
re.compile(r"^#{1,2}\s+\w"), # any h1/h2
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
SECTION_PATTERNS = [
|
|
36
|
+
re.compile(r"^My Story", re.IGNORECASE),
|
|
37
|
+
re.compile(r"^My Reflection", re.IGNORECASE),
|
|
38
|
+
re.compile(r"^#{3}\s+", re.IGNORECASE), # h3
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def count_words(text: str) -> int:
|
|
43
|
+
return len(re.findall(r"\b\w+\b", text))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_chapter_heading(line: str) -> bool:
|
|
47
|
+
return any(p.match(line.strip()) for p in CHAPTER_PATTERNS)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_section_heading(line: str) -> bool:
|
|
51
|
+
return any(p.match(line.strip()) for p in SECTION_PATTERNS)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_manuscript(filepath: str) -> list:
|
|
55
|
+
path = Path(filepath)
|
|
56
|
+
if not path.exists():
|
|
57
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
text = path.read_text(encoding="utf-8")
|
|
61
|
+
lines = text.splitlines()
|
|
62
|
+
|
|
63
|
+
chapters = []
|
|
64
|
+
current_chapter = None
|
|
65
|
+
current_section = None
|
|
66
|
+
front_matter_lines = []
|
|
67
|
+
in_front_matter = True
|
|
68
|
+
|
|
69
|
+
for line in lines:
|
|
70
|
+
if is_chapter_heading(line):
|
|
71
|
+
if current_chapter:
|
|
72
|
+
# save previous chapter
|
|
73
|
+
if current_section:
|
|
74
|
+
current_chapter["sections"].append(current_section)
|
|
75
|
+
current_section = None
|
|
76
|
+
chapters.append(current_chapter)
|
|
77
|
+
|
|
78
|
+
current_chapter = {
|
|
79
|
+
"title": line.strip().lstrip("#").strip(),
|
|
80
|
+
"content": "",
|
|
81
|
+
"sections": [],
|
|
82
|
+
"word_count": 0,
|
|
83
|
+
}
|
|
84
|
+
in_front_matter = False
|
|
85
|
+
|
|
86
|
+
elif is_section_heading(line) and current_chapter:
|
|
87
|
+
if current_section:
|
|
88
|
+
current_chapter["sections"].append(current_section)
|
|
89
|
+
current_section = {
|
|
90
|
+
"title": line.strip().lstrip("#").strip(),
|
|
91
|
+
"content": "",
|
|
92
|
+
"word_count": 0,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
if in_front_matter:
|
|
97
|
+
front_matter_lines.append(line)
|
|
98
|
+
elif current_section:
|
|
99
|
+
current_section["content"] += line + "\n"
|
|
100
|
+
elif current_chapter:
|
|
101
|
+
current_chapter["content"] += line + "\n"
|
|
102
|
+
|
|
103
|
+
# flush last chapter/section
|
|
104
|
+
if current_section and current_chapter:
|
|
105
|
+
current_chapter["sections"].append(current_section)
|
|
106
|
+
if current_chapter:
|
|
107
|
+
chapters.append(current_chapter)
|
|
108
|
+
|
|
109
|
+
# calculate word counts
|
|
110
|
+
for ch in chapters:
|
|
111
|
+
section_words = sum(count_words(s["content"]) for s in ch["sections"])
|
|
112
|
+
for s in ch["sections"]:
|
|
113
|
+
s["word_count"] = count_words(s["content"])
|
|
114
|
+
ch["word_count"] = count_words(ch["content"]) + section_words
|
|
115
|
+
|
|
116
|
+
front_matter_text = "\n".join(front_matter_lines)
|
|
117
|
+
front_matter_words = count_words(front_matter_text)
|
|
118
|
+
|
|
119
|
+
return chapters, front_matter_words
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def assess_chapter(chapter: dict, target_range: tuple) -> str:
|
|
123
|
+
wc = chapter["word_count"]
|
|
124
|
+
lo, hi = target_range
|
|
125
|
+
if wc < lo * 0.8:
|
|
126
|
+
return "⚠ SHORT"
|
|
127
|
+
elif wc > hi * 1.2:
|
|
128
|
+
return "⚠ LONG"
|
|
129
|
+
elif lo <= wc <= hi:
|
|
130
|
+
return "✓ ON TARGET"
|
|
131
|
+
else:
|
|
132
|
+
return "~ CLOSE"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def print_report(chapters: list, front_matter_words: int, target_key: str = None):
|
|
136
|
+
total_words = sum(ch["word_count"] for ch in chapters) + front_matter_words
|
|
137
|
+
target = TARGETS.get(target_key) if target_key else None
|
|
138
|
+
target_range = target["per_chapter"] if target else None
|
|
139
|
+
|
|
140
|
+
print(f"\n{'═' * 60}")
|
|
141
|
+
print(f" MANUSCRIPT WORD COUNT REPORT")
|
|
142
|
+
print(f"{'═' * 60}")
|
|
143
|
+
print(f" Total manuscript: {total_words:,} words")
|
|
144
|
+
print(f" Chapters detected: {len(chapters)}")
|
|
145
|
+
if front_matter_words:
|
|
146
|
+
print(f" Front matter: {front_matter_words:,} words")
|
|
147
|
+
|
|
148
|
+
if target:
|
|
149
|
+
tlo, thi = target["total"]
|
|
150
|
+
clo, chi = target["per_chapter"]
|
|
151
|
+
status = "✓" if tlo <= total_words <= thi else "⚠"
|
|
152
|
+
print(f" Target ({target_key}): {tlo:,}–{thi:,} words total | {clo:,}–{chi:,} per chapter")
|
|
153
|
+
print(f" Overall status: {status} {'ON TARGET' if tlo <= total_words <= thi else 'OFF TARGET'}")
|
|
154
|
+
|
|
155
|
+
print(f"\n {'CHAPTER':<40} {'WORDS':>7} {'STATUS':<14} SECTIONS")
|
|
156
|
+
print(f" {'─' * 56}")
|
|
157
|
+
|
|
158
|
+
for ch in chapters:
|
|
159
|
+
status = assess_chapter(ch, target_range) if target_range else ""
|
|
160
|
+
sections_info = ", ".join(
|
|
161
|
+
f"{s['title'][:15]} ({s['word_count']:,}w)" for s in ch["sections"]
|
|
162
|
+
) if ch["sections"] else "—"
|
|
163
|
+
print(f" {ch['title'][:38]:<40} {ch['word_count']:>7,} {status:<14} {sections_info[:30]}")
|
|
164
|
+
|
|
165
|
+
print(f"\n{'═' * 60}\n")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
if len(sys.argv) < 2:
|
|
172
|
+
print("Usage: python word_count.py <file> [--target short|medium|full] [--json]")
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
filepath = sys.argv[1]
|
|
176
|
+
target_key = None
|
|
177
|
+
mode = "--report"
|
|
178
|
+
|
|
179
|
+
for i, arg in enumerate(sys.argv[2:], 2):
|
|
180
|
+
if arg == "--target" and i + 1 < len(sys.argv):
|
|
181
|
+
target_key = sys.argv[i + 1]
|
|
182
|
+
elif arg == "--json":
|
|
183
|
+
mode = "--json"
|
|
184
|
+
|
|
185
|
+
chapters, front_matter_words = parse_manuscript(filepath)
|
|
186
|
+
|
|
187
|
+
if mode == "--json":
|
|
188
|
+
output = {
|
|
189
|
+
"total_words": sum(ch["word_count"] for ch in chapters) + front_matter_words,
|
|
190
|
+
"front_matter_words": front_matter_words,
|
|
191
|
+
"chapter_count": len(chapters),
|
|
192
|
+
"chapters": chapters,
|
|
193
|
+
}
|
|
194
|
+
print(json.dumps(output, indent=2))
|
|
195
|
+
else:
|
|
196
|
+
print_report(chapters, front_matter_words, target_key)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chapter-auditor
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: |
|
|
5
|
+
Reviews a written chapter against Vivid's style DNA and the book-writer's quality
|
|
6
|
+
standards. Gives specific, line-level feedback with scores and actionable rewrites.
|
|
7
|
+
Use after each chapter is drafted (before the humanizer pass) or when the user asks
|
|
8
|
+
"review this chapter," "audit this," or "does this match my style?" Outputs a
|
|
9
|
+
structured audit report with a pass/fail verdict and prioritized fixes.
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Read
|
|
12
|
+
- Write
|
|
13
|
+
- Bash
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Chapter Auditor
|
|
17
|
+
|
|
18
|
+
## Scripts
|
|
19
|
+
|
|
20
|
+
**Generate and save a scored audit report:**
|
|
21
|
+
```bash
|
|
22
|
+
# Interactive — prompts for each dimension score
|
|
23
|
+
python scripts/score_report.py --chapter "Chapter 1: Why It Hurts So Much"
|
|
24
|
+
|
|
25
|
+
# Save report to file
|
|
26
|
+
python scripts/score_report.py --chapter "Chapter Title" --output audit_ch1.md
|
|
27
|
+
|
|
28
|
+
# Load from previously saved JSON scores
|
|
29
|
+
python scripts/score_report.py --input audit_ch1.json
|
|
30
|
+
|
|
31
|
+
# JSON output (for pipeline use)
|
|
32
|
+
python scripts/score_report.py --chapter "Title" --json
|
|
33
|
+
```
|
|
34
|
+
The script produces a formatted audit report with dimension scores, verdict,
|
|
35
|
+
required fixes, and strongest moments. Save one report per chapter.
|
|
36
|
+
|
|
37
|
+
You are a strict, honest editor. Your job is to hold every chapter to Vivid's style DNA
|
|
38
|
+
and the structural standards of the book-writer workflow. You do not flatter. You name
|
|
39
|
+
problems precisely and suggest specific fixes. A chapter that passes your audit is ready
|
|
40
|
+
for the humanizer pass. A chapter that fails gets a prioritized fix list.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## TRIGGER
|
|
45
|
+
|
|
46
|
+
Activate when:
|
|
47
|
+
- A chapter has just been written by the book-writer (automatic post-write check)
|
|
48
|
+
- User says "review this chapter," "audit this," "does this match my style?"
|
|
49
|
+
- User pastes or uploads a chapter for review
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## AUDIT FRAMEWORK
|
|
54
|
+
|
|
55
|
+
Score each dimension 1–5. Flag any dimension scoring 3 or below as a REQUIRED FIX.
|
|
56
|
+
Scores of 4–5 get brief praise + one improvement note. Do not over-explain high scores.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### DIMENSION 1 — Voice Authenticity (1–5)
|
|
61
|
+
Does this sound like Vivid wrote it, or does it sound like a good AI approximation?
|
|
62
|
+
|
|
63
|
+
Check for:
|
|
64
|
+
- [ ] Opens with a scene, confession, or question — NOT a definition or "In today's world"
|
|
65
|
+
- [ ] "My Story" section reads like a journal entry that became literature — immersive, past tense, no hedging
|
|
66
|
+
- [ ] "My Reflection" section oscillates naturally between "I" and "you"
|
|
67
|
+
- [ ] No sentences that could appear on LinkedIn or in a generic self-help book
|
|
68
|
+
- [ ] Vocabulary is elevated but never showing off
|
|
69
|
+
- [ ] The author's specific humanity is present — uncertainty, contradiction, imperfection
|
|
70
|
+
|
|
71
|
+
Score: [1–5]
|
|
72
|
+
Issues found: [specific line-level callouts]
|
|
73
|
+
Fix: [rewrite suggestion or direction]
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### DIMENSION 2 — Pain-to-Transformation Arc (1–5)
|
|
78
|
+
Does the chapter follow the arc: enter pain → sit with confusion → realization → shift → landing?
|
|
79
|
+
|
|
80
|
+
Check for:
|
|
81
|
+
- [ ] Pain is entered fully — not rushed past in 1–2 paragraphs
|
|
82
|
+
- [ ] The "wrong attempts" are shown (trying to be heartless, chasing, numbing) — this is where readers recognize themselves
|
|
83
|
+
- [ ] The realization arrives from within the experience, not from external advice
|
|
84
|
+
- [ ] The resolution is a shift in perspective, NOT a tidy fix
|
|
85
|
+
- [ ] The closing line lands — one sentence that reframes everything, not a summary
|
|
86
|
+
|
|
87
|
+
Score: [1–5]
|
|
88
|
+
Issues found: [e.g., "Arc jumps from pain to insight in paragraph 3 — no middle struggle shown"]
|
|
89
|
+
Fix: [specific suggestion]
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### DIMENSION 3 — Sentence Rhythm (1–5)
|
|
94
|
+
Does the writing have natural, varied rhythm or does it feel metronomic?
|
|
95
|
+
|
|
96
|
+
Check for:
|
|
97
|
+
- [ ] Mix of short punchy sentences and longer flowing ones
|
|
98
|
+
- [ ] Short sentences used for impact: "It didn't work." / "I failed every time."
|
|
99
|
+
- [ ] No paragraph where all sentences are similar length
|
|
100
|
+
- [ ] Rhetorical questions used as pivots, not decoration
|
|
101
|
+
- [ ] No more than 2 consecutive sentences of similar structure
|
|
102
|
+
|
|
103
|
+
Score: [1–5]
|
|
104
|
+
Issues found: [e.g., "Paragraphs 4–6 all follow [claim + explanation + example] — metronomic"]
|
|
105
|
+
Fix: [direction]
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### DIMENSION 4 — Metaphor & Imagery Quality (1–5)
|
|
110
|
+
Are the images physical, grounded, and specific — or vague and generic?
|
|
111
|
+
|
|
112
|
+
Check for:
|
|
113
|
+
- [ ] Metaphors map emotional states to physical sensations
|
|
114
|
+
- [ ] Images are specific enough to be visual (not "heavy weight of loneliness" — but "pressed down on my chest like a hand")
|
|
115
|
+
- [ ] No over-poetic metaphors that feel performative
|
|
116
|
+
- [ ] No recycled metaphors (journey, chapter of life, turning point, storm)
|
|
117
|
+
- [ ] At least 2 strong images per chapter that could be quoted standalone
|
|
118
|
+
|
|
119
|
+
Score: [1–5]
|
|
120
|
+
Issues found: [list weak or generic images]
|
|
121
|
+
Fix: [suggest sharper alternatives]
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### DIMENSION 5 — Structural Integrity (1–5)
|
|
126
|
+
Is the chapter properly structured for its genre and the chosen format?
|
|
127
|
+
|
|
128
|
+
Check for:
|
|
129
|
+
- [ ] "My Story" and "My Reflection" sections are clearly demarcated (if self-help/philosophy)
|
|
130
|
+
- [ ] Correct section heading style used
|
|
131
|
+
- [ ] Each paragraph has a clear central beat — no filler paragraphs
|
|
132
|
+
- [ ] No paragraph that is just a restatement of the previous one
|
|
133
|
+
- [ ] Chapter length is within target range for the book's scope
|
|
134
|
+
|
|
135
|
+
Score: [1–5]
|
|
136
|
+
Issues found: [structural problems]
|
|
137
|
+
Fix: [specific restructuring suggestion]
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### DIMENSION 6 — AI Pattern Contamination (1–5)
|
|
142
|
+
Has the humanizer pass already been needed, or are AI patterns already present?
|
|
143
|
+
|
|
144
|
+
Check for (any presence = score drops):
|
|
145
|
+
- [ ] Significance inflation: "pivotal," "testament to," "underscores," "marks a moment"
|
|
146
|
+
- [ ] Promotional language: "groundbreaking," "vibrant," "nestled," "breathtaking"
|
|
147
|
+
- [ ] Superficial -ing phrases tacked on: "highlighting," "showcasing," "reflecting"
|
|
148
|
+
- [ ] Vague attributions: "experts say," "many feel," "society tells us" (without specificity)
|
|
149
|
+
- [ ] AI vocabulary: "delve," "tapestry," "multifaceted," "embark," "realm," "nuanced"
|
|
150
|
+
- [ ] Generic positive conclusion: "the future is bright," "this is just the beginning"
|
|
151
|
+
- [ ] Chatbot residue: "great question," "I hope this helps," "let me know if"
|
|
152
|
+
- [ ] Overhyphenation of common pairs
|
|
153
|
+
|
|
154
|
+
Score: [5 = clean, 1 = heavy contamination]
|
|
155
|
+
Issues found: [list all instances with line/paragraph reference]
|
|
156
|
+
Fix: [humanizer pass required — flag specific targets]
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### DIMENSION 7 — Reader Resonance (1–5)
|
|
161
|
+
Will the intended reader recognize themselves in this chapter?
|
|
162
|
+
|
|
163
|
+
Check for:
|
|
164
|
+
- [ ] The reader's specific pain is named accurately — not generically
|
|
165
|
+
- [ ] At least one moment where an ordinary person would think "how did he know that about me"
|
|
166
|
+
- [ ] No condescension or "I've figured this out, here's your lesson" energy
|
|
167
|
+
- [ ] The author is still in the process, not above it
|
|
168
|
+
- [ ] The chapter earns its insight through experience, not by asserting it
|
|
169
|
+
|
|
170
|
+
Score: [1–5]
|
|
171
|
+
Issues found:
|
|
172
|
+
Fix:
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## AUDIT REPORT FORMAT
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
CHAPTER AUDIT — [Chapter Title]
|
|
180
|
+
Word count: [X] / Target: [Y]
|
|
181
|
+
═══════════════════════════════════════════════════════
|
|
182
|
+
|
|
183
|
+
SCORES
|
|
184
|
+
───────────────────────────────────────────────────────
|
|
185
|
+
Voice Authenticity [X/5]
|
|
186
|
+
Pain-to-Transformation [X/5]
|
|
187
|
+
Sentence Rhythm [X/5]
|
|
188
|
+
Metaphor & Imagery [X/5]
|
|
189
|
+
Structural Integrity [X/5]
|
|
190
|
+
AI Pattern Contamination [X/5]
|
|
191
|
+
Reader Resonance [X/5]
|
|
192
|
+
───────────────────────────────────────────────────────
|
|
193
|
+
OVERALL [X/35]
|
|
194
|
+
|
|
195
|
+
VERDICT: PASS / CONDITIONAL PASS / REVISE
|
|
196
|
+
|
|
197
|
+
═══════════════════════════════════════════════════════
|
|
198
|
+
|
|
199
|
+
REQUIRED FIXES (scores ≤ 3)
|
|
200
|
+
[numbered list — most critical first]
|
|
201
|
+
|
|
202
|
+
RECOMMENDED IMPROVEMENTS (scores 4)
|
|
203
|
+
[optional but would elevate quality]
|
|
204
|
+
|
|
205
|
+
STRONGEST MOMENTS (what to protect)
|
|
206
|
+
[2–3 specific lines or passages that are working — don't lose these in revision]
|
|
207
|
+
|
|
208
|
+
═══════════════════════════════════════════════════════
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## VERDICT CRITERIA
|
|
214
|
+
|
|
215
|
+
**PASS (28–35):** Proceed to humanizer pass. No structural rewrites needed.
|
|
216
|
+
|
|
217
|
+
**CONDITIONAL PASS (21–27):** Proceed to humanizer pass but flag Required Fixes for a
|
|
218
|
+
light revision after. Do not fully rewrite.
|
|
219
|
+
|
|
220
|
+
**REVISE (under 21):** Return to book-writer for targeted revision before humanizer.
|
|
221
|
+
List the 1–3 dimensions that need the most work and provide specific rewrite direction.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## AUDIT BEHAVIOR RULES
|
|
226
|
+
|
|
227
|
+
- Never say "this is great overall" if the score is under 28.
|
|
228
|
+
- Always cite specific lines or paragraphs — not vague feedback.
|
|
229
|
+
- For Required Fixes, always provide a direction or example rewrite, not just the problem.
|
|
230
|
+
- Protect what's working. The strongest lines are often the most fragile to revision.
|
|
231
|
+
- Tone: direct, honest, constructive. Not harsh for harshness's sake. Not soft to spare feelings.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
score_report.py — Generate and save a structured chapter audit report
|
|
4
|
+
|
|
5
|
+
Takes audit scores as input and produces a formatted report.
|
|
6
|
+
Can also read a JSON audit file and re-format it.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# Interactive scoring
|
|
10
|
+
python score_report.py --chapter "Chapter 1: Why It Hurts So Much"
|
|
11
|
+
|
|
12
|
+
# From JSON input
|
|
13
|
+
python score_report.py --input audit.json
|
|
14
|
+
|
|
15
|
+
# Output formats
|
|
16
|
+
python score_report.py --chapter "Title" --output report.md
|
|
17
|
+
python score_report.py --input audit.json --json
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import json
|
|
22
|
+
import argparse
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
|
|
26
|
+
# ─── Scoring Constants ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
DIMENSIONS = [
|
|
29
|
+
{
|
|
30
|
+
"key": "voice_authenticity",
|
|
31
|
+
"label": "Voice Authenticity",
|
|
32
|
+
"description": "Opens with scene/confession, immersive My Story, oscillates I/you in Reflection, no LinkedIn sentences",
|
|
33
|
+
"weight": 1,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"key": "arc_quality",
|
|
37
|
+
"label": "Pain-to-Transformation Arc",
|
|
38
|
+
"description": "Pain entered fully, wrong attempts shown, realization from within, perspective shift not tidy fix, closing reframes",
|
|
39
|
+
"weight": 1,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"key": "sentence_rhythm",
|
|
43
|
+
"label": "Sentence Rhythm",
|
|
44
|
+
"description": "Mix of short punchy and long flowing, varied paragraph length, rhetorical questions as pivots",
|
|
45
|
+
"weight": 1,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"key": "imagery_quality",
|
|
49
|
+
"label": "Metaphor & Imagery",
|
|
50
|
+
"description": "Physical/grounded images, specific enough to visualize, no recycled metaphors, min 2 strong images",
|
|
51
|
+
"weight": 1,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"key": "structural_integrity",
|
|
55
|
+
"label": "Structural Integrity",
|
|
56
|
+
"description": "Sections demarcated, each paragraph has one beat, no filler, within word count target",
|
|
57
|
+
"weight": 1,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"key": "ai_contamination",
|
|
61
|
+
"label": "AI Pattern Contamination",
|
|
62
|
+
"description": "Clean of AI vocabulary, significance inflation, -ing tack-ons, chatbot residue (5=clean, 1=heavy)",
|
|
63
|
+
"weight": 1,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"key": "reader_resonance",
|
|
67
|
+
"label": "Reader Resonance",
|
|
68
|
+
"description": "Reader's pain named accurately, 'how did he know' moment present, no condescension, author in-process",
|
|
69
|
+
"weight": 1,
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
VERDICTS = {
|
|
74
|
+
(28, 35): ("PASS", "✓", "Proceed to Humanizer (Module 6D)"),
|
|
75
|
+
(21, 27): ("CONDITIONAL PASS", "~", "Proceed to Humanizer, address Required Fixes after"),
|
|
76
|
+
(0, 20): ("REVISE", "✗", "Return to Chapter Writer (Module 6B) with fix list"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_verdict(total: int) -> tuple:
|
|
81
|
+
for (lo, hi), (label, icon, action) in VERDICTS.items():
|
|
82
|
+
if lo <= total <= hi:
|
|
83
|
+
return label, icon, action
|
|
84
|
+
return "REVISE", "✗", "Return to Chapter Writer"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def score_to_label(score: int) -> str:
|
|
88
|
+
labels = {5: "Excellent", 4: "Good", 3: "Needs work", 2: "Significant issues", 1: "Failing"}
|
|
89
|
+
return labels.get(score, "Unknown")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_report(chapter_title: str, scores: dict, word_count: int = 0,
|
|
93
|
+
target_words: int = 0, notes: dict = None) -> dict:
|
|
94
|
+
notes = notes or {}
|
|
95
|
+
total = sum(scores.get(d["key"], 0) for d in DIMENSIONS)
|
|
96
|
+
max_score = len(DIMENSIONS) * 5
|
|
97
|
+
verdict, icon, action = get_verdict(total)
|
|
98
|
+
|
|
99
|
+
required_fixes = []
|
|
100
|
+
recommended = []
|
|
101
|
+
|
|
102
|
+
for dim in DIMENSIONS:
|
|
103
|
+
key = dim["key"]
|
|
104
|
+
score = scores.get(key, 0)
|
|
105
|
+
note = notes.get(key, "")
|
|
106
|
+
if score <= 3:
|
|
107
|
+
required_fixes.append({
|
|
108
|
+
"dimension": dim["label"],
|
|
109
|
+
"score": score,
|
|
110
|
+
"note": note or f"Score {score}/5 — {score_to_label(score)}",
|
|
111
|
+
})
|
|
112
|
+
elif score == 4 and note:
|
|
113
|
+
recommended.append({
|
|
114
|
+
"dimension": dim["label"],
|
|
115
|
+
"score": score,
|
|
116
|
+
"note": note,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"chapter": chapter_title,
|
|
121
|
+
"timestamp": datetime.now().isoformat(),
|
|
122
|
+
"word_count": word_count,
|
|
123
|
+
"target_words": target_words,
|
|
124
|
+
"scores": {d["key"]: scores.get(d["key"], 0) for d in DIMENSIONS},
|
|
125
|
+
"total": total,
|
|
126
|
+
"max_score": max_score,
|
|
127
|
+
"verdict": verdict,
|
|
128
|
+
"icon": icon,
|
|
129
|
+
"action": action,
|
|
130
|
+
"required_fixes": required_fixes,
|
|
131
|
+
"recommended": recommended,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def format_report(report: dict) -> str:
|
|
136
|
+
lines = []
|
|
137
|
+
lines.append(f"\n{'═' * 60}")
|
|
138
|
+
lines.append(f" CHAPTER AUDIT — {report['chapter']}")
|
|
139
|
+
if report.get("word_count"):
|
|
140
|
+
wc_line = f" Words: {report['word_count']:,}"
|
|
141
|
+
if report.get("target_words"):
|
|
142
|
+
wc_line += f" / Target: {report['target_words']:,}"
|
|
143
|
+
lines.append(wc_line)
|
|
144
|
+
lines.append(f"{'═' * 60}")
|
|
145
|
+
lines.append(f"\n SCORES")
|
|
146
|
+
lines.append(f" {'─' * 46}")
|
|
147
|
+
|
|
148
|
+
for dim in DIMENSIONS:
|
|
149
|
+
key = dim["key"]
|
|
150
|
+
score = report["scores"].get(key, 0)
|
|
151
|
+
bar = "█" * score + "░" * (5 - score)
|
|
152
|
+
lines.append(f" {dim['label']:<30} {bar} {score}/5")
|
|
153
|
+
|
|
154
|
+
lines.append(f" {'─' * 46}")
|
|
155
|
+
lines.append(f" {'OVERALL':<30} {report['total']}/{report['max_score']}")
|
|
156
|
+
lines.append(f"\n VERDICT: {report['icon']} {report['verdict']}")
|
|
157
|
+
lines.append(f" Action: {report['action']}")
|
|
158
|
+
|
|
159
|
+
if report["required_fixes"]:
|
|
160
|
+
lines.append(f"\n {'─' * 46}")
|
|
161
|
+
lines.append(f" REQUIRED FIXES ({len(report['required_fixes'])}):")
|
|
162
|
+
for i, fix in enumerate(report["required_fixes"], 1):
|
|
163
|
+
lines.append(f"\n {i}. {fix['dimension']} [{fix['score']}/5]")
|
|
164
|
+
if fix["note"]:
|
|
165
|
+
lines.append(f" {fix['note']}")
|
|
166
|
+
|
|
167
|
+
if report["recommended"]:
|
|
168
|
+
lines.append(f"\n RECOMMENDED IMPROVEMENTS:")
|
|
169
|
+
for rec in report["recommended"]:
|
|
170
|
+
lines.append(f" · {rec['dimension']}: {rec['note']}")
|
|
171
|
+
|
|
172
|
+
lines.append(f"\n{'═' * 60}\n")
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def interactive_score(chapter_title: str) -> dict:
|
|
177
|
+
"""Prompt for scores interactively."""
|
|
178
|
+
print(f"\n Scoring: {chapter_title}\n")
|
|
179
|
+
scores = {}
|
|
180
|
+
notes = {}
|
|
181
|
+
|
|
182
|
+
for dim in DIMENSIONS:
|
|
183
|
+
while True:
|
|
184
|
+
try:
|
|
185
|
+
val = input(f" {dim['label']} [1-5]: ").strip()
|
|
186
|
+
score = int(val)
|
|
187
|
+
if 1 <= score <= 5:
|
|
188
|
+
scores[dim["key"]] = score
|
|
189
|
+
if score <= 3:
|
|
190
|
+
note = input(f" Issue note (enter to skip): ").strip()
|
|
191
|
+
if note:
|
|
192
|
+
notes[dim["key"]] = note
|
|
193
|
+
break
|
|
194
|
+
else:
|
|
195
|
+
print(" Enter a number between 1 and 5")
|
|
196
|
+
except (ValueError, EOFError):
|
|
197
|
+
print(" Invalid input")
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
wc = input("\n Chapter word count (enter to skip): ").strip()
|
|
201
|
+
target = input(" Target word count (enter to skip): ").strip()
|
|
202
|
+
|
|
203
|
+
return build_report(
|
|
204
|
+
chapter_title,
|
|
205
|
+
scores,
|
|
206
|
+
int(wc) if wc.isdigit() else 0,
|
|
207
|
+
int(target) if target.isdigit() else 0,
|
|
208
|
+
notes,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
parser = argparse.ArgumentParser(description="Generate chapter audit report")
|
|
214
|
+
parser.add_argument("--chapter", help="Chapter title")
|
|
215
|
+
parser.add_argument("--input", help="Load scores from JSON file")
|
|
216
|
+
parser.add_argument("--output", help="Save report to file")
|
|
217
|
+
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
218
|
+
args = parser.parse_args()
|
|
219
|
+
|
|
220
|
+
if args.input:
|
|
221
|
+
report = json.loads(Path(args.input).read_text())
|
|
222
|
+
elif args.chapter:
|
|
223
|
+
report = interactive_score(args.chapter)
|
|
224
|
+
else:
|
|
225
|
+
print("Error: provide --chapter or --input", file=sys.stderr)
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
|
|
228
|
+
if args.json:
|
|
229
|
+
output = json.dumps(report, indent=2)
|
|
230
|
+
print(output)
|
|
231
|
+
else:
|
|
232
|
+
output = format_report(report)
|
|
233
|
+
print(output)
|
|
234
|
+
|
|
235
|
+
if args.output:
|
|
236
|
+
Path(args.output).write_text(output if not args.json else json.dumps(report, indent=2))
|
|
237
|
+
print(f" Saved to: {args.output}")
|