nkshirsa commited on
Commit
6672372
·
verified ·
1 Parent(s): 37b310e

Add phd_research_os_v2/layer7/obsidian.py

Browse files
Files changed (1) hide show
  1. phd_research_os_v2/layer7/obsidian.py +186 -0
phd_research_os_v2/layer7/obsidian.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Layer 7: Obsidian Export (v2)
3
+ ===============================
4
+ Export v2 claims, graph, conflicts to Obsidian vault with full provenance.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from ..core.database import get_db, from_fixed
12
+
13
+
14
+ class ObsidianExporterV2:
15
+ """Export Research OS v2 data to linked Obsidian vault."""
16
+
17
+ def __init__(self, vault_path: str = "vault", db_path: str = None):
18
+ self.vault_path = Path(vault_path)
19
+ self.db_path = db_path or os.environ.get("RESEARCH_OS_DB", "data/research_os_v2.db")
20
+ for d in ["Claims", "Sources", "Goals", "Conflicts", "Decisions", "Canonicals"]:
21
+ (self.vault_path / d).mkdir(parents=True, exist_ok=True)
22
+
23
+ def export_all(self) -> dict:
24
+ conn = get_db(self.db_path)
25
+ stats = {}
26
+
27
+ # Claims
28
+ claims = conn.execute("SELECT * FROM claims ORDER BY composite_confidence DESC").fetchall()
29
+ for c in claims:
30
+ self._export_claim(dict(c))
31
+ stats["claims"] = len(claims)
32
+
33
+ # Canonical claims
34
+ canonicals = conn.execute("SELECT * FROM canonical_claims ORDER BY evidence_count DESC").fetchall()
35
+ for c in canonicals:
36
+ self._export_canonical(dict(c))
37
+ stats["canonicals"] = len(canonicals)
38
+
39
+ # Conflicts
40
+ conflicts = conn.execute("SELECT * FROM conflicts").fetchall()
41
+ for c in conflicts:
42
+ self._export_conflict(dict(c))
43
+ stats["conflicts"] = len(conflicts)
44
+
45
+ # Goals
46
+ goals = conn.execute("SELECT * FROM goals").fetchall()
47
+ for g in goals:
48
+ self._export_goal(dict(g))
49
+ stats["goals"] = len(goals)
50
+
51
+ # Dashboard
52
+ self._export_dashboard(conn)
53
+ stats["dashboard"] = 1
54
+
55
+ conn.close()
56
+ return stats
57
+
58
+ def _export_claim(self, claim: dict):
59
+ cid = claim["claim_id"]
60
+ conf = from_fixed(claim.get("composite_confidence", 0))
61
+ ev_q = from_fixed(claim.get("evidence_quality", 0)) if claim.get("evidence_quality") else "—"
62
+ truth = from_fixed(claim.get("truth_likelihood", 0)) if claim.get("truth_likelihood") else "—"
63
+ qual_s = from_fixed(claim.get("qualifier_strength_score", 0)) if claim.get("qualifier_strength_score") else "—"
64
+ qualifiers = json.loads(claim.get("qualifiers", "[]")) if isinstance(claim.get("qualifiers"), str) else claim.get("qualifiers", [])
65
+ missing = json.loads(claim.get("missing_fields", "[]")) if isinstance(claim.get("missing_fields"), str) else claim.get("missing_fields", [])
66
+
67
+ content = f"""---
68
+ claim_id: {cid}
69
+ epistemic_tag: {claim.get('epistemic_tag', '')}
70
+ composite_confidence: {conf}
71
+ evidence_quality: {ev_q}
72
+ truth_likelihood: {truth}
73
+ qualifier_strength: {qual_s}
74
+ status: {claim.get('status', '')}
75
+ section: {claim.get('source_section', '')}
76
+ is_null_result: {bool(claim.get('is_null_result', 0))}
77
+ canonical_id: {claim.get('canonical_id', '')}
78
+ source_doi: {claim.get('source_doi', '')}
79
+ ---
80
+
81
+ ## {claim.get('text', '')}
82
+
83
+ ### Scores
84
+ | Score | Value |
85
+ |-------|-------|
86
+ | Composite Confidence | {conf:.3f} |
87
+ | Evidence Quality | {ev_q} |
88
+ | Truth Likelihood | {truth} |
89
+ | Qualifier Strength | {qual_s} |
90
+
91
+ ### Qualifiers
92
+ {', '.join(qualifiers) if qualifiers else 'None — claim is unhedged'}
93
+
94
+ ### Missing Fields
95
+ {', '.join(missing) if missing else 'None — claim is complete'}
96
+
97
+ ### Source
98
+ - **Section**: {claim.get('source_section', 'unknown')}
99
+ - **Page**: {claim.get('source_page', '?')}
100
+ - **Quote**: _{claim.get('source_quote', 'N/A')}_
101
+ - **DOI**: {claim.get('source_doi', 'N/A')}
102
+ {f'- **Canonical**: [[{claim.get("canonical_id", "")}]]' if claim.get('canonical_id') else ''}
103
+ """
104
+ (self.vault_path / "Claims" / f"{cid}.md").write_text(content)
105
+
106
+ def _export_canonical(self, canon: dict):
107
+ cid = canon["canonical_id"]
108
+ conf = from_fixed(canon.get("composite_confidence", 0))
109
+ aliases = json.loads(canon.get("aliases", "[]")) if isinstance(canon.get("aliases"), str) else canon.get("aliases", [])
110
+ sources = json.loads(canon.get("source_dois", "[]")) if isinstance(canon.get("source_dois"), str) else canon.get("source_dois", [])
111
+
112
+ content = f"""---
113
+ canonical_id: {cid}
114
+ epistemic_tag: {canon.get('epistemic_tag', '')}
115
+ confidence: {conf}
116
+ evidence_count: {canon.get('evidence_count', 0)}
117
+ ---
118
+
119
+ ## {canon.get('representative_text', '')}
120
+
121
+ ### Evidence ({canon.get('evidence_count', 0)} sources)
122
+ {chr(10).join(f'- {doi}' for doi in sources) if sources else 'No sources linked'}
123
+
124
+ ### Linked Claims
125
+ {chr(10).join(f'- [[{a}]]' for a in aliases) if aliases else 'No aliases'}
126
+ """
127
+ (self.vault_path / "Canonicals" / f"{cid}.md").write_text(content)
128
+
129
+ def _export_conflict(self, conflict: dict):
130
+ content = f"""---
131
+ conflict_id: {conflict['conflict_id']}
132
+ conflict_type: {conflict.get('conflict_type', '')}
133
+ resolution_status: {conflict.get('resolution_status', 'Unresolved')}
134
+ hypothesis_confidence: low
135
+ ---
136
+
137
+ ## Conflict: [[{conflict.get('claim_a_id', '')}]] vs [[{conflict.get('claim_b_id', '')}]]
138
+
139
+ **Type**: {conflict.get('conflict_type', '')}
140
+ **Status**: {conflict.get('resolution_status', 'Unresolved')}
141
+
142
+ ⚠️ **Hypothesis confidence: LOW** — Requires human review
143
+ """
144
+ (self.vault_path / "Conflicts" / f"{conflict['conflict_id']}.md").write_text(content)
145
+
146
+ def _export_goal(self, goal: dict):
147
+ content = f"""---
148
+ goal_id: {goal['goal_id']}
149
+ priority: {goal.get('priority', '')}
150
+ status: {goal.get('status', '')}
151
+ ---
152
+
153
+ ## {goal.get('description', '')}
154
+
155
+ **Priority**: {goal.get('priority', '')}
156
+ **Status**: {goal.get('status', '')}
157
+ """
158
+ (self.vault_path / "Goals" / f"{goal['goal_id']}.md").write_text(content)
159
+
160
+ def _export_dashboard(self, conn):
161
+ claims_count = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0]
162
+ canon_count = conn.execute("SELECT COUNT(*) FROM canonical_claims").fetchone()[0]
163
+ conflicts_count = conn.execute("SELECT COUNT(*) FROM conflicts WHERE resolution_status = 'Unresolved'").fetchone()[0]
164
+ goals_count = conn.execute("SELECT COUNT(*) FROM goals WHERE status = 'Active'").fetchone()[0]
165
+ docs_count = conn.execute("SELECT COUNT(*) FROM documents").fetchone()[0]
166
+
167
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
168
+
169
+ content = f"""---
170
+ type: dashboard
171
+ updated: {now}
172
+ ---
173
+
174
+ # PhD Research OS v2.0 Dashboard
175
+
176
+ *Updated: {now}*
177
+
178
+ | Metric | Count |
179
+ |--------|-------|
180
+ | Documents Ingested | {docs_count} |
181
+ | Total Claims | {claims_count} |
182
+ | Canonical Claims | {canon_count} |
183
+ | Unresolved Conflicts | {conflicts_count} |
184
+ | Active Goals | {goals_count} |
185
+ """
186
+ (self.vault_path / "Dashboard.md").write_text(content)