nkshirsa commited on
Commit
d86f9d6
·
verified ·
1 Parent(s): 0fd0e51

Add phd_research_os/db.py

Browse files
Files changed (1) hide show
  1. phd_research_os/db.py +521 -0
phd_research_os/db.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PhD Research OS — Core Data Layer (Phase 0)
3
+ ============================================
4
+ SQLite database with CRUD operations for Claim, Source, Goal, Conflict,
5
+ Decision, Override, and Experiment objects.
6
+
7
+ All objects carry schema_version tags. Fixed-point confidence math.
8
+ """
9
+
10
+ import sqlite3
11
+ import json
12
+ import uuid
13
+ import os
14
+ from datetime import datetime, timezone
15
+ from typing import Optional
16
+ from dataclasses import dataclass, field, asdict
17
+
18
+ SCHEMA_VERSION = "1.0"
19
+ DB_PATH = os.environ.get("RESEARCH_OS_DB", "data/research_os.db")
20
+
21
+
22
+ def get_db(db_path: str = None) -> sqlite3.Connection:
23
+ """Get database connection with WAL mode and foreign keys."""
24
+ path = db_path or DB_PATH
25
+ os.makedirs(os.path.dirname(path) if os.path.dirname(path) else ".", exist_ok=True)
26
+ conn = sqlite3.connect(path)
27
+ conn.row_factory = sqlite3.Row
28
+ conn.execute("PRAGMA journal_mode=WAL")
29
+ conn.execute("PRAGMA foreign_keys=ON")
30
+ return conn
31
+
32
+
33
+ def init_db(db_path: str = None):
34
+ """Initialize all tables."""
35
+ conn = get_db(db_path)
36
+ conn.executescript("""
37
+ CREATE TABLE IF NOT EXISTS claims (
38
+ claim_id TEXT PRIMARY KEY,
39
+ text TEXT NOT NULL,
40
+ status TEXT NOT NULL CHECK(status IN ('Complete', 'Incomplete')),
41
+ epistemic_tag TEXT NOT NULL CHECK(epistemic_tag IN ('Fact', 'Interpretation', 'Hypothesis', 'Conflict_Hypothesis')),
42
+ confidence INTEGER NOT NULL, -- Fixed-point: actual * 1000
43
+ evidence_strength INTEGER, -- Fixed-point: actual * 1000
44
+ study_type TEXT,
45
+ study_quality_weight INTEGER, -- Fixed-point: actual * 1000
46
+ journal_tier_weight INTEGER, -- Fixed-point: actual * 1000
47
+ completeness_penalty INTEGER, -- Fixed-point: actual * 1000
48
+ source_doi TEXT,
49
+ source_type TEXT,
50
+ missing_fields TEXT, -- JSON array
51
+ parameters TEXT, -- JSON dict
52
+ is_canonical INTEGER DEFAULT 0,
53
+ expert_override TEXT, -- JSON object if set
54
+ schema_version TEXT NOT NULL DEFAULT '1.0',
55
+ created_at TEXT NOT NULL,
56
+ updated_at TEXT NOT NULL
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS sources (
60
+ doi TEXT PRIMARY KEY,
61
+ title TEXT NOT NULL,
62
+ authors TEXT, -- JSON array
63
+ year INTEGER,
64
+ journal TEXT,
65
+ journal_tier INTEGER CHECK(journal_tier IN (1, 2, 3)),
66
+ study_type TEXT,
67
+ is_canonical INTEGER DEFAULT 0,
68
+ source_type TEXT,
69
+ schema_version TEXT NOT NULL DEFAULT '1.0',
70
+ created_at TEXT NOT NULL
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS goals (
74
+ goal_id TEXT PRIMARY KEY,
75
+ description TEXT NOT NULL,
76
+ priority TEXT NOT NULL CHECK(priority IN ('high', 'medium', 'low')),
77
+ status TEXT NOT NULL CHECK(status IN ('Active', 'Paused', 'Completed')),
78
+ linked_claim_ids TEXT, -- JSON array
79
+ schema_version TEXT NOT NULL DEFAULT '1.0',
80
+ created_at TEXT NOT NULL,
81
+ updated_at TEXT NOT NULL
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS conflicts (
85
+ conflict_id TEXT PRIMARY KEY,
86
+ claim_a_id TEXT NOT NULL,
87
+ claim_b_id TEXT NOT NULL,
88
+ conflict_type TEXT NOT NULL CHECK(conflict_type IN ('value_mismatch', 'methodology_difference', 'scope_difference', 'no_conflict')),
89
+ generated_hypothesis TEXT,
90
+ hypothesis_confidence TEXT DEFAULT 'low',
91
+ resolution_status TEXT NOT NULL DEFAULT 'Unresolved' CHECK(resolution_status IN ('Unresolved', 'Human_Resolved')),
92
+ resolution_notes TEXT,
93
+ key_differences TEXT, -- JSON array
94
+ schema_version TEXT NOT NULL DEFAULT '1.0',
95
+ created_at TEXT NOT NULL,
96
+ resolved_at TEXT,
97
+ FOREIGN KEY(claim_a_id) REFERENCES claims(claim_id),
98
+ FOREIGN KEY(claim_b_id) REFERENCES claims(claim_id)
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS decisions (
102
+ decision_id TEXT PRIMARY KEY,
103
+ linked_claim_ids TEXT, -- JSON array
104
+ recommended_action TEXT NOT NULL,
105
+ action_description TEXT,
106
+ expected_information_gain INTEGER, -- Fixed-point * 1000
107
+ linked_goal_id TEXT,
108
+ status TEXT NOT NULL DEFAULT 'Proposed' CHECK(status IN ('Proposed', 'Accepted', 'Rejected', 'Completed')),
109
+ why_not_log TEXT, -- Reason if rejected
110
+ priority TEXT DEFAULT 'medium',
111
+ estimated_effort TEXT,
112
+ schema_version TEXT NOT NULL DEFAULT '1.0',
113
+ created_at TEXT NOT NULL,
114
+ FOREIGN KEY(linked_goal_id) REFERENCES goals(goal_id)
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS overrides (
118
+ override_id TEXT PRIMARY KEY,
119
+ claim_id TEXT NOT NULL,
120
+ who TEXT NOT NULL,
121
+ rationale TEXT NOT NULL,
122
+ authoritative_source TEXT,
123
+ original_confidence INTEGER, -- Fixed-point * 1000
124
+ new_confidence INTEGER, -- Fixed-point * 1000
125
+ status TEXT DEFAULT 'Provisional',
126
+ schema_version TEXT NOT NULL DEFAULT '1.0',
127
+ created_at TEXT NOT NULL,
128
+ FOREIGN KEY(claim_id) REFERENCES claims(claim_id)
129
+ );
130
+
131
+ CREATE TABLE IF NOT EXISTS experiments (
132
+ experiment_id TEXT PRIMARY KEY,
133
+ date TEXT NOT NULL,
134
+ instrument TEXT,
135
+ measurement_type TEXT,
136
+ raw_file_path TEXT,
137
+ summary_statistics TEXT, -- JSON dict
138
+ linked_claim_ids TEXT, -- JSON array
139
+ approved INTEGER DEFAULT 0,
140
+ schema_version TEXT NOT NULL DEFAULT '1.0',
141
+ created_at TEXT NOT NULL
142
+ );
143
+
144
+ CREATE TABLE IF NOT EXISTS api_usage_log (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ timestamp TEXT NOT NULL,
147
+ model TEXT NOT NULL,
148
+ tokens_in INTEGER NOT NULL,
149
+ tokens_out INTEGER NOT NULL,
150
+ cost_usd REAL NOT NULL,
151
+ task_type TEXT
152
+ );
153
+
154
+ CREATE TABLE IF NOT EXISTS calibration_log (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ conflict_id TEXT,
157
+ system_predicted_confidence INTEGER, -- Fixed-point * 1000
158
+ human_actual_judgment TEXT,
159
+ timestamp TEXT NOT NULL
160
+ );
161
+
162
+ CREATE TABLE IF NOT EXISTS embedding_cache (
163
+ text_hash TEXT PRIMARY KEY,
164
+ embedding BLOB NOT NULL,
165
+ model TEXT NOT NULL,
166
+ created_at TEXT NOT NULL
167
+ );
168
+ """)
169
+ conn.commit()
170
+ conn.close()
171
+
172
+
173
+ # ============================================================
174
+ # Fixed-Point Math Utilities
175
+ # ============================================================
176
+
177
+ def to_fixed(value: float) -> int:
178
+ """Convert float to fixed-point integer (×1000).
179
+ Research OS Rule 5: All probability/confidence use scaled integers."""
180
+ return round(value * 1000)
181
+
182
+ def from_fixed(value: int) -> float:
183
+ """Convert fixed-point integer back to float."""
184
+ return value / 1000.0
185
+
186
+
187
+ # ============================================================
188
+ # CRUD Operations
189
+ # ============================================================
190
+
191
+ def now_iso() -> str:
192
+ return datetime.now(timezone.utc).isoformat()
193
+
194
+ def gen_id(prefix: str) -> str:
195
+ return f"{prefix}_{uuid.uuid4().hex[:8].upper()}"
196
+
197
+
198
+ # --- Claims ---
199
+
200
+ def create_claim(conn: sqlite3.Connection, text: str, epistemic_tag: str,
201
+ confidence: float, source_doi: str = None,
202
+ evidence_strength: float = None, study_type: str = None,
203
+ study_quality_weight: float = None, journal_tier_weight: float = None,
204
+ completeness_penalty: float = None, missing_fields: list = None,
205
+ parameters: dict = None, is_canonical: bool = False) -> str:
206
+ """Create a new Claim Object. Returns claim_id."""
207
+ claim_id = gen_id("CLM")
208
+ status = "Incomplete" if missing_fields else "Complete"
209
+ now = now_iso()
210
+
211
+ conn.execute("""
212
+ INSERT INTO claims (claim_id, text, status, epistemic_tag, confidence,
213
+ evidence_strength, study_type, study_quality_weight, journal_tier_weight,
214
+ completeness_penalty, source_doi, missing_fields, parameters,
215
+ is_canonical, schema_version, created_at, updated_at)
216
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
217
+ """, (claim_id, text, status, epistemic_tag, to_fixed(confidence),
218
+ to_fixed(evidence_strength) if evidence_strength else None,
219
+ study_type,
220
+ to_fixed(study_quality_weight) if study_quality_weight else None,
221
+ to_fixed(journal_tier_weight) if journal_tier_weight else None,
222
+ to_fixed(completeness_penalty) if completeness_penalty else None,
223
+ source_doi,
224
+ json.dumps(missing_fields or []),
225
+ json.dumps(parameters or {}),
226
+ int(is_canonical),
227
+ SCHEMA_VERSION, now, now))
228
+ conn.commit()
229
+ return claim_id
230
+
231
+
232
+ def get_claim(conn: sqlite3.Connection, claim_id: str) -> Optional[dict]:
233
+ """Get a single claim by ID."""
234
+ row = conn.execute("SELECT * FROM claims WHERE claim_id = ?", (claim_id,)).fetchone()
235
+ if row is None:
236
+ return None
237
+ d = dict(row)
238
+ # Convert fixed-point back to float
239
+ for key in ['confidence', 'evidence_strength', 'study_quality_weight',
240
+ 'journal_tier_weight', 'completeness_penalty']:
241
+ if d.get(key) is not None:
242
+ d[key] = from_fixed(d[key])
243
+ d['missing_fields'] = json.loads(d.get('missing_fields', '[]'))
244
+ d['parameters'] = json.loads(d.get('parameters', '{}'))
245
+ if d.get('expert_override'):
246
+ d['expert_override'] = json.loads(d['expert_override'])
247
+ return d
248
+
249
+
250
+ def update_claim(conn: sqlite3.Connection, claim_id: str, **kwargs) -> bool:
251
+ """Update claim fields. Fixed-point conversion applied automatically."""
252
+ fixed_fields = {'confidence', 'evidence_strength', 'study_quality_weight',
253
+ 'journal_tier_weight', 'completeness_penalty'}
254
+ json_fields = {'missing_fields', 'parameters', 'expert_override'}
255
+
256
+ updates = []
257
+ values = []
258
+ for key, val in kwargs.items():
259
+ if key in fixed_fields and val is not None:
260
+ updates.append(f"{key} = ?")
261
+ values.append(to_fixed(val))
262
+ elif key in json_fields and val is not None:
263
+ updates.append(f"{key} = ?")
264
+ values.append(json.dumps(val))
265
+ else:
266
+ updates.append(f"{key} = ?")
267
+ values.append(val)
268
+
269
+ updates.append("updated_at = ?")
270
+ values.append(now_iso())
271
+ values.append(claim_id)
272
+
273
+ if updates:
274
+ conn.execute(f"UPDATE claims SET {', '.join(updates)} WHERE claim_id = ?", values)
275
+ conn.commit()
276
+ return True
277
+ return False
278
+
279
+
280
+ def search_claims(conn: sqlite3.Connection, query: str = None,
281
+ epistemic_tag: str = None, min_confidence: float = None,
282
+ source_doi: str = None, limit: int = 50) -> list:
283
+ """Search claims with optional filters."""
284
+ conditions = []
285
+ params = []
286
+
287
+ if query:
288
+ conditions.append("text LIKE ?")
289
+ params.append(f"%{query}%")
290
+ if epistemic_tag:
291
+ conditions.append("epistemic_tag = ?")
292
+ params.append(epistemic_tag)
293
+ if min_confidence is not None:
294
+ conditions.append("confidence >= ?")
295
+ params.append(to_fixed(min_confidence))
296
+ if source_doi:
297
+ conditions.append("source_doi = ?")
298
+ params.append(source_doi)
299
+
300
+ where = " AND ".join(conditions) if conditions else "1=1"
301
+ rows = conn.execute(
302
+ f"SELECT * FROM claims WHERE {where} ORDER BY confidence DESC LIMIT ?",
303
+ params + [limit]
304
+ ).fetchall()
305
+
306
+ results = []
307
+ for row in rows:
308
+ d = dict(row)
309
+ for key in ['confidence', 'evidence_strength', 'study_quality_weight',
310
+ 'journal_tier_weight', 'completeness_penalty']:
311
+ if d.get(key) is not None:
312
+ d[key] = from_fixed(d[key])
313
+ d['missing_fields'] = json.loads(d.get('missing_fields', '[]'))
314
+ d['parameters'] = json.loads(d.get('parameters', '{}'))
315
+ results.append(d)
316
+ return results
317
+
318
+
319
+ # --- Sources ---
320
+
321
+ def create_source(conn: sqlite3.Connection, doi: str, title: str,
322
+ authors: list = None, year: int = None, journal: str = None,
323
+ journal_tier: int = None, study_type: str = None,
324
+ is_canonical: bool = False) -> str:
325
+ """Create a Source Object."""
326
+ conn.execute("""
327
+ INSERT OR REPLACE INTO sources (doi, title, authors, year, journal, journal_tier,
328
+ study_type, is_canonical, schema_version, created_at)
329
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
330
+ """, (doi, title, json.dumps(authors or []), year, journal, journal_tier,
331
+ study_type, int(is_canonical), SCHEMA_VERSION, now_iso()))
332
+ conn.commit()
333
+ return doi
334
+
335
+
336
+ def get_source(conn: sqlite3.Connection, doi: str) -> Optional[dict]:
337
+ """Get source by DOI."""
338
+ row = conn.execute("SELECT * FROM sources WHERE doi = ?", (doi,)).fetchone()
339
+ if row is None:
340
+ return None
341
+ d = dict(row)
342
+ d['authors'] = json.loads(d.get('authors', '[]'))
343
+ return d
344
+
345
+
346
+ # --- Goals ---
347
+
348
+ def create_goal(conn: sqlite3.Connection, description: str, priority: str,
349
+ linked_claim_ids: list = None) -> str:
350
+ """Create a Goal Object."""
351
+ goal_id = gen_id("GOAL")
352
+ now = now_iso()
353
+ conn.execute("""
354
+ INSERT INTO goals (goal_id, description, priority, status, linked_claim_ids,
355
+ schema_version, created_at, updated_at)
356
+ VALUES (?, ?, ?, 'Active', ?, ?, ?, ?)
357
+ """, (goal_id, description, priority, json.dumps(linked_claim_ids or []),
358
+ SCHEMA_VERSION, now, now))
359
+ conn.commit()
360
+ return goal_id
361
+
362
+
363
+ def get_goals_by_priority(conn: sqlite3.Connection, status: str = "Active") -> list:
364
+ """Get goals sorted by priority."""
365
+ priority_order = {"high": 1, "medium": 2, "low": 3}
366
+ rows = conn.execute(
367
+ "SELECT * FROM goals WHERE status = ? ORDER BY priority", (status,)
368
+ ).fetchall()
369
+ results = [dict(r) for r in rows]
370
+ for r in results:
371
+ r['linked_claim_ids'] = json.loads(r.get('linked_claim_ids', '[]'))
372
+ results.sort(key=lambda x: priority_order.get(x['priority'], 99))
373
+ return results
374
+
375
+
376
+ # --- Conflicts ---
377
+
378
+ def create_conflict(conn: sqlite3.Connection, claim_a_id: str, claim_b_id: str,
379
+ conflict_type: str, generated_hypothesis: str = None,
380
+ key_differences: list = None) -> str:
381
+ """Create a Conflict Resolution Object."""
382
+ conflict_id = gen_id("CONF")
383
+ conn.execute("""
384
+ INSERT INTO conflicts (conflict_id, claim_a_id, claim_b_id, conflict_type,
385
+ generated_hypothesis, hypothesis_confidence, key_differences,
386
+ schema_version, created_at)
387
+ VALUES (?, ?, ?, ?, ?, 'low', ?, ?, ?)
388
+ """, (conflict_id, claim_a_id, claim_b_id, conflict_type,
389
+ generated_hypothesis, json.dumps(key_differences or []),
390
+ SCHEMA_VERSION, now_iso()))
391
+ conn.commit()
392
+ return conflict_id
393
+
394
+
395
+ # --- Decisions ---
396
+
397
+ def create_decision(conn: sqlite3.Connection, recommended_action: str,
398
+ action_description: str, expected_info_gain: float,
399
+ linked_goal_id: str = None, linked_claim_ids: list = None,
400
+ priority: str = "medium", estimated_effort: str = None) -> str:
401
+ """Create a Decision Object."""
402
+ dec_id = gen_id("DEC")
403
+ conn.execute("""
404
+ INSERT INTO decisions (decision_id, linked_claim_ids, recommended_action,
405
+ action_description, expected_information_gain, linked_goal_id,
406
+ priority, estimated_effort, schema_version, created_at)
407
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
408
+ """, (dec_id, json.dumps(linked_claim_ids or []), recommended_action,
409
+ action_description, to_fixed(expected_info_gain), linked_goal_id,
410
+ priority, estimated_effort, SCHEMA_VERSION, now_iso()))
411
+ conn.commit()
412
+ return dec_id
413
+
414
+
415
+ # --- Overrides ---
416
+
417
+ def create_override(conn: sqlite3.Connection, claim_id: str, who: str,
418
+ rationale: str, new_confidence: float,
419
+ authoritative_source: str = None) -> str:
420
+ """Create an expert override. Locks claim confidence."""
421
+ override_id = gen_id("OVR")
422
+
423
+ # Get original confidence
424
+ claim = get_claim(conn, claim_id)
425
+ if not claim:
426
+ raise ValueError(f"Claim {claim_id} not found")
427
+
428
+ conn.execute("""
429
+ INSERT INTO overrides (override_id, claim_id, who, rationale,
430
+ authoritative_source, original_confidence, new_confidence,
431
+ schema_version, created_at)
432
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
433
+ """, (override_id, claim_id, who, rationale, authoritative_source,
434
+ to_fixed(claim['confidence']), to_fixed(new_confidence),
435
+ SCHEMA_VERSION, now_iso()))
436
+
437
+ # Update the claim with override
438
+ update_claim(conn, claim_id, confidence=new_confidence,
439
+ expert_override={"override_id": override_id, "who": who,
440
+ "rationale": rationale})
441
+ conn.commit()
442
+ return override_id
443
+
444
+
445
+ # --- API Usage Logging ---
446
+
447
+ def log_api_usage(conn: sqlite3.Connection, model: str, tokens_in: int,
448
+ tokens_out: int, cost_usd: float, task_type: str = None):
449
+ """Log API usage for cost tracking."""
450
+ conn.execute("""
451
+ INSERT INTO api_usage_log (timestamp, model, tokens_in, tokens_out, cost_usd, task_type)
452
+ VALUES (?, ?, ?, ?, ?, ?)
453
+ """, (now_iso(), model, tokens_in, tokens_out, cost_usd, task_type))
454
+ conn.commit()
455
+
456
+
457
+ def get_cost_summary(conn: sqlite3.Connection, days: int = 7) -> dict:
458
+ """Get API cost summary for the last N days."""
459
+ row = conn.execute("""
460
+ SELECT SUM(cost_usd) as total_cost, SUM(tokens_in) as total_in,
461
+ SUM(tokens_out) as total_out, COUNT(*) as num_calls
462
+ FROM api_usage_log
463
+ WHERE timestamp >= datetime('now', ?)
464
+ """, (f"-{days} days",)).fetchone()
465
+ return dict(row) if row else {"total_cost": 0, "total_in": 0, "total_out": 0, "num_calls": 0}
466
+
467
+
468
+ if __name__ == "__main__":
469
+ # Self-test
470
+ init_db("test_research_os.db")
471
+ conn = get_db("test_research_os.db")
472
+
473
+ # Test claim CRUD
474
+ cid = create_claim(conn, "Test claim about graphene", "Fact", 0.85,
475
+ evidence_strength=0.9, study_quality_weight=1.0,
476
+ journal_tier_weight=1.0, completeness_penalty=1.0)
477
+ claim = get_claim(conn, cid)
478
+ assert claim is not None
479
+ assert claim['confidence'] == 0.85
480
+ assert claim['status'] == 'Complete'
481
+
482
+ # Test with missing fields
483
+ cid2 = create_claim(conn, "Incomplete claim", "Hypothesis", 0.35,
484
+ missing_fields=["temperature", "pH"])
485
+ claim2 = get_claim(conn, cid2)
486
+ assert claim2['status'] == 'Incomplete'
487
+ assert len(claim2['missing_fields']) == 2
488
+
489
+ # Test source
490
+ create_source(conn, "10.1234/test", "Test Paper", ["Author A"], 2024,
491
+ "Nature", 1)
492
+ src = get_source(conn, "10.1234/test")
493
+ assert src is not None
494
+
495
+ # Test goal
496
+ gid = create_goal(conn, "Achieve LOD < 1 fM", "high", [cid])
497
+ goals = get_goals_by_priority(conn)
498
+ assert len(goals) == 1
499
+
500
+ # Test conflict
501
+ conf_id = create_conflict(conn, cid, cid2, "value_mismatch",
502
+ "Different measurement conditions")
503
+
504
+ # Test override
505
+ ovr_id = create_override(conn, cid, "Dr. Smith", "Expert knowledge", 0.95)
506
+ claim_updated = get_claim(conn, cid)
507
+ assert claim_updated['confidence'] == 0.95
508
+ assert claim_updated['expert_override'] is not None
509
+
510
+ # Test search
511
+ results = search_claims(conn, query="graphene")
512
+ assert len(results) >= 1
513
+
514
+ # Test fixed-point math
515
+ assert to_fixed(0.85) == 850
516
+ assert from_fixed(850) == 0.85
517
+ assert to_fixed(0.123) == 123
518
+
519
+ conn.close()
520
+ os.remove("test_research_os.db")
521
+ print("All database tests passed! ✓")