@clawos-dev/clawd 0.2.50-beta.77.3a9364e → 0.2.51-beta.78.2024c11

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 (41) hide show
  1. package/dist/persona-defaults/persona-clawd-helper/CLAUDE.md +1 -1
  2. package/dist/persona-defaults/persona-knowledge-base/CLAUDE.md +19 -0
  3. package/dist/persona-defaults/persona-researcher/CLAUDE.md +20 -1
  4. package/package.json +1 -1
  5. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/SKILL.md +0 -187
  6. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/archive-template.md +0 -21
  7. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/article-template.md +0 -20
  8. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/index-template.md +0 -18
  9. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/raw-template.md +0 -7
  10. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/README.md +0 -119
  11. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/SKILL.md +0 -108
  12. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/continuation.md +0 -167
  13. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/html-generation.md +0 -103
  14. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/methodology.md +0 -421
  15. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/quality-gates.md +0 -192
  16. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/report-assembly.md +0 -130
  17. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/weasyprint_guidelines.md +0 -324
  18. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/requirements.txt +0 -14
  19. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/claim.schema.json +0 -49
  20. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/evidence.schema.json +0 -43
  21. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/run_manifest.schema.json +0 -97
  22. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/source.schema.json +0 -49
  23. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/citation_manager.py +0 -300
  24. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/evidence_store.py +0 -205
  25. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/extract_claims.py +0 -358
  26. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/md_to_html.py +0 -330
  27. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/research_engine.py +0 -584
  28. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/source_evaluator.py +0 -292
  29. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/validate_report.py +0 -354
  30. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_citations.py +0 -426
  31. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_claim_support.py +0 -344
  32. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_html.py +0 -220
  33. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/mckinsey_report_template.html +0 -443
  34. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/report_template.md +0 -414
  35. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/invalid_report.md +0 -27
  36. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/valid_report.md +0 -114
  37. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_citation_manager.py +0 -195
  38. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_evidence_store.py +0 -166
  39. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_extract_claims.py +0 -213
  40. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_verify_claim_support.py +0 -230
  41. package/dist/persona-defaults/persona-researcher/skills-lock.json +0 -11
@@ -1,344 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Claim-Support Verification — checks whether evidence supports claims.
4
-
5
- CLI subcommands:
6
- verify Check all claims against evidence, update support_status
7
- report Generate a support verification summary
8
-
9
- Version 1 is deterministic and cheap: entity, number, date, and
10
- lexical-overlap checks over stored evidence. No LLM calls.
11
-
12
- Only factual claims hard-fail on unsupported status.
13
- Synthesis/recommendation need traceability but softer thresholds.
14
- """
15
-
16
- import argparse
17
- import json
18
- import os
19
- import re
20
- import sys
21
- from collections import Counter
22
- from datetime import datetime, timezone
23
-
24
-
25
- # ---------------------------------------------------------------------------
26
- # JSONL helpers
27
- # ---------------------------------------------------------------------------
28
-
29
- def read_jsonl(path: str) -> list[dict]:
30
- rows = []
31
- if not os.path.exists(path):
32
- return rows
33
- with open(path) as f:
34
- for line in f:
35
- line = line.strip()
36
- if line:
37
- rows.append(json.loads(line))
38
- return rows
39
-
40
-
41
- def write_jsonl(path: str, rows: list[dict]) -> None:
42
- with open(path, 'w') as f:
43
- for row in rows:
44
- f.write(json.dumps(row, ensure_ascii=False) + '\n')
45
-
46
-
47
- # ---------------------------------------------------------------------------
48
- # Support verification logic
49
- # ---------------------------------------------------------------------------
50
-
51
- # Extract numbers (integers and decimals)
52
- NUMBER_RE = re.compile(r'\b\d+(?:\.\d+)?(?:%|x|X)?\b')
53
-
54
- # Extract year-like numbers
55
- YEAR_RE = re.compile(r'\b(19|20)\d{2}\b')
56
-
57
- # Extract capitalized entities (naive NER)
58
- ENTITY_RE = re.compile(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b')
59
-
60
- # Common stop entities to ignore
61
- STOP_ENTITIES = frozenset([
62
- 'The', 'This', 'That', 'These', 'However', 'Furthermore',
63
- 'Moreover', 'Additionally', 'Therefore', 'Nevertheless',
64
- ])
65
-
66
-
67
- def extract_tokens(text: str) -> set[str]:
68
- """Extract significant lowercase tokens (>3 chars)."""
69
- words = re.findall(r'\b[a-z]{4,}\b', text.lower())
70
- return set(words)
71
-
72
-
73
- def extract_numbers(text: str) -> set[str]:
74
- """Extract numeric values."""
75
- return set(NUMBER_RE.findall(text))
76
-
77
-
78
- def extract_years(text: str) -> set[str]:
79
- """Extract year mentions."""
80
- return set(YEAR_RE.findall(text))
81
-
82
-
83
- def extract_entities(text: str) -> set[str]:
84
- """Extract capitalized entity mentions."""
85
- ents = set(ENTITY_RE.findall(text))
86
- return ents - STOP_ENTITIES
87
-
88
-
89
- def compute_support_score(claim_text: str, evidence_quotes: list[str]) -> tuple[str, float, str]:
90
- """
91
- Compute support status for a claim given its linked evidence quotes.
92
-
93
- Returns (status, score, notes).
94
- Score range: 0.0 (no overlap) to 1.0 (strong support).
95
- """
96
- if not evidence_quotes:
97
- return ('unsupported', 0.0, 'no evidence linked')
98
-
99
- claim_tokens = extract_tokens(claim_text)
100
- claim_numbers = extract_numbers(claim_text)
101
- claim_years = extract_years(claim_text)
102
- claim_entities = extract_entities(claim_text)
103
-
104
- best_score = 0.0
105
- best_notes = []
106
-
107
- for quote in evidence_quotes:
108
- ev_tokens = extract_tokens(quote)
109
- ev_numbers = extract_numbers(quote)
110
- ev_years = extract_years(quote)
111
- ev_entities = extract_entities(quote)
112
-
113
- # Token overlap (Jaccard-like)
114
- if claim_tokens:
115
- token_overlap = len(claim_tokens & ev_tokens) / len(claim_tokens)
116
- else:
117
- token_overlap = 0.0
118
-
119
- # Number match
120
- if claim_numbers:
121
- number_match = len(claim_numbers & ev_numbers) / len(claim_numbers)
122
- else:
123
- number_match = 1.0 # No numbers to check
124
-
125
- # Year match
126
- if claim_years:
127
- year_match = len(claim_years & ev_years) / len(claim_years)
128
- else:
129
- year_match = 1.0
130
-
131
- # Entity match
132
- if claim_entities:
133
- entity_match = len(claim_entities & ev_entities) / len(claim_entities)
134
- else:
135
- entity_match = 1.0
136
-
137
- # Weighted composite
138
- score = (
139
- 0.4 * token_overlap +
140
- 0.25 * number_match +
141
- 0.15 * year_match +
142
- 0.2 * entity_match
143
- )
144
-
145
- if score > best_score:
146
- best_score = score
147
- best_notes = []
148
- if token_overlap < 0.3:
149
- best_notes.append('low lexical overlap')
150
- if claim_numbers and number_match < 0.5:
151
- best_notes.append('number mismatch')
152
- if claim_years and year_match < 1.0:
153
- best_notes.append('year mismatch')
154
- if claim_entities and entity_match < 0.3:
155
- best_notes.append('entity mismatch')
156
-
157
- # Threshold decision
158
- if best_score >= 0.6:
159
- status = 'supported'
160
- elif best_score >= 0.35:
161
- status = 'partial'
162
- else:
163
- status = 'needs_review'
164
-
165
- notes = '; '.join(best_notes) if best_notes else 'adequate overlap'
166
- return (status, round(best_score, 3), notes)
167
-
168
-
169
- # ---------------------------------------------------------------------------
170
- # Subcommands
171
- # ---------------------------------------------------------------------------
172
-
173
- def cmd_verify(args: argparse.Namespace) -> None:
174
- """Verify all claims against evidence, update claims.jsonl."""
175
- claims_path = os.path.join(args.dir, 'claims.jsonl')
176
- evidence_path = os.path.join(args.dir, 'evidence.jsonl')
177
- sources_path = os.path.join(args.dir, 'sources.jsonl')
178
-
179
- claims = read_jsonl(claims_path)
180
- evidence = read_jsonl(evidence_path)
181
- sources = read_jsonl(sources_path)
182
-
183
- # Build evidence index by source_id
184
- ev_by_source: dict[str, list[str]] = {}
185
- ev_by_id: dict[str, dict] = {}
186
- for ev in evidence:
187
- sid = ev.get('source_id', '')
188
- eid = ev.get('evidence_id', '')
189
- ev_by_source.setdefault(sid, []).append(ev.get('quote', ''))
190
- ev_by_id[eid] = ev
191
-
192
- # Deduplicate claims
193
- seen = set()
194
- unique_claims = []
195
- for c in claims:
196
- cid = c.get('claim_id')
197
- if cid not in seen:
198
- seen.add(cid)
199
- unique_claims.append(c)
200
-
201
- verified = 0
202
- updated_claims = []
203
-
204
- for claim in unique_claims:
205
- claim_type = claim.get('claim_type', 'factual')
206
-
207
- # Gather evidence for this claim
208
- cited_ids = claim.get('cited_source_ids', [])
209
- evidence_ids = claim.get('evidence_ids', [])
210
-
211
- # Collect evidence quotes from linked evidence_ids
212
- quotes = []
213
- for eid in evidence_ids:
214
- if eid in ev_by_id:
215
- quotes.append(ev_by_id[eid].get('quote', ''))
216
-
217
- # Also gather from cited sources
218
- for sid in cited_ids:
219
- if sid in ev_by_source:
220
- quotes.extend(ev_by_source[sid])
221
-
222
- if not quotes and not cited_ids and not evidence_ids:
223
- # No links at all
224
- if claim_type == 'speculation':
225
- claim['support_status'] = 'supported' # Speculation doesn't need evidence
226
- else:
227
- claim['support_status'] = 'unsupported'
228
- elif not quotes:
229
- # Has cited sources but no evidence captured yet
230
- claim['support_status'] = 'needs_review'
231
- else:
232
- status, score, notes = compute_support_score(claim['text'], quotes)
233
- claim['support_status'] = status
234
- claim['_support_score'] = score
235
- claim['_support_notes'] = notes
236
-
237
- verified += 1
238
- updated_claims.append(claim)
239
-
240
- # Rewrite claims.jsonl with updated statuses
241
- write_jsonl(claims_path, updated_claims)
242
-
243
- # Compute summary
244
- status_counts = Counter(c.get('support_status') for c in updated_claims)
245
- factual_unsupported = sum(
246
- 1 for c in updated_claims
247
- if c.get('claim_type') == 'factual' and c.get('support_status') == 'unsupported'
248
- )
249
- total_factual = sum(1 for c in updated_claims if c.get('claim_type') == 'factual')
250
-
251
- # Strict mode: fail if any factual claim is unsupported
252
- passed = True
253
- if args.strict and factual_unsupported > 0:
254
- passed = False
255
-
256
- print(json.dumps({
257
- 'status': 'pass' if passed else 'fail',
258
- 'verified': verified,
259
- 'support_status_counts': dict(status_counts),
260
- 'factual_unsupported': factual_unsupported,
261
- 'total_factual': total_factual,
262
- 'unsupported_rate': round(factual_unsupported / max(total_factual, 1), 3),
263
- }, indent=2))
264
-
265
- if not passed:
266
- sys.exit(1)
267
-
268
-
269
- def cmd_report(args: argparse.Namespace) -> None:
270
- """Generate human-readable support verification report."""
271
- claims_path = os.path.join(args.dir, 'claims.jsonl')
272
- claims = read_jsonl(claims_path)
273
-
274
- # Deduplicate
275
- seen = set()
276
- unique = []
277
- for c in claims:
278
- cid = c.get('claim_id')
279
- if cid not in seen:
280
- seen.add(cid)
281
- unique.append(c)
282
-
283
- lines = ['# Claim Support Verification Report', '']
284
-
285
- # Summary
286
- status_counts = Counter(c.get('support_status') for c in unique)
287
- type_counts = Counter(c.get('claim_type') for c in unique)
288
- lines.append(f'**Total claims:** {len(unique)}')
289
- lines.append(f'**By type:** {dict(type_counts)}')
290
- lines.append(f'**By status:** {dict(status_counts)}')
291
- lines.append('')
292
-
293
- # Unsupported factual claims (the failures)
294
- unsupported_factual = [
295
- c for c in unique
296
- if c.get('claim_type') == 'factual' and c.get('support_status') in ('unsupported', 'needs_review')
297
- ]
298
- if unsupported_factual:
299
- lines.append('## Unsupported/Review-needed Factual Claims')
300
- lines.append('')
301
- for c in unsupported_factual:
302
- lines.append(f'- [{c["support_status"]}] `{c["section_id"]}`: {c["text"][:100]}...')
303
- if c.get('_support_notes'):
304
- lines.append(f' Notes: {c["_support_notes"]}')
305
- lines.append('')
306
-
307
- # All clear
308
- if not unsupported_factual:
309
- lines.append('## All factual claims have adequate support.')
310
- lines.append('')
311
-
312
- print('\n'.join(lines))
313
-
314
-
315
- # ---------------------------------------------------------------------------
316
- # CLI entry point
317
- # ---------------------------------------------------------------------------
318
-
319
- def main() -> None:
320
- parser = argparse.ArgumentParser(
321
- prog='verify_claim_support',
322
- description='Claim-support verification for deep-research v3.0',
323
- )
324
- sub = parser.add_subparsers(dest='command', required=True)
325
-
326
- # verify
327
- p_ver = sub.add_parser('verify', help='Verify claims against evidence')
328
- p_ver.add_argument('--dir', required=True, help='Run directory')
329
- p_ver.add_argument('--strict', action='store_true', help='Exit 1 if any factual claim unsupported')
330
-
331
- # report
332
- p_rep = sub.add_parser('report', help='Generate verification report')
333
- p_rep.add_argument('--dir', required=True, help='Run directory')
334
-
335
- args = parser.parse_args()
336
- dispatch = {
337
- 'verify': cmd_verify,
338
- 'report': cmd_report,
339
- }
340
- dispatch[args.command](args)
341
-
342
-
343
- if __name__ == '__main__':
344
- main()
@@ -1,220 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- HTML Report Verification Script
4
- Validates that HTML reports are properly generated with all sections from MD
5
- """
6
-
7
- import argparse
8
- import re
9
- from pathlib import Path
10
- from typing import List, Tuple
11
-
12
-
13
- class HTMLVerifier:
14
- """Verify HTML research reports"""
15
-
16
- def __init__(self, html_path: Path, md_path: Path):
17
- self.html_path = html_path
18
- self.md_path = md_path
19
- self.errors = []
20
- self.warnings = []
21
-
22
- def verify(self) -> bool:
23
- """
24
- Run all verification checks
25
-
26
- Returns:
27
- True if all checks pass, False otherwise
28
- """
29
- print(f"\n{'='*60}")
30
- print(f"HTML REPORT VERIFICATION")
31
- print(f"{'='*60}\n")
32
-
33
- print(f"HTML File: {self.html_path}")
34
- print(f"MD File: {self.md_path}\n")
35
-
36
- # Read files
37
- try:
38
- html_content = self.html_path.read_text()
39
- md_content = self.md_path.read_text()
40
- except Exception as e:
41
- self.errors.append(f"Failed to read files: {e}")
42
- return False
43
-
44
- # Run checks
45
- self._check_sections(html_content, md_content)
46
- self._check_no_placeholders(html_content)
47
- self._check_no_emojis(html_content)
48
- self._check_structure(html_content)
49
- self._check_citations(html_content, md_content)
50
- self._check_bibliography(html_content, md_content)
51
-
52
- # Report results
53
- self._print_results()
54
-
55
- return len(self.errors) == 0
56
-
57
- def _check_sections(self, html: str, md: str):
58
- """Verify all markdown sections are present in HTML"""
59
- # Extract section headings from markdown
60
- md_sections = re.findall(r'^## (.+)$', md, re.MULTILINE)
61
-
62
- # Extract sections from HTML
63
- html_sections = re.findall(r'<h2 class="section-title">(.+?)</h2>', html)
64
-
65
- # Check if we have placeholder sections like <div class="section">#</div>
66
- placeholder_sections = re.findall(r'<div class="section">#</div>', html)
67
-
68
- if placeholder_sections:
69
- self.errors.append(
70
- f"Found {len(placeholder_sections)} placeholder sections (empty '#' divs) - content not converted properly"
71
- )
72
-
73
- # Compare section counts
74
- if len(md_sections) > len(html_sections) + 1: # +1 for bibliography which is separate
75
- self.errors.append(
76
- f"Section count mismatch: MD has {len(md_sections)} sections, HTML has only {len(html_sections)} + bibliography"
77
- )
78
- missing = set(md_sections) - set(html_sections)
79
- if missing:
80
- self.errors.append(f"Missing sections in HTML: {missing}")
81
-
82
- # Verify Executive Summary is present
83
- if "Executive Summary" in md and "Executive Summary" not in html:
84
- self.errors.append("Executive Summary missing from HTML")
85
-
86
- def _check_no_placeholders(self, html: str):
87
- """Check for common placeholders that shouldn't be in final report"""
88
- placeholders = [
89
- '{{TITLE}}', '{{DATE}}', '{{CONTENT}}', '{{BIBLIOGRAPHY}}',
90
- '{{METRICS_DASHBOARD}}', '{{SOURCE_COUNT}}', 'TODO', 'TBD',
91
- 'PLACEHOLDER', 'FIXME'
92
- ]
93
-
94
- found = []
95
- for placeholder in placeholders:
96
- if placeholder in html:
97
- found.append(placeholder)
98
-
99
- if found:
100
- self.errors.append(f"Found unreplaced placeholders: {', '.join(found)}")
101
-
102
- def _check_no_emojis(self, html: str):
103
- """Verify no emojis are present in HTML"""
104
- # Common emoji patterns
105
- emoji_pattern = re.compile(
106
- "["
107
- "\U0001F600-\U0001F64F" # emoticons
108
- "\U0001F300-\U0001F5FF" # symbols & pictographs
109
- "\U0001F680-\U0001F6FF" # transport & map symbols
110
- "\U0001F1E0-\U0001F1FF" # flags
111
- "\U00002702-\U000027B0"
112
- "\U000024C2-\U0001F251"
113
- "]+",
114
- flags=re.UNICODE
115
- )
116
-
117
- emojis = emoji_pattern.findall(html)
118
- if emojis:
119
- unique_emojis = set(emojis)
120
- self.errors.append(f"Found {len(emojis)} emojis in HTML (should be none): {unique_emojis}")
121
-
122
- def _check_structure(self, html: str):
123
- """Verify HTML has proper structure"""
124
- required_elements = [
125
- ('<html', 'HTML tag'),
126
- ('<head', 'head tag'),
127
- ('<body', 'body tag'),
128
- ('<title>', 'title tag'),
129
- ('class="header"', 'header section'),
130
- ('class="content"', 'content section'),
131
- ('class="bibliography"', 'bibliography section'),
132
- ]
133
-
134
- for element, name in required_elements:
135
- if element not in html:
136
- self.errors.append(f"Missing {name} in HTML")
137
-
138
- # Check for unclosed tags (basic check)
139
- open_divs = html.count('<div')
140
- close_divs = html.count('</div>')
141
-
142
- if abs(open_divs - close_divs) > 2: # Allow small discrepancy
143
- self.warnings.append(
144
- f"Possible unclosed divs: {open_divs} opening tags, {close_divs} closing tags"
145
- )
146
-
147
- def _check_citations(self, html: str, md: str):
148
- """Verify citations are present"""
149
- # Extract citations from markdown
150
- md_citations = set(re.findall(r'\[(\d+)\]', md))
151
-
152
- # Extract citations from HTML (excluding bibliography)
153
- html_content = html.split('class="bibliography"')[0] if 'class="bibliography"' in html else html
154
- html_citations = set(re.findall(r'\[(\d+)\]', html_content))
155
-
156
- if len(md_citations) > 0 and len(html_citations) == 0:
157
- self.errors.append("No citations found in HTML content (but present in MD)")
158
-
159
- if len(md_citations) > len(html_citations) * 1.5: # Allow some variation
160
- self.warnings.append(
161
- f"Fewer citations in HTML ({len(html_citations)}) than MD ({len(md_citations)})"
162
- )
163
-
164
- def _check_bibliography(self, html: str, md: str):
165
- """Verify bibliography is present and formatted"""
166
- if '## Bibliography' in md:
167
- if 'class="bibliography"' not in html:
168
- self.errors.append("Bibliography section missing from HTML")
169
- elif 'class="bib-entry"' not in html:
170
- self.warnings.append("Bibliography present but entries not properly formatted")
171
-
172
- def _print_results(self):
173
- """Print verification results"""
174
- print(f"\n{'-'*60}")
175
- print("VERIFICATION RESULTS")
176
- print(f"{'-'*60}\n")
177
-
178
- if self.errors:
179
- print(f"❌ ERRORS ({len(self.errors)}):")
180
- for i, error in enumerate(self.errors, 1):
181
- print(f" {i}. {error}")
182
- print()
183
-
184
- if self.warnings:
185
- print(f"⚠️ WARNINGS ({len(self.warnings)}):")
186
- for i, warning in enumerate(self.warnings, 1):
187
- print(f" {i}. {warning}")
188
- print()
189
-
190
- if not self.errors and not self.warnings:
191
- print("✅ All checks passed! HTML report is valid.")
192
- print()
193
-
194
- print(f"{'-'*60}\n")
195
-
196
-
197
- def main():
198
- """Main entry point"""
199
- parser = argparse.ArgumentParser(description='Verify HTML research report')
200
- parser.add_argument('--html', type=Path, required=True, help='Path to HTML report')
201
- parser.add_argument('--md', type=Path, required=True, help='Path to markdown report')
202
-
203
- args = parser.parse_args()
204
-
205
- if not args.html.exists():
206
- print(f"Error: HTML file not found: {args.html}")
207
- return 1
208
-
209
- if not args.md.exists():
210
- print(f"Error: Markdown file not found: {args.md}")
211
- return 1
212
-
213
- verifier = HTMLVerifier(args.html, args.md)
214
- success = verifier.verify()
215
-
216
- return 0 if success else 1
217
-
218
-
219
- if __name__ == "__main__":
220
- exit(main())