@clawos-dev/clawd 0.2.47-beta.71.63ae386 → 0.2.47

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/cli.cjs +217 -115
  2. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/SKILL.md +187 -0
  3. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/archive-template.md +21 -0
  4. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/article-template.md +20 -0
  5. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/index-template.md +18 -0
  6. package/dist/persona-defaults/persona-knowledge-base/.claude/skills/karpathy-llm-wiki/references/raw-template.md +7 -0
  7. package/dist/persona-defaults/persona-knowledge-base/CLAUDE.md +105 -0
  8. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/README.md +119 -0
  9. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/SKILL.md +108 -0
  10. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/continuation.md +167 -0
  11. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/html-generation.md +103 -0
  12. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/methodology.md +421 -0
  13. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/quality-gates.md +192 -0
  14. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/report-assembly.md +130 -0
  15. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/reference/weasyprint_guidelines.md +324 -0
  16. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/requirements.txt +14 -0
  17. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/claim.schema.json +49 -0
  18. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/evidence.schema.json +43 -0
  19. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/run_manifest.schema.json +97 -0
  20. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/schemas/source.schema.json +49 -0
  21. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/citation_manager.py +300 -0
  22. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/evidence_store.py +205 -0
  23. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/extract_claims.py +358 -0
  24. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/md_to_html.py +330 -0
  25. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/research_engine.py +584 -0
  26. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/source_evaluator.py +292 -0
  27. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/validate_report.py +354 -0
  28. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_citations.py +426 -0
  29. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_claim_support.py +344 -0
  30. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/scripts/verify_html.py +220 -0
  31. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/mckinsey_report_template.html +443 -0
  32. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/templates/report_template.md +414 -0
  33. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/invalid_report.md +27 -0
  34. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/fixtures/valid_report.md +114 -0
  35. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_citation_manager.py +195 -0
  36. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_evidence_store.py +166 -0
  37. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_extract_claims.py +213 -0
  38. package/dist/persona-defaults/persona-researcher/.claude/skills/deep-research/tests/test_verify_claim_support.py +230 -0
  39. package/dist/persona-defaults/persona-researcher/CLAUDE.md +30 -0
  40. package/dist/persona-defaults/persona-researcher/skills-lock.json +11 -0
  41. package/package.json +2 -2
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """Smoke tests for citation_manager.py CLI."""
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+
11
+ SCRIPT = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'citation_manager.py')
12
+
13
+
14
+ def run_cm(*args: str) -> dict:
15
+ """Run citation_manager.py with args, return parsed JSON from stdout."""
16
+ result = subprocess.run(
17
+ [sys.executable, SCRIPT, *args],
18
+ capture_output=True, text=True,
19
+ )
20
+ if result.returncode != 0:
21
+ raise RuntimeError(f'Exit {result.returncode}: {result.stderr}')
22
+ return json.loads(result.stdout) if result.stdout.strip().startswith(('{', '[')) else result.stdout
23
+
24
+
25
+ class TestInitRun(unittest.TestCase):
26
+ def test_creates_manifest_and_artifacts(self):
27
+ with tempfile.TemporaryDirectory() as d:
28
+ out = run_cm('init-run', '--out-dir', d, '--query', 'test question', '--mode', 'deep')
29
+ self.assertEqual(out['status'], 'ok')
30
+
31
+ # Manifest exists and has correct fields
32
+ manifest = json.load(open(os.path.join(d, 'run_manifest.json')))
33
+ self.assertEqual(manifest['version'], '3.0.0')
34
+ self.assertEqual(manifest['query'], 'test question')
35
+ self.assertEqual(manifest['mode'], 'deep')
36
+ self.assertIsNotNone(manifest['started_at'])
37
+ self.assertIsNone(manifest['finished_at'])
38
+ self.assertEqual(manifest['artifact_paths']['sources'], 'sources.jsonl')
39
+
40
+ # Empty JSONL files exist
41
+ for name in ('sources.jsonl', 'evidence.jsonl', 'claims.jsonl'):
42
+ path = os.path.join(d, name)
43
+ self.assertTrue(os.path.exists(path), f'{name} missing')
44
+ self.assertEqual(os.path.getsize(path), 0)
45
+
46
+
47
+ class TestRegisterSource(unittest.TestCase):
48
+ def setUp(self):
49
+ self.tmpdir = tempfile.mkdtemp()
50
+ run_cm('init-run', '--out-dir', self.tmpdir, '--query', 'test')
51
+
52
+ def tearDown(self):
53
+ import shutil
54
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
55
+
56
+ def test_register_and_dedup(self):
57
+ src = json.dumps({
58
+ 'raw_url': 'https://arxiv.org/abs/2305.14251',
59
+ 'title': 'FActScore',
60
+ 'source_type': 'academic',
61
+ 'year': '2023',
62
+ })
63
+ out1 = run_cm('register-source', '--json', src, '--dir', self.tmpdir)
64
+ self.assertEqual(out1['status'], 'registered')
65
+ self.assertEqual(len(out1['source_id']), 16)
66
+ self.assertTrue(out1['canonical_locator'].startswith('arxiv:'))
67
+
68
+ # Same URL -> duplicate
69
+ out2 = run_cm('register-source', '--json', src, '--dir', self.tmpdir)
70
+ self.assertEqual(out2['status'], 'duplicate')
71
+ self.assertEqual(out2['source_id'], out1['source_id'])
72
+
73
+ def test_doi_canonicalization(self):
74
+ src = json.dumps({
75
+ 'raw_url': 'https://doi.org/10.1038/s41586-023-06745-9',
76
+ 'title': 'Some Nature paper',
77
+ })
78
+ out = run_cm('register-source', '--json', src, '--dir', self.tmpdir)
79
+ self.assertTrue(out['canonical_locator'].startswith('doi:10.1038/'))
80
+
81
+ def test_url_normalization(self):
82
+ src1 = json.dumps({
83
+ 'raw_url': 'https://Example.Com/article?utm_source=google&id=42',
84
+ 'title': 'Test',
85
+ })
86
+ src2 = json.dumps({
87
+ 'raw_url': 'https://example.com/article?id=42&utm_medium=email',
88
+ 'title': 'Test duplicate',
89
+ })
90
+ out1 = run_cm('register-source', '--json', src1, '--dir', self.tmpdir)
91
+ out2 = run_cm('register-source', '--json', src2, '--dir', self.tmpdir)
92
+ # Both should resolve to same canonical locator -> same source_id
93
+ self.assertEqual(out1['source_id'], out2['source_id'])
94
+ self.assertEqual(out2['status'], 'duplicate')
95
+
96
+
97
+ class TestAssignDisplayNumbers(unittest.TestCase):
98
+ def test_assigns_in_order(self):
99
+ with tempfile.TemporaryDirectory() as d:
100
+ run_cm('init-run', '--out-dir', d, '--query', 'test')
101
+
102
+ for i, url in enumerate(['https://a.com/1', 'https://b.com/2', 'https://c.com/3']):
103
+ run_cm('register-source', '--json', json.dumps({
104
+ 'raw_url': url, 'title': f'Source {i+1}',
105
+ }), '--dir', d)
106
+
107
+ mapping = run_cm('assign-display-numbers', '--dir', d)
108
+ self.assertEqual(len(mapping), 3)
109
+ # Values should be 1, 2, 3
110
+ self.assertEqual(sorted(mapping.values()), [1, 2, 3])
111
+
112
+
113
+ class TestExportBibliography(unittest.TestCase):
114
+ def test_markdown_export(self):
115
+ with tempfile.TemporaryDirectory() as d:
116
+ run_cm('init-run', '--out-dir', d, '--query', 'test')
117
+ run_cm('register-source', '--json', json.dumps({
118
+ 'raw_url': 'https://arxiv.org/abs/2305.14251',
119
+ 'title': 'FActScore',
120
+ 'authors': ['Min, S.', 'Krishna, K.'],
121
+ 'year': '2023',
122
+ 'source_type': 'academic',
123
+ }), '--dir', d)
124
+
125
+ out = run_cm('export-bibliography', '--dir', d, '--style', 'markdown')
126
+ self.assertIn('[1]', out)
127
+ self.assertIn('FActScore', out)
128
+ self.assertIn('Min, S. & Krishna, K.', out)
129
+
130
+ def test_json_export(self):
131
+ with tempfile.TemporaryDirectory() as d:
132
+ run_cm('init-run', '--out-dir', d, '--query', 'test')
133
+ run_cm('register-source', '--json', json.dumps({
134
+ 'raw_url': 'https://example.com/paper',
135
+ 'title': 'Test Paper',
136
+ }), '--dir', d)
137
+
138
+ out = run_cm('export-bibliography', '--dir', d, '--style', 'json')
139
+ self.assertEqual(len(out), 1)
140
+ self.assertEqual(out[0]['display_number'], 1)
141
+ self.assertEqual(out[0]['title'], 'Test Paper')
142
+
143
+
144
+ class TestCanonicalization(unittest.TestCase):
145
+ """Unit tests for canonicalize_locator without running the CLI."""
146
+
147
+ @classmethod
148
+ def setUpClass(cls):
149
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
150
+ from citation_manager import canonicalize_locator, compute_source_id
151
+ cls.canonicalize = staticmethod(canonicalize_locator)
152
+ cls.compute_id = staticmethod(compute_source_id)
153
+
154
+ def test_doi_from_url(self):
155
+ canonicalize_locator = self.canonicalize
156
+ self.assertEqual(
157
+ canonicalize_locator('https://doi.org/10.1038/s41586-023-06745-9'),
158
+ 'doi:10.1038/s41586-023-06745-9',
159
+ )
160
+ self.assertEqual(
161
+ canonicalize_locator('https://dx.doi.org/10.1234/test.'),
162
+ 'doi:10.1234/test',
163
+ )
164
+
165
+ def test_arxiv_from_url(self):
166
+ canonicalize_locator = self.canonicalize
167
+ self.assertEqual(
168
+ canonicalize_locator('https://arxiv.org/abs/2305.14251v2'),
169
+ 'arxiv:2305.14251v2',
170
+ )
171
+ self.assertEqual(
172
+ canonicalize_locator('arxiv:2401.15884'),
173
+ 'arxiv:2401.15884',
174
+ )
175
+
176
+ def test_url_strips_tracking(self):
177
+ canonicalize_locator = self.canonicalize
178
+ result = canonicalize_locator('https://Example.Com/page?utm_source=x&key=val')
179
+ self.assertNotIn('utm_source', result)
180
+ self.assertIn('key=val', result)
181
+ self.assertTrue(result.startswith('https://example.com'))
182
+
183
+ def test_url_strips_fragment(self):
184
+ canonicalize_locator = self.canonicalize
185
+ result = canonicalize_locator('https://example.com/page#section')
186
+ self.assertNotIn('#section', result)
187
+
188
+ def test_url_strips_trailing_slash(self):
189
+ canonicalize_locator = self.canonicalize
190
+ result = canonicalize_locator('https://example.com/page/')
191
+ self.assertFalse(result.endswith('/'))
192
+
193
+
194
+ if __name__ == '__main__':
195
+ unittest.main()
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """Smoke tests for evidence_store.py CLI."""
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+
12
+ SCRIPT = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'evidence_store.py')
13
+
14
+
15
+ def run_es(*args: str) -> dict | list:
16
+ """Run evidence_store.py with args, return parsed JSON from stdout."""
17
+ result = subprocess.run(
18
+ [sys.executable, SCRIPT, *args],
19
+ capture_output=True, text=True,
20
+ )
21
+ if result.returncode != 0:
22
+ raise RuntimeError(f'Exit {result.returncode}: {result.stderr}')
23
+ return json.loads(result.stdout)
24
+
25
+
26
+ class TestInit(unittest.TestCase):
27
+ def test_creates_empty_file(self):
28
+ with tempfile.TemporaryDirectory() as d:
29
+ out = run_es('init', '--dir', d)
30
+ self.assertEqual(out['status'], 'ok')
31
+ path = os.path.join(d, 'evidence.jsonl')
32
+ self.assertTrue(os.path.exists(path))
33
+ self.assertEqual(os.path.getsize(path), 0)
34
+
35
+
36
+ class TestAddEvidence(unittest.TestCase):
37
+ def setUp(self):
38
+ self.tmpdir = tempfile.mkdtemp()
39
+ run_es('init', '--dir', self.tmpdir)
40
+
41
+ def tearDown(self):
42
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
43
+
44
+ def test_add_and_dedup(self):
45
+ ev = json.dumps({
46
+ 'source_id': 'abcdef0123456789',
47
+ 'quote': 'FActScore decomposes generation into atomic facts.',
48
+ 'evidence_type': 'direct_quote',
49
+ 'locator': 'page 3',
50
+ 'retrieval_query': 'factuality evaluation methods',
51
+ })
52
+ out1 = run_es('add', '--json', ev, '--dir', self.tmpdir)
53
+ self.assertEqual(out1['status'], 'added')
54
+ self.assertEqual(len(out1['evidence_id']), 16)
55
+
56
+ # Same quote -> duplicate
57
+ out2 = run_es('add', '--json', ev, '--dir', self.tmpdir)
58
+ self.assertEqual(out2['status'], 'duplicate')
59
+ self.assertEqual(out2['evidence_id'], out1['evidence_id'])
60
+
61
+ def test_whitespace_normalization(self):
62
+ ev1 = json.dumps({
63
+ 'source_id': 'abcdef0123456789',
64
+ 'quote': ' FActScore decomposes generation into atomic facts. ',
65
+ 'evidence_type': 'direct_quote',
66
+ })
67
+ ev2 = json.dumps({
68
+ 'source_id': 'abcdef0123456789',
69
+ 'quote': 'FActScore decomposes generation into atomic facts.',
70
+ 'evidence_type': 'direct_quote',
71
+ })
72
+ out1 = run_es('add', '--json', ev1, '--dir', self.tmpdir)
73
+ out2 = run_es('add', '--json', ev2, '--dir', self.tmpdir)
74
+ # Should be same ID due to normalization
75
+ self.assertEqual(out1['evidence_id'], out2['evidence_id'])
76
+ self.assertEqual(out2['status'], 'duplicate')
77
+
78
+ def test_different_sources_different_ids(self):
79
+ ev1 = json.dumps({
80
+ 'source_id': 'aaaaaaaaaaaaaaaa',
81
+ 'quote': 'Same quote text.',
82
+ 'evidence_type': 'paraphrase',
83
+ })
84
+ ev2 = json.dumps({
85
+ 'source_id': 'bbbbbbbbbbbbbbbb',
86
+ 'quote': 'Same quote text.',
87
+ 'evidence_type': 'paraphrase',
88
+ })
89
+ out1 = run_es('add', '--json', ev1, '--dir', self.tmpdir)
90
+ out2 = run_es('add', '--json', ev2, '--dir', self.tmpdir)
91
+ self.assertNotEqual(out1['evidence_id'], out2['evidence_id'])
92
+ self.assertEqual(out2['status'], 'added')
93
+
94
+
95
+ class TestListAndExport(unittest.TestCase):
96
+ def setUp(self):
97
+ self.tmpdir = tempfile.mkdtemp()
98
+ run_es('init', '--dir', self.tmpdir)
99
+ # Add 3 evidence items from 2 sources
100
+ for src, quote in [
101
+ ('src_aaa', 'First quote from source A.'),
102
+ ('src_aaa', 'Second quote from source A.'),
103
+ ('src_bbb', 'Quote from source B.'),
104
+ ]:
105
+ run_es('add', '--json', json.dumps({
106
+ 'source_id': src,
107
+ 'quote': quote,
108
+ 'evidence_type': 'direct_quote',
109
+ }), '--dir', self.tmpdir)
110
+
111
+ def tearDown(self):
112
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
113
+
114
+ def test_list_all(self):
115
+ out = run_es('list', '--dir', self.tmpdir)
116
+ self.assertEqual(out['count'], 3)
117
+
118
+ def test_list_filtered(self):
119
+ out = run_es('list', '--dir', self.tmpdir, '--source-id', 'src_aaa')
120
+ self.assertEqual(out['count'], 2)
121
+
122
+ out = run_es('list', '--dir', self.tmpdir, '--source-id', 'src_bbb')
123
+ self.assertEqual(out['count'], 1)
124
+
125
+ def test_export(self):
126
+ out = run_es('export', '--dir', self.tmpdir)
127
+ self.assertIsInstance(out, list)
128
+ self.assertEqual(len(out), 3)
129
+ # Each has required fields
130
+ for row in out:
131
+ self.assertIn('evidence_id', row)
132
+ self.assertIn('source_id', row)
133
+ self.assertIn('quote', row)
134
+ self.assertIn('evidence_type', row)
135
+ self.assertIn('captured_at', row)
136
+
137
+
138
+ class TestEvidenceID(unittest.TestCase):
139
+ """Unit tests for compute_evidence_id."""
140
+
141
+ @classmethod
142
+ def setUpClass(cls):
143
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
144
+ from evidence_store import compute_evidence_id, normalize_quote
145
+ cls.compute_id = staticmethod(compute_evidence_id)
146
+ cls.normalize = staticmethod(normalize_quote)
147
+
148
+ def test_deterministic(self):
149
+ id1 = self.compute_id('src_a', 'test quote', 'page 1')
150
+ id2 = self.compute_id('src_a', 'test quote', 'page 1')
151
+ self.assertEqual(id1, id2)
152
+
153
+ def test_locator_matters(self):
154
+ id1 = self.compute_id('src_a', 'test quote', 'page 1')
155
+ id2 = self.compute_id('src_a', 'test quote', 'page 2')
156
+ self.assertNotEqual(id1, id2)
157
+
158
+ def test_normalize_whitespace(self):
159
+ self.assertEqual(
160
+ self.normalize(' hello world '),
161
+ 'hello world',
162
+ )
163
+
164
+
165
+ if __name__ == '__main__':
166
+ unittest.main()
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for extract_claims.py CLI."""
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+
12
+ SCRIPT = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'extract_claims.py')
13
+ FIXTURES = os.path.join(os.path.dirname(__file__), 'fixtures')
14
+
15
+
16
+ def run_ec(*args: str) -> dict | list:
17
+ """Run extract_claims.py with args."""
18
+ result = subprocess.run(
19
+ [sys.executable, SCRIPT, *args],
20
+ capture_output=True, text=True,
21
+ )
22
+ if result.returncode != 0:
23
+ raise RuntimeError(f'Exit {result.returncode}: {result.stderr}')
24
+ return json.loads(result.stdout)
25
+
26
+
27
+ SAMPLE_REPORT = """\
28
+ ---
29
+ title: Test Research Report
30
+ ---
31
+
32
+ ## Executive Summary
33
+
34
+ This report examines the impact of quantum computing on cryptography [1, 2]. The field has advanced significantly since 2020, with major breakthroughs in error correction.
35
+
36
+ ## Introduction
37
+
38
+ Quantum computing represents a paradigm shift in computational capability. Researchers at Google demonstrated quantum supremacy in 2019 using a 53-qubit processor [3]. This milestone confirmed theoretical predictions made decades earlier.
39
+
40
+ ## Finding 1
41
+
42
+ The Shor algorithm can factor large numbers exponentially faster than classical methods [4]. Current RSA-2048 encryption could be broken by a sufficiently large quantum computer. However, such machines are estimated to require millions of physical qubits [5, 6].
43
+
44
+ ## Finding 2
45
+
46
+ Post-quantum cryptography standards should be adopted within the next 5 years. Organizations should consider hybrid classical-quantum approaches during the transition period. NIST has already standardized several lattice-based algorithms [7].
47
+
48
+ ## Synthesis
49
+
50
+ Taken together, the evidence suggests that quantum computing poses a real but manageable threat to current cryptographic systems. The timeline for practical quantum attacks remains uncertain, but proactive migration reduces risk substantially.
51
+
52
+ ## Recommendations
53
+
54
+ Organizations should begin evaluating post-quantum cryptography solutions immediately. Security teams should conduct a cryptographic inventory to identify vulnerable systems. Companies should consider implementing crypto-agility frameworks to enable rapid algorithm switching.
55
+
56
+ ## Bibliography
57
+
58
+ [1] Smith et al. (2023). Quantum Computing Advances.
59
+ [2] Johnson (2024). Cryptographic Implications.
60
+ """
61
+
62
+
63
+ class TestExtract(unittest.TestCase):
64
+ def setUp(self):
65
+ self.tmpdir = tempfile.mkdtemp()
66
+ # Create empty claims.jsonl
67
+ open(os.path.join(self.tmpdir, 'claims.jsonl'), 'w').close()
68
+ # Write sample report
69
+ self.report_path = os.path.join(self.tmpdir, 'report.md')
70
+ with open(self.report_path, 'w') as f:
71
+ f.write(SAMPLE_REPORT)
72
+
73
+ def tearDown(self):
74
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
75
+
76
+ def test_extract_finds_claims(self):
77
+ out = run_ec('extract', '--report', self.report_path, '--dir', self.tmpdir)
78
+ self.assertEqual(out['status'], 'ok')
79
+ self.assertGreater(out['claims_added'], 5)
80
+
81
+ def test_extract_idempotent(self):
82
+ out1 = run_ec('extract', '--report', self.report_path, '--dir', self.tmpdir)
83
+ out2 = run_ec('extract', '--report', self.report_path, '--dir', self.tmpdir)
84
+ self.assertEqual(out2['claims_added'], 0)
85
+ self.assertEqual(out2['claims_skipped'], out1['claims_added'])
86
+
87
+ def test_claim_types_assigned(self):
88
+ run_ec('extract', '--report', self.report_path, '--dir', self.tmpdir)
89
+ out = run_ec('stats', '--dir', self.tmpdir)
90
+ # Should have at least factual and recommendation types
91
+ self.assertIn('factual', out['by_type'])
92
+ self.assertIn('recommendation', out['by_type'])
93
+
94
+ def test_sections_detected(self):
95
+ run_ec('extract', '--report', self.report_path, '--dir', self.tmpdir)
96
+ out = run_ec('stats', '--dir', self.tmpdir)
97
+ self.assertIn('finding_1', out['by_section'])
98
+ self.assertIn('finding_2', out['by_section'])
99
+ self.assertIn('recommendations', out['by_section'])
100
+
101
+
102
+ class TestAdd(unittest.TestCase):
103
+ def setUp(self):
104
+ self.tmpdir = tempfile.mkdtemp()
105
+ open(os.path.join(self.tmpdir, 'claims.jsonl'), 'w').close()
106
+
107
+ def tearDown(self):
108
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
109
+
110
+ def test_add_and_dedup(self):
111
+ claim = json.dumps({
112
+ 'section_id': 'finding_1',
113
+ 'text': 'Quantum computers can break RSA encryption.',
114
+ 'claim_type': 'factual',
115
+ })
116
+ out1 = run_ec('add', '--json', claim, '--dir', self.tmpdir)
117
+ self.assertEqual(out1['status'], 'added')
118
+ self.assertEqual(len(out1['claim_id']), 16)
119
+
120
+ out2 = run_ec('add', '--json', claim, '--dir', self.tmpdir)
121
+ self.assertEqual(out2['status'], 'duplicate')
122
+
123
+ def test_add_with_sources(self):
124
+ claim = json.dumps({
125
+ 'section_id': 'finding_1',
126
+ 'text': 'NIST standardized CRYSTALS-Kyber in 2024.',
127
+ 'claim_type': 'factual',
128
+ 'cited_source_ids': ['abcdef0123456789'],
129
+ 'evidence_ids': ['1234567890abcdef'],
130
+ })
131
+ out = run_ec('add', '--json', claim, '--dir', self.tmpdir)
132
+ self.assertEqual(out['status'], 'added')
133
+
134
+
135
+ class TestListAndStats(unittest.TestCase):
136
+ def setUp(self):
137
+ self.tmpdir = tempfile.mkdtemp()
138
+ open(os.path.join(self.tmpdir, 'claims.jsonl'), 'w').close()
139
+ # Add mixed claims
140
+ for sec, text, ctype in [
141
+ ('finding_1', 'The sky appears blue due to Rayleigh scattering.', 'factual'),
142
+ ('finding_1', 'Light wavelengths scatter differently in the atmosphere.', 'factual'),
143
+ ('synthesis', 'Overall, atmospheric optics explains most visual phenomena.', 'synthesis'),
144
+ ('recommendations', 'Researchers should investigate polarization effects further.', 'recommendation'),
145
+ ]:
146
+ run_ec('add', '--json', json.dumps({
147
+ 'section_id': sec, 'text': text, 'claim_type': ctype,
148
+ }), '--dir', self.tmpdir)
149
+
150
+ def tearDown(self):
151
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
152
+
153
+ def test_list_all(self):
154
+ out = run_ec('list', '--dir', self.tmpdir)
155
+ self.assertEqual(out['count'], 4)
156
+
157
+ def test_list_by_section(self):
158
+ out = run_ec('list', '--dir', self.tmpdir, '--section', 'finding_1')
159
+ self.assertEqual(out['count'], 2)
160
+
161
+ def test_list_by_type(self):
162
+ out = run_ec('list', '--dir', self.tmpdir, '--type', 'recommendation')
163
+ self.assertEqual(out['count'], 1)
164
+
165
+ def test_stats(self):
166
+ out = run_ec('stats', '--dir', self.tmpdir)
167
+ self.assertEqual(out['total'], 4)
168
+ self.assertEqual(out['by_type']['factual'], 2)
169
+ self.assertEqual(out['by_type']['synthesis'], 1)
170
+ self.assertEqual(out['by_type']['recommendation'], 1)
171
+
172
+
173
+ class TestClaimID(unittest.TestCase):
174
+ """Unit tests for compute_claim_id."""
175
+
176
+ @classmethod
177
+ def setUpClass(cls):
178
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
179
+ from extract_claims import compute_claim_id, classify_claim
180
+ cls.compute_id = staticmethod(compute_claim_id)
181
+ cls.classify = staticmethod(classify_claim)
182
+
183
+ def test_deterministic(self):
184
+ id1 = self.compute_id('finding_1', 'Test claim.')
185
+ id2 = self.compute_id('finding_1', 'Test claim.')
186
+ self.assertEqual(id1, id2)
187
+
188
+ def test_section_matters(self):
189
+ id1 = self.compute_id('finding_1', 'Same text.')
190
+ id2 = self.compute_id('finding_2', 'Same text.')
191
+ self.assertNotEqual(id1, id2)
192
+
193
+ def test_classify_recommendation(self):
194
+ self.assertEqual(
195
+ self.classify('Organizations should adopt PQC immediately.', 'recommendations'),
196
+ 'recommendation',
197
+ )
198
+
199
+ def test_classify_factual(self):
200
+ self.assertEqual(
201
+ self.classify('RSA-2048 uses 2048-bit keys.', 'finding_1'),
202
+ 'factual',
203
+ )
204
+
205
+ def test_classify_synthesis(self):
206
+ self.assertEqual(
207
+ self.classify('Taken together, the results indicate a clear trend.', 'synthesis'),
208
+ 'synthesis',
209
+ )
210
+
211
+
212
+ if __name__ == '__main__':
213
+ unittest.main()