@clawos-dev/clawd 0.2.50 → 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.
- package/dist/persona-defaults/persona-clawd-helper/CLAUDE.md +1 -1
- package/dist/persona-defaults/persona-knowledge-base/CLAUDE.md +19 -0
- package/dist/persona-defaults/persona-researcher/CLAUDE.md +20 -1
- package/package.json +1 -1
- package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/SKILL.md +0 -187
- package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/archive-template.md +0 -21
- package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/article-template.md +0 -20
- package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/index-template.md +0 -18
- package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/raw-template.md +0 -7
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/README.md +0 -119
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/SKILL.md +0 -108
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/continuation.md +0 -167
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/html-generation.md +0 -103
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/methodology.md +0 -421
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/quality-gates.md +0 -192
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/report-assembly.md +0 -130
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/weasyprint_guidelines.md +0 -324
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/requirements.txt +0 -14
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/claim.schema.json +0 -49
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/evidence.schema.json +0 -43
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/run_manifest.schema.json +0 -97
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/source.schema.json +0 -49
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/citation_manager.py +0 -300
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/evidence_store.py +0 -205
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/extract_claims.py +0 -358
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/md_to_html.py +0 -330
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/research_engine.py +0 -584
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/source_evaluator.py +0 -292
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/validate_report.py +0 -354
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_citations.py +0 -426
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_claim_support.py +0 -344
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_html.py +0 -220
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/mckinsey_report_template.html +0 -443
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/report_template.md +0 -414
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/invalid_report.md +0 -27
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/valid_report.md +0 -114
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_citation_manager.py +0 -195
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_evidence_store.py +0 -166
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_extract_claims.py +0 -213
- package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_verify_claim_support.py +0 -230
- 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()
|
package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_html.py
DELETED
|
@@ -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())
|