@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.
Files changed (43) hide show
  1. package/README.md +58 -67
  2. package/bin/cli.js +7 -3
  3. package/package.json +2 -5
  4. package/skills/book-creator/SKILL.md +848 -0
  5. package/skills/book-creator/references/kdp_specs.md +139 -0
  6. package/skills/book-creator/references/log_schema.md +149 -0
  7. package/skills/book-creator/references/patterns_quick_ref.md +71 -0
  8. package/skills/book-creator/references/thinkers_reference.md +104 -0
  9. package/skills/book-creator/scripts/__pycache__/bank_formatter.cpython-312.pyc +0 -0
  10. package/skills/book-creator/scripts/__pycache__/conflict_check.cpython-312.pyc +0 -0
  11. package/skills/book-creator/scripts/__pycache__/dna_scan.cpython-312.pyc +0 -0
  12. package/skills/book-creator/scripts/__pycache__/kdp_check.cpython-312.pyc +0 -0
  13. package/skills/book-creator/scripts/__pycache__/log_manager.cpython-312.pyc +0 -0
  14. package/skills/book-creator/scripts/__pycache__/scan_ai_patterns.cpython-312.pyc +0 -0
  15. package/skills/book-creator/scripts/__pycache__/score_report.cpython-312.pyc +0 -0
  16. package/skills/book-creator/scripts/__pycache__/toc_extract.cpython-312.pyc +0 -0
  17. package/skills/book-creator/scripts/__pycache__/validate_concept.cpython-312.pyc +0 -0
  18. package/skills/book-creator/scripts/__pycache__/word_count.cpython-312.pyc +0 -0
  19. package/skills/book-creator/scripts/bank_formatter.py +206 -0
  20. package/skills/book-creator/scripts/conflict_check.py +179 -0
  21. package/skills/book-creator/scripts/dna_scan.py +168 -0
  22. package/skills/book-creator/scripts/kdp_check.py +255 -0
  23. package/skills/book-creator/scripts/log_manager.py +258 -0
  24. package/skills/book-creator/scripts/scan_ai_patterns.py +279 -0
  25. package/skills/book-creator/scripts/score_report.py +237 -0
  26. package/skills/book-creator/scripts/toc_extract.py +151 -0
  27. package/skills/book-creator/scripts/validate_concept.py +255 -0
  28. package/skills/book-creator/scripts/word_count.py +196 -0
  29. package/skills/book-writer/scripts/__pycache__/kdp_check.cpython-312.pyc +0 -0
  30. package/skills/book-writer/scripts/__pycache__/toc_extract.cpython-312.pyc +0 -0
  31. package/skills/book-writer/scripts/__pycache__/word_count.cpython-312.pyc +0 -0
  32. package/skills/book-writer.zip +0 -0
  33. package/skills/chapter-auditor/scripts/__pycache__/score_report.cpython-312.pyc +0 -0
  34. package/skills/concept-expander/scripts/__pycache__/validate_concept.cpython-312.pyc +0 -0
  35. package/skills/continuity-tracker/scripts/__pycache__/conflict_check.cpython-312.pyc +0 -0
  36. package/skills/continuity-tracker/scripts/__pycache__/log_manager.cpython-312.pyc +0 -0
  37. package/skills/humanizer/scripts/__pycache__/dna_scan.cpython-312.pyc +0 -0
  38. package/skills/humanizer/scripts/__pycache__/scan_ai_patterns.cpython-312.pyc +0 -0
  39. package/skills/overhaul/scripts/__pycache__/changelog_gen.cpython-312.pyc +0 -0
  40. package/skills/overhaul/scripts/__pycache__/skill_parser.cpython-312.pyc +0 -0
  41. package/skills/overhaul/scripts/__pycache__/version_bump.cpython-312.pyc +0 -0
  42. package/skills/overhaul.zip +0 -0
  43. 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)