@demig0d2/skills 1.0.2 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -67
- package/bin/cli.js +7 -3
- package/package.json +2 -5
- package/skills/book-creator/SKILL.md +848 -0
- package/skills/book-creator/references/kdp_specs.md +139 -0
- package/skills/book-creator/references/log_schema.md +149 -0
- package/skills/book-creator/references/patterns_quick_ref.md +71 -0
- package/skills/book-creator/references/thinkers_reference.md +104 -0
- package/skills/book-creator/scripts/__pycache__/bank_formatter.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/conflict_check.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/dna_scan.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/kdp_check.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/log_manager.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/scan_ai_patterns.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/score_report.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/toc_extract.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/validate_concept.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/__pycache__/word_count.cpython-312.pyc +0 -0
- package/skills/book-creator/scripts/bank_formatter.py +206 -0
- package/skills/book-creator/scripts/conflict_check.py +179 -0
- package/skills/book-creator/scripts/dna_scan.py +168 -0
- package/skills/book-creator/scripts/kdp_check.py +255 -0
- package/skills/book-creator/scripts/log_manager.py +258 -0
- package/skills/book-creator/scripts/scan_ai_patterns.py +279 -0
- package/skills/book-creator/scripts/score_report.py +237 -0
- package/skills/book-creator/scripts/toc_extract.py +151 -0
- package/skills/book-creator/scripts/validate_concept.py +255 -0
- package/skills/book-creator/scripts/word_count.py +196 -0
- package/skills/book-writer/scripts/__pycache__/kdp_check.cpython-312.pyc +0 -0
- package/skills/book-writer/scripts/__pycache__/toc_extract.cpython-312.pyc +0 -0
- package/skills/book-writer/scripts/__pycache__/word_count.cpython-312.pyc +0 -0
- package/skills/book-writer.zip +0 -0
- package/skills/chapter-auditor/scripts/__pycache__/score_report.cpython-312.pyc +0 -0
- package/skills/concept-expander/scripts/__pycache__/validate_concept.cpython-312.pyc +0 -0
- package/skills/continuity-tracker/scripts/__pycache__/conflict_check.cpython-312.pyc +0 -0
- package/skills/continuity-tracker/scripts/__pycache__/log_manager.cpython-312.pyc +0 -0
- package/skills/humanizer/scripts/__pycache__/dna_scan.cpython-312.pyc +0 -0
- package/skills/humanizer/scripts/__pycache__/scan_ai_patterns.cpython-312.pyc +0 -0
- package/skills/overhaul/scripts/__pycache__/changelog_gen.cpython-312.pyc +0 -0
- package/skills/overhaul/scripts/__pycache__/skill_parser.cpython-312.pyc +0 -0
- package/skills/overhaul/scripts/__pycache__/version_bump.cpython-312.pyc +0 -0
- package/skills/overhaul.zip +0 -0
- package/skills/research-aggregator/scripts/__pycache__/bank_formatter.cpython-312.pyc +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
bank_formatter.py — Format and validate a research bank
|
|
4
|
+
|
|
5
|
+
Reads a research bank JSON or markdown file and outputs a
|
|
6
|
+
structured, validated bank ready for the book-writer to use.
|
|
7
|
+
Also validates quote lengths and source attribution.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python bank_formatter.py <bank.json>
|
|
11
|
+
python bank_formatter.py <bank.json> --chapter 3
|
|
12
|
+
python bank_formatter.py <bank.json> --validate
|
|
13
|
+
python bank_formatter.py <bank.json> --markdown
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import re
|
|
18
|
+
import json
|
|
19
|
+
import argparse
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
MAX_QUOTE_WORDS = 30
|
|
23
|
+
|
|
24
|
+
# ─── Banned / Overexposed Terms ───────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
BANNED_SOURCES = [
|
|
27
|
+
"tony robbins", "brené brown", "marie kondo", "simon sinek",
|
|
28
|
+
"gary vee", "gary vaynerchuk", "oprah",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
OVEREXPOSED_QUOTES = [
|
|
32
|
+
"be the change you wish to see",
|
|
33
|
+
"the unexamined life is not worth living",
|
|
34
|
+
"insanity is doing the same thing",
|
|
35
|
+
"you miss 100% of the shots",
|
|
36
|
+
"it always seems impossible until it's done",
|
|
37
|
+
"in the middle of difficulty lies opportunity",
|
|
38
|
+
"the only way to do great work",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_quote(quote: str) -> list:
|
|
43
|
+
"""Return list of issues with a quote."""
|
|
44
|
+
issues = []
|
|
45
|
+
words = quote.split()
|
|
46
|
+
if len(words) > MAX_QUOTE_WORDS:
|
|
47
|
+
issues.append(f"Quote too long ({len(words)} words, max {MAX_QUOTE_WORDS})")
|
|
48
|
+
|
|
49
|
+
quote_lower = quote.lower()
|
|
50
|
+
for overexposed in OVEREXPOSED_QUOTES:
|
|
51
|
+
if overexposed in quote_lower:
|
|
52
|
+
issues.append(f"Overexposed quote detected — find something more original")
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
return issues
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_source(source: str) -> list:
|
|
59
|
+
"""Return list of issues with a source attribution."""
|
|
60
|
+
issues = []
|
|
61
|
+
source_lower = source.lower()
|
|
62
|
+
|
|
63
|
+
for banned in BANNED_SOURCES:
|
|
64
|
+
if banned in source_lower:
|
|
65
|
+
issues.append(f"Pop-psychology source not aligned with Vivid's voice: {source}")
|
|
66
|
+
|
|
67
|
+
if source.lower() in ("unknown", "anonymous", "") :
|
|
68
|
+
issues.append("Source is unknown — verify or remove")
|
|
69
|
+
|
|
70
|
+
return issues
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_bank(bank: dict) -> dict:
|
|
74
|
+
"""Run full validation on a research bank."""
|
|
75
|
+
all_issues = []
|
|
76
|
+
|
|
77
|
+
for chapter_key, chapter_data in bank.get("chapters", {}).items():
|
|
78
|
+
chapter_issues = []
|
|
79
|
+
|
|
80
|
+
for quote_item in chapter_data.get("quotes", []):
|
|
81
|
+
quote = quote_item.get("quote", "")
|
|
82
|
+
source = quote_item.get("source", "")
|
|
83
|
+
q_issues = validate_quote(quote)
|
|
84
|
+
s_issues = validate_source(source)
|
|
85
|
+
if q_issues or s_issues:
|
|
86
|
+
chapter_issues.append({
|
|
87
|
+
"chapter": chapter_key,
|
|
88
|
+
"type": "quote",
|
|
89
|
+
"content": quote[:60],
|
|
90
|
+
"issues": q_issues + s_issues,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
for anchor in chapter_data.get("philosophical_anchors", []):
|
|
94
|
+
if not anchor.get("connection"):
|
|
95
|
+
chapter_issues.append({
|
|
96
|
+
"chapter": chapter_key,
|
|
97
|
+
"type": "anchor",
|
|
98
|
+
"content": str(anchor)[:60],
|
|
99
|
+
"issues": ["Missing 'connection' field — how does this relate to the chapter?"],
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
all_issues.extend(chapter_issues)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"total_issues": len(all_issues),
|
|
106
|
+
"clean": len(all_issues) == 0,
|
|
107
|
+
"issues": all_issues,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_chapter_bank(chapter_key: str, chapter_data: dict) -> str:
|
|
112
|
+
lines = []
|
|
113
|
+
lines.append(f"\n{'─' * 56}")
|
|
114
|
+
lines.append(f"CHAPTER {chapter_key}: {chapter_data.get('title', '')}")
|
|
115
|
+
lines.append(f"Theme: {chapter_data.get('theme', 'Not specified')}")
|
|
116
|
+
|
|
117
|
+
anchors = chapter_data.get("philosophical_anchors", [])
|
|
118
|
+
if anchors:
|
|
119
|
+
lines.append(f"\nPhilosophical anchors:")
|
|
120
|
+
for a in anchors:
|
|
121
|
+
thinker = a.get("thinker", "Unknown")
|
|
122
|
+
idea = a.get("idea", "")
|
|
123
|
+
connection = a.get("connection", "")
|
|
124
|
+
lines.append(f" • {thinker} — {idea}")
|
|
125
|
+
if connection:
|
|
126
|
+
lines.append(f" → {connection}")
|
|
127
|
+
|
|
128
|
+
quotes = chapter_data.get("quotes", [])
|
|
129
|
+
if quotes:
|
|
130
|
+
lines.append(f"\nQuotes ({len(quotes)}):")
|
|
131
|
+
for q in quotes:
|
|
132
|
+
lines.append(f" \"{q.get('quote', '')}\"")
|
|
133
|
+
lines.append(f" — {q.get('source', 'Unknown')}")
|
|
134
|
+
|
|
135
|
+
research = chapter_data.get("research", [])
|
|
136
|
+
if research:
|
|
137
|
+
lines.append(f"\nResearch / data:")
|
|
138
|
+
for r in research:
|
|
139
|
+
lines.append(f" • {r.get('finding', '')} [{r.get('source', '')}]")
|
|
140
|
+
|
|
141
|
+
opposition = chapter_data.get("opposition", "")
|
|
142
|
+
if opposition:
|
|
143
|
+
lines.append(f"\nConceptual opposition:")
|
|
144
|
+
lines.append(f" {opposition}")
|
|
145
|
+
|
|
146
|
+
notes = chapter_data.get("writing_notes", "")
|
|
147
|
+
if notes:
|
|
148
|
+
lines.append(f"\nWriting notes:")
|
|
149
|
+
lines.append(f" {notes}")
|
|
150
|
+
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def print_bank(bank: dict, chapter_filter: int = None):
|
|
155
|
+
print(f"\n{'═' * 60}")
|
|
156
|
+
print(f" RESEARCH BANK — {bank.get('book_title', 'Unknown Book')}")
|
|
157
|
+
print(f"{'═' * 60}")
|
|
158
|
+
|
|
159
|
+
global_anchors = bank.get("global_anchors", [])
|
|
160
|
+
if global_anchors:
|
|
161
|
+
print(f"\nGLOBAL ANCHORS (whole manuscript):")
|
|
162
|
+
for a in global_anchors:
|
|
163
|
+
print(f" • {a}")
|
|
164
|
+
|
|
165
|
+
chapters = bank.get("chapters", {})
|
|
166
|
+
for key, data in sorted(chapters.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 0):
|
|
167
|
+
if chapter_filter and int(key) != chapter_filter:
|
|
168
|
+
continue
|
|
169
|
+
print(format_chapter_bank(key, data))
|
|
170
|
+
|
|
171
|
+
print(f"\n{'═' * 60}")
|
|
172
|
+
print(f" Chapters in bank: {len(chapters)}")
|
|
173
|
+
print()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
parser = argparse.ArgumentParser()
|
|
178
|
+
parser.add_argument("bank_file")
|
|
179
|
+
parser.add_argument("--chapter", type=int, help="Show specific chapter only")
|
|
180
|
+
parser.add_argument("--validate", action="store_true")
|
|
181
|
+
parser.add_argument("--markdown", action="store_true")
|
|
182
|
+
parser.add_argument("--json", action="store_true")
|
|
183
|
+
args = parser.parse_args()
|
|
184
|
+
|
|
185
|
+
path = Path(args.bank_file)
|
|
186
|
+
if not path.exists():
|
|
187
|
+
print(f"Error: File not found: {args.bank_file}", file=sys.stderr)
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
bank = json.loads(path.read_text(encoding="utf-8"))
|
|
191
|
+
|
|
192
|
+
if args.validate:
|
|
193
|
+
result = validate_bank(bank)
|
|
194
|
+
if result["clean"]:
|
|
195
|
+
print(f"\n ✓ Research bank is clean — {len(bank.get('chapters', {}))} chapters validated\n")
|
|
196
|
+
else:
|
|
197
|
+
print(f"\n ✗ {result['total_issues']} issue(s) found:\n")
|
|
198
|
+
for issue in result["issues"]:
|
|
199
|
+
print(f" Ch.{issue['chapter']} [{issue['type']}]: {issue['content']}")
|
|
200
|
+
for i in issue["issues"]:
|
|
201
|
+
print(f" → {i}")
|
|
202
|
+
print()
|
|
203
|
+
elif args.json:
|
|
204
|
+
print(json.dumps(bank, indent=2))
|
|
205
|
+
else:
|
|
206
|
+
print_bank(bank, args.chapter)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
conflict_check.py — Check a chapter for continuity conflicts against the log
|
|
4
|
+
|
|
5
|
+
Scans a chapter text file against the established facts, retired metaphors,
|
|
6
|
+
and delivered insights in the continuity log. Surfaces potential conflicts.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python conflict_check.py <chapter_file> [--log log.json]
|
|
10
|
+
python conflict_check.py <chapter_file> [--log log.json] --json
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import re
|
|
15
|
+
import json
|
|
16
|
+
import argparse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOG = "continuity_log.json"
|
|
20
|
+
|
|
21
|
+
# ─── Conflict Detection Rules ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
# Known fact patterns that might be stated inconsistently
|
|
24
|
+
NUMERICAL_PATTERN = re.compile(r'\b(\d+)\s+(years?|months?|days?|chapters?|books?|pages?)\b', re.IGNORECASE)
|
|
25
|
+
NAME_PATTERN = re.compile(r'\b[A-Z][a-z]+ [A-Z][a-z]+\b')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_log(log_path: str) -> dict:
|
|
29
|
+
path = Path(log_path)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
return None
|
|
32
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_retired_metaphors(chapter_text: str, retired: list) -> list:
|
|
36
|
+
"""Find retired metaphors used again in this chapter."""
|
|
37
|
+
conflicts = []
|
|
38
|
+
chapter_lower = chapter_text.lower()
|
|
39
|
+
for metaphor in retired:
|
|
40
|
+
# Check key phrases from the metaphor
|
|
41
|
+
key_words = [w for w in metaphor.lower().split() if len(w) > 4]
|
|
42
|
+
matches = sum(1 for w in key_words if w in chapter_lower)
|
|
43
|
+
if matches >= 2:
|
|
44
|
+
conflicts.append({
|
|
45
|
+
"type": "repeated_metaphor",
|
|
46
|
+
"severity": "medium",
|
|
47
|
+
"detail": f'Retired metaphor may be reused: "{metaphor}"',
|
|
48
|
+
"suggestion": "Use a fresh image — this metaphor was already delivered in a prior chapter",
|
|
49
|
+
})
|
|
50
|
+
return conflicts
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_repeated_insights(chapter_text: str, insights: dict) -> list:
|
|
54
|
+
"""Check if an insight already fully delivered is being re-delivered."""
|
|
55
|
+
conflicts = []
|
|
56
|
+
chapter_lower = chapter_text.lower()
|
|
57
|
+
|
|
58
|
+
for ch_num, insight in insights.items():
|
|
59
|
+
# Look for key phrase overlap
|
|
60
|
+
insight_words = [w for w in insight.lower().split() if len(w) > 5]
|
|
61
|
+
if not insight_words:
|
|
62
|
+
continue
|
|
63
|
+
matches = sum(1 for w in insight_words if w in chapter_lower)
|
|
64
|
+
overlap_ratio = matches / len(insight_words) if insight_words else 0
|
|
65
|
+
|
|
66
|
+
if overlap_ratio > 0.5:
|
|
67
|
+
conflicts.append({
|
|
68
|
+
"type": "repeated_insight",
|
|
69
|
+
"severity": "medium",
|
|
70
|
+
"detail": f'Insight from Ch.{ch_num} may be re-delivered: "{insight[:60]}..."',
|
|
71
|
+
"suggestion": "Build on this insight rather than restating it — add a new dimension",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return conflicts
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_fact_consistency(chapter_text: str, established_facts: list) -> list:
|
|
78
|
+
"""Flag when numerical or named facts appear inconsistently."""
|
|
79
|
+
conflicts = []
|
|
80
|
+
|
|
81
|
+
# Extract numbers from chapter
|
|
82
|
+
chapter_numbers = NUMERICAL_PATTERN.findall(chapter_text)
|
|
83
|
+
|
|
84
|
+
for fact in established_facts:
|
|
85
|
+
# Check if fact contains numbers that contradict chapter
|
|
86
|
+
fact_numbers = NUMERICAL_PATTERN.findall(fact)
|
|
87
|
+
for fn_val, fn_unit in fact_numbers:
|
|
88
|
+
for cn_val, cn_unit in chapter_numbers:
|
|
89
|
+
if cn_unit.rstrip("s") == fn_unit.rstrip("s") and cn_val != fn_val:
|
|
90
|
+
conflicts.append({
|
|
91
|
+
"type": "numerical_inconsistency",
|
|
92
|
+
"severity": "high",
|
|
93
|
+
"detail": f'Number mismatch: fact says "{fn_val} {fn_unit}", chapter says "{cn_val} {cn_unit}"',
|
|
94
|
+
"suggestion": f'Verify against established fact: "{fact[:80]}"',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return conflicts
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_conflict_check(chapter_path: str, log_path: str) -> dict:
|
|
101
|
+
chapter_text = Path(chapter_path).read_text(encoding="utf-8")
|
|
102
|
+
log = load_log(log_path)
|
|
103
|
+
|
|
104
|
+
result = {
|
|
105
|
+
"chapter_file": chapter_path,
|
|
106
|
+
"log_file": log_path,
|
|
107
|
+
"conflicts": [],
|
|
108
|
+
"warnings": [],
|
|
109
|
+
"clean": True,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if not log:
|
|
113
|
+
result["warnings"].append("No continuity log found — run log_manager.py init first")
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
# Run checks
|
|
117
|
+
retired = log.get("metaphors", {}).get("retired", [])
|
|
118
|
+
insights = log.get("insights", {})
|
|
119
|
+
facts = log.get("established_facts", [])
|
|
120
|
+
|
|
121
|
+
metaphor_conflicts = check_retired_metaphors(chapter_text, retired)
|
|
122
|
+
insight_conflicts = check_repeated_insights(chapter_text, insights)
|
|
123
|
+
fact_conflicts = check_fact_consistency(chapter_text, facts)
|
|
124
|
+
|
|
125
|
+
all_conflicts = metaphor_conflicts + insight_conflicts + fact_conflicts
|
|
126
|
+
|
|
127
|
+
result["conflicts"] = all_conflicts
|
|
128
|
+
result["clean"] = len(all_conflicts) == 0
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def print_report(result: dict):
|
|
134
|
+
print(f"\n{'═' * 60}")
|
|
135
|
+
print(f" CONTINUITY CONFLICT CHECK")
|
|
136
|
+
print(f" Chapter: {result['chapter_file']}")
|
|
137
|
+
print(f" Log: {result['log_file']}")
|
|
138
|
+
print(f"{'═' * 60}")
|
|
139
|
+
|
|
140
|
+
if result.get("warnings"):
|
|
141
|
+
for w in result["warnings"]:
|
|
142
|
+
print(f"\n ⚠ {w}")
|
|
143
|
+
|
|
144
|
+
if result["clean"]:
|
|
145
|
+
print(f"\n ✓ No conflicts detected. Chapter is consistent with log.\n")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
high = [c for c in result["conflicts"] if c["severity"] == "high"]
|
|
149
|
+
medium = [c for c in result["conflicts"] if c["severity"] == "medium"]
|
|
150
|
+
|
|
151
|
+
print(f"\n Conflicts: {len(result['conflicts'])} ({len(high)} high, {len(medium)} medium)\n")
|
|
152
|
+
|
|
153
|
+
for i, conflict in enumerate(result["conflicts"], 1):
|
|
154
|
+
icon = "✗" if conflict["severity"] == "high" else "⚠"
|
|
155
|
+
print(f" {icon} [{conflict['severity'].upper()}] {conflict['type'].replace('_', ' ').title()}")
|
|
156
|
+
print(f" {conflict['detail']}")
|
|
157
|
+
print(f" → {conflict['suggestion']}\n")
|
|
158
|
+
|
|
159
|
+
print(f"{'═' * 60}")
|
|
160
|
+
print(f" Resolve conflicts before updating the continuity log.\n")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
parser = argparse.ArgumentParser()
|
|
165
|
+
parser.add_argument("chapter_file")
|
|
166
|
+
parser.add_argument("--log", default=DEFAULT_LOG)
|
|
167
|
+
parser.add_argument("--json", action="store_true")
|
|
168
|
+
args = parser.parse_args()
|
|
169
|
+
|
|
170
|
+
if not Path(args.chapter_file).exists():
|
|
171
|
+
print(f"Error: File not found: {args.chapter_file}", file=sys.stderr)
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
result = run_conflict_check(args.chapter_file, args.log)
|
|
175
|
+
|
|
176
|
+
if args.json:
|
|
177
|
+
print(json.dumps(result, indent=2))
|
|
178
|
+
else:
|
|
179
|
+
print_report(result)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dna_scan.py — Scan text for Vivid's protected DNA constructions
|
|
4
|
+
|
|
5
|
+
Identifies Vivid's authentic signature constructions that must NOT be
|
|
6
|
+
stripped by the humanizer. Marks them so the pattern elimination pass
|
|
7
|
+
knows to skip them.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python dna_scan.py <input_file>
|
|
11
|
+
python dna_scan.py <input_file> --json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# ─── DNA Constructions to Protect ────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
DNA_RULES = [
|
|
22
|
+
{
|
|
23
|
+
"name": "Signature Phrases",
|
|
24
|
+
"description": "Vivid's recurring voice markers — never strip",
|
|
25
|
+
"patterns": [
|
|
26
|
+
r"\bbut here'?s\b",
|
|
27
|
+
r"\bthe hardest part wasn'?t\b",
|
|
28
|
+
r"\bthat is what .{3,40} did to me from the inside\b",
|
|
29
|
+
r"\band you start to see\b",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "Protected Negative Parallelisms",
|
|
34
|
+
"description": "Grounded in specific experience — not generic contrast",
|
|
35
|
+
"patterns": [
|
|
36
|
+
r"\bnot the .{5,60}, but what\b",
|
|
37
|
+
r"\bnot the .{5,60}, but the\b",
|
|
38
|
+
],
|
|
39
|
+
"note": "Only protect if both sides reference concrete, specific experience",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "Protected Em Dashes (Emotional Weight)",
|
|
43
|
+
"description": "Em dashes around emotionally significant phrases",
|
|
44
|
+
"patterns": [
|
|
45
|
+
r"— that .{3,50} —",
|
|
46
|
+
r"— [a-z].{3,50} —",
|
|
47
|
+
],
|
|
48
|
+
"note": "Only protect when em dash is around a named emotional moment",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Physical Imagery",
|
|
52
|
+
"description": "Vivid's grounded physical metaphors — never flatten",
|
|
53
|
+
"patterns": [
|
|
54
|
+
r"\bpressed down on my chest\b",
|
|
55
|
+
r"\bknife that cut\b",
|
|
56
|
+
r"\bscraps of attention\b",
|
|
57
|
+
r"\binvisible chains\b",
|
|
58
|
+
r"\bdeafening scream inside\b",
|
|
59
|
+
r"\bsuffocating grip\b",
|
|
60
|
+
r"\bunraveling\b",
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "Protected Elevated Vocabulary",
|
|
65
|
+
"description": "Elevated words used to name felt experience — not decoration",
|
|
66
|
+
"patterns": [
|
|
67
|
+
r"\binsidious\b", r"\bimperceptibly\b", r"\bagonizing\b",
|
|
68
|
+
r"\bdesolate\b", r"\bconspicuously\b", r"\bgrotesque\b",
|
|
69
|
+
r"\bincessant\b", r"\bsuffocating\b",
|
|
70
|
+
],
|
|
71
|
+
"note": "Protect only when modifying a specific felt experience",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "Reframe Closings",
|
|
75
|
+
"description": "Single-sentence reframe closings — core Vivid structure",
|
|
76
|
+
"patterns": [
|
|
77
|
+
r"^[A-Z].{20,120}\.$", # standalone short paragraph (reframe closing)
|
|
78
|
+
],
|
|
79
|
+
"note": "Flag for review — Claude determines if it's a reframe closing",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# ─── Scanner ──────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def scan_dna(filepath: str) -> dict:
|
|
86
|
+
path = Path(filepath)
|
|
87
|
+
if not path.exists():
|
|
88
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
text = path.read_text(encoding="utf-8")
|
|
92
|
+
lines = text.splitlines()
|
|
93
|
+
|
|
94
|
+
results = {
|
|
95
|
+
"file": str(path),
|
|
96
|
+
"total_lines": len(lines),
|
|
97
|
+
"protected_constructions": [],
|
|
98
|
+
"total_protected": 0,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for rule in DNA_RULES:
|
|
102
|
+
rule_findings = []
|
|
103
|
+
for line_num, line in enumerate(lines, start=1):
|
|
104
|
+
for pattern in rule["patterns"]:
|
|
105
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
106
|
+
rule_findings.append({
|
|
107
|
+
"line": line_num,
|
|
108
|
+
"text": line.strip()[:120],
|
|
109
|
+
"note": rule.get("note", ""),
|
|
110
|
+
})
|
|
111
|
+
break # one match per line per rule is enough
|
|
112
|
+
|
|
113
|
+
if rule_findings:
|
|
114
|
+
results["protected_constructions"].append({
|
|
115
|
+
"rule": rule["name"],
|
|
116
|
+
"description": rule["description"],
|
|
117
|
+
"count": len(rule_findings),
|
|
118
|
+
"instances": rule_findings,
|
|
119
|
+
})
|
|
120
|
+
results["total_protected"] += len(rule_findings)
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_dna_report(results: dict):
|
|
126
|
+
print(f"\n{'═' * 60}")
|
|
127
|
+
print(f" DNA PROTECTION SCAN")
|
|
128
|
+
print(f" File: {results['file']}")
|
|
129
|
+
print(f"{'═' * 60}")
|
|
130
|
+
|
|
131
|
+
if results["total_protected"] == 0:
|
|
132
|
+
print("\n No protected DNA constructions found.\n")
|
|
133
|
+
print(" This is normal for non-Vivid text or early drafts.\n")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
print(f"\n Protected constructions found: {results['total_protected']}")
|
|
137
|
+
print(f" These will be EXEMPT from AI pattern elimination.\n")
|
|
138
|
+
|
|
139
|
+
for item in results["protected_constructions"]:
|
|
140
|
+
print(f" {'─' * 56}")
|
|
141
|
+
print(f" ✓ PROTECT: {item['rule']} ({item['count']} instance{'s' if item['count'] > 1 else ''})")
|
|
142
|
+
print(f" {item['description']}")
|
|
143
|
+
for inst in item["instances"][:3]:
|
|
144
|
+
print(f" Line {inst['line']:>4}: {inst['text'][:90]}")
|
|
145
|
+
if inst.get("note"):
|
|
146
|
+
print(f" Note: {inst['note']}")
|
|
147
|
+
|
|
148
|
+
print(f"\n{'═' * 60}")
|
|
149
|
+
print(f" Mark these {results['total_protected']} line(s) as protected")
|
|
150
|
+
print(f" before running scan_ai_patterns.py.\n")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
if len(sys.argv) < 2:
|
|
157
|
+
print("Usage: python dna_scan.py <input_file> [--json]")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
filepath = sys.argv[1]
|
|
161
|
+
mode = sys.argv[2] if len(sys.argv) > 2 else "--report"
|
|
162
|
+
|
|
163
|
+
results = scan_dna(filepath)
|
|
164
|
+
|
|
165
|
+
if mode == "--json":
|
|
166
|
+
print(json.dumps(results, indent=2))
|
|
167
|
+
else:
|
|
168
|
+
print_dna_report(results)
|