@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.
Files changed (29) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/bin/cli.js +313 -0
  4. package/package.json +44 -0
  5. package/skills/book-writer/SKILL.md +1396 -0
  6. package/skills/book-writer/references/kdp_specs.md +139 -0
  7. package/skills/book-writer/scripts/kdp_check.py +255 -0
  8. package/skills/book-writer/scripts/toc_extract.py +151 -0
  9. package/skills/book-writer/scripts/word_count.py +196 -0
  10. package/skills/chapter-auditor/SKILL.md +231 -0
  11. package/skills/chapter-auditor/scripts/score_report.py +237 -0
  12. package/skills/concept-expander/SKILL.md +170 -0
  13. package/skills/concept-expander/scripts/validate_concept.py +255 -0
  14. package/skills/continuity-tracker/SKILL.md +251 -0
  15. package/skills/continuity-tracker/references/log_schema.md +149 -0
  16. package/skills/continuity-tracker/scripts/conflict_check.py +179 -0
  17. package/skills/continuity-tracker/scripts/log_manager.py +258 -0
  18. package/skills/humanizer/SKILL.md +632 -0
  19. package/skills/humanizer/references/patterns_quick_ref.md +71 -0
  20. package/skills/humanizer/scripts/dna_scan.py +168 -0
  21. package/skills/humanizer/scripts/scan_ai_patterns.py +279 -0
  22. package/skills/overhaul/SKILL.md +697 -0
  23. package/skills/overhaul/references/upgrade_checklist.md +81 -0
  24. package/skills/overhaul/scripts/changelog_gen.py +183 -0
  25. package/skills/overhaul/scripts/skill_parser.py +265 -0
  26. package/skills/overhaul/scripts/version_bump.py +128 -0
  27. package/skills/research-aggregator/SKILL.md +194 -0
  28. package/skills/research-aggregator/references/thinkers_reference.md +104 -0
  29. 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}")