@heretek-ai/epistemic-swarm 0.7.0 → 0.7.2

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 (61) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/cli.js +1 -0
  3. package/extensions/pi/index.js +7 -1
  4. package/package.json +1 -1
  5. package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +76 -39
  6. package/plugins/factory/skills/factory/scripts/factory.py +1 -1
  7. package/plugins/opencode/index.js +41 -1
  8. package/plugins/opencode/tui.js +66 -38
  9. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  22. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  23. package/runner/claim_store.py +15 -4
  24. package/runner/living_dossiers.py +74 -56
  25. package/runner/mcp_server.py +37 -2
  26. package/runner/pcrb.py +53 -29
  27. package/runner/refinement.py +70 -27
  28. package/runner/research_swarm.py +336 -127
  29. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  36. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  37. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  38. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  39. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  40. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  41. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  44. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  45. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  46. package/runner/tests/test_backends.py +280 -0
  47. package/runner/tests/test_sweep_regressions.py +71 -38
  48. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  49. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  50. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  51. package/scripts/build_adapters.py +27 -18
  52. package/skills/darkharvest/scripts/harvest.py +76 -39
  53. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  54. package/skills/epistemic_search/scripts/search.py +29 -19
  55. package/skills/epistemic_search/scripts/webcache.py +29 -17
  56. package/skills/factory/scripts/factory.py +1 -1
  57. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  58. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  59. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  60. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  61. package/skills/swarm_config/configure.py +70 -37
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "High-Integrity Dialectic Research Agent Harness for Claude Code enforcing empirical evidence over parametric hallucination.",
5
5
  "author": {
6
6
  "name": "Heretek AI",
package/bin/cli.js CHANGED
@@ -74,6 +74,7 @@ Options:
74
74
  --mode <mode> Operating mode (research, audit, scout, hybrid, brainstorm)
75
75
  --engine <engine> Search engine (duckduckgo, brave, firecrawl, searxng)
76
76
  --depth <n> Max dialectic iterations (1-4)
77
+ --backend <b> Agent runtime: auto (host-native), claude, opencode
77
78
  --dir <path> Path to .research workspace directory (default: .research)
78
79
 
79
80
  Examples:
@@ -49,7 +49,13 @@ function callMcp(tool, args = {}, ctx = {}) {
49
49
  {
50
50
  encoding: 'utf-8',
51
51
  cwd: ctx.cwd || process.cwd(),
52
- env: { ...process.env, PYTHONPATH: PKG_ROOT },
52
+ // Project root: evidence defaults under <project>/.research instead
53
+ // of the server process cwd.
54
+ env: {
55
+ ...process.env,
56
+ PYTHONPATH: PKG_ROOT,
57
+ IUMBTEMS_PROJECT_DIR: ctx.cwd || process.cwd(),
58
+ },
53
59
  }
54
60
  );
55
61
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretek-ai/epistemic-swarm",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "IUMBTEMS: I Use My Brain To Express My Self — High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi, OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -59,11 +59,19 @@ TEXT_EXTS = {".md", ".json", ".toml", ".yaml", ".yml", ".txt", ".py", ".js", ".t
59
59
  DEFAULT_MAX_FILES = 120
60
60
  DEFAULT_MAX_BYTES = 400_000
61
61
  DEFAULT_TIMEOUT = 120
62
+ DEFAULT_BASE_DIR = ".research"
63
+ MAX_SCAN_DEPTH = 3
64
+ MAX_SINGLE_FILE_BYTES = 100_000
65
+ SNIPPET_CHARS = 4000
66
+
67
+ SPDX_APACHE2 = "Apache-2.0"
68
+ PERMISSIVE_SPDX = frozenset({"MIT", SPDX_APACHE2, "BSD-3-Clause", "ISC"})
69
+ COPYLEFT_SPDX = frozenset({"GPL", "AGPL"})
62
70
 
63
71
  SPDX_MAP = {
64
72
  "mit": "MIT",
65
- "apache-2.0": "Apache-2.0",
66
- "apache 2.0": "Apache-2.0",
73
+ "apache-2.0": SPDX_APACHE2,
74
+ "apache 2.0": SPDX_APACHE2,
67
75
  "bsd-3-clause": "BSD-3-Clause",
68
76
  "bsd 3-clause": "BSD-3-Clause",
69
77
  "isc": "ISC",
@@ -97,43 +105,74 @@ def detect_license(text):
97
105
 
98
106
 
99
107
  def scan_tree(root, max_files=DEFAULT_MAX_FILES, max_bytes=DEFAULT_MAX_BYTES):
100
- entries = []
101
- total_bytes = 0
102
- manifests = {}
103
- license_text = ""
104
- readme_head = ""
108
+ root = Path(root)
109
+ state = _ScanState()
105
110
  for dirpath, dirnames, filenames in os.walk(root):
106
111
  dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
107
112
  rel = Path(dirpath).relative_to(root)
108
- depth = len(rel.parts) if str(rel) != "." else 0
109
- if depth > 3:
113
+ if (len(rel.parts) if str(rel) != "." else 0) > MAX_SCAN_DEPTH:
110
114
  dirnames[:] = []
111
115
  continue
116
+ prefix = "" if str(rel) == "." else str(rel) + os.sep
112
117
  for f in sorted(filenames):
113
- if len(entries) >= max_files or total_bytes >= max_bytes:
114
- return entries, manifests, license_text, readme_head, True
115
- fp = Path(dirpath) / f
116
- try:
117
- size = fp.stat().st_size
118
- except OSError:
119
- continue
120
- if size > 100_000:
121
- continue
122
- rel_p = str(rel / f) if str(rel) != "." else f
123
- entries.append(rel_p)
124
- if f in MANIFEST_FILES or Path(f).suffix in TEXT_EXTS:
125
- try:
126
- content = fp.read_text(encoding="utf-8", errors="replace")
127
- except OSError:
128
- continue
129
- total_bytes += len(content.encode("utf-8", errors="replace"))
130
- if f in MANIFEST_FILES and f not in manifests:
131
- manifests[f] = content[:4000]
132
- if f.lower().startswith("license") and not license_text:
133
- license_text = content[:4000]
134
- if f.lower() == "readme.md" and not readme_head:
135
- readme_head = content[:4000]
136
- return entries, manifests, license_text, readme_head, False
118
+ if state.capped(max_files, max_bytes):
119
+ return state.finish(truncated=True)
120
+ _scan_file(dirpath, prefix + f, state)
121
+ return state.finish(truncated=False)
122
+
123
+
124
+ class _ScanState:
125
+ """Mutable accumulator for one scan_tree pass (keeps helpers small)."""
126
+
127
+ def __init__(self):
128
+ self.entries = []
129
+ self.total_bytes = 0
130
+ self.manifests = {}
131
+ self.license_text = ""
132
+ self.readme_head = ""
133
+
134
+ def capped(self, max_files, max_bytes):
135
+ return len(self.entries) >= max_files or self.total_bytes >= max_bytes
136
+
137
+ def finish(self, truncated):
138
+ return (
139
+ self.entries,
140
+ self.manifests,
141
+ self.license_text,
142
+ self.readme_head,
143
+ truncated,
144
+ )
145
+
146
+
147
+ def _scan_file(dirpath, rel_entry, state):
148
+ filename = os.path.basename(rel_entry)
149
+ try:
150
+ size = (Path(dirpath) / filename).stat().st_size
151
+ except OSError:
152
+ return
153
+ if size > MAX_SINGLE_FILE_BYTES:
154
+ return
155
+ state.entries.append(rel_entry)
156
+ if filename in MANIFEST_FILES or Path(filename).suffix in TEXT_EXTS:
157
+ _capture_text(dirpath, filename, state)
158
+
159
+
160
+ def _capture_text(dirpath, filename, state):
161
+ try:
162
+ content = (Path(dirpath) / filename).read_text(
163
+ encoding="utf-8", errors="replace"
164
+ )
165
+ except OSError:
166
+ return
167
+ state.total_bytes += len(content.encode("utf-8", errors="replace"))
168
+ snippet = content[:SNIPPET_CHARS]
169
+ if filename in MANIFEST_FILES and filename not in state.manifests:
170
+ state.manifests[filename] = snippet
171
+ lowered = filename.lower()
172
+ if lowered.startswith("license") and not state.license_text:
173
+ state.license_text = snippet
174
+ if lowered == "readme.md" and not state.readme_head:
175
+ state.readme_head = snippet
137
176
 
138
177
 
139
178
  def harvest_repo(
@@ -141,7 +180,7 @@ def harvest_repo(
141
180
  max_files=DEFAULT_MAX_FILES,
142
181
  max_bytes=DEFAULT_MAX_BYTES,
143
182
  timeout=DEFAULT_TIMEOUT,
144
- base_dir=".research",
183
+ base_dir=DEFAULT_BASE_DIR,
145
184
  ):
146
185
  started = datetime.now(timezone.utc).isoformat()
147
186
  tmp = tempfile.mkdtemp(prefix="darkharvest-")
@@ -162,9 +201,7 @@ def harvest_repo(
162
201
  )
163
202
  spdx = detect_license(license_text + manifests.get("package.json", ""))
164
203
  harvestable = (
165
- "depend-or-vendor"
166
- if spdx in ("MIT", "Apache-2.0", "BSD-3-Clause", "ISC")
167
- else "clean-room-rebuild-only"
204
+ "depend-or-vendor" if spdx in PERMISSIVE_SPDX else "clean-room-rebuild-only"
168
205
  )
169
206
  hasher = SourceHasher(Path(base_dir))
170
207
  cache_blob = (
@@ -174,7 +211,7 @@ def harvest_repo(
174
211
  )
175
212
  source_hash = hasher.store_source(url, cache_blob, f"Darkharvest {url}")
176
213
  warnings = []
177
- if spdx in ("GPL", "AGPL"):
214
+ if spdx in COPYLEFT_SPDX:
178
215
  warnings.append(
179
216
  "⚠️ LICENSE: strong copyleft — spec rebuild only, never vendor"
180
217
  )
@@ -209,7 +246,7 @@ def main():
209
246
  ap.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES)
210
247
  ap.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES)
211
248
  ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
212
- ap.add_argument("--dir", default=".research")
249
+ ap.add_argument("--dir", default=DEFAULT_BASE_DIR)
213
250
  args = ap.parse_args()
214
251
 
215
252
  if not re.match(r"^https?://", args.repo):
@@ -150,7 +150,7 @@ def cmd_expansion(args):
150
150
  state["expansion"]["loops_planned"] = n
151
151
  save_state(args.run, state)
152
152
  done = 0
153
- for i in range(1, n + 1):
153
+ for _ in range(1, n + 1):
154
154
  if stop_requested(args.run):
155
155
  print(f"🛑 STOP file present — halting after {done}/{n} loops.")
156
156
  break
@@ -71,7 +71,16 @@ function callMcp(tool, args = {}, cwd = undefined) {
71
71
  [MCP_SERVER, 'call', tool, JSON.stringify(args || {})],
72
72
  {
73
73
  cwd: cwd || process.cwd(),
74
- env: { ...process.env, PYTHONPATH: PKG_ROOT },
74
+ // Host hint: the runner defaults to the host-native backend
75
+ // (opencode run here), unless the caller passes backend explicitly.
76
+ // Project root: evidence defaults under <project>/.research instead
77
+ // of the server process cwd.
78
+ env: {
79
+ ...process.env,
80
+ PYTHONPATH: PKG_ROOT,
81
+ IUMBTEMS_HOST: 'opencode',
82
+ IUMBTEMS_PROJECT_DIR: cwd || process.cwd(),
83
+ },
75
84
  }
76
85
  );
77
86
  } catch (err) {
@@ -160,6 +169,11 @@ const TOOL_CATALOG = [
160
169
  description: 'Run in mock/dry-run mode without external API charges',
161
170
  default: false,
162
171
  },
172
+ backend: {
173
+ type: 'string',
174
+ enum: ['auto', 'claude', 'opencode'],
175
+ description: 'Agent runtime backend (default auto = host-native)',
176
+ },
163
177
  },
164
178
  required: ['objective'],
165
179
  },
@@ -180,6 +194,11 @@ const TOOL_CATALOG = [
180
194
  description: 'Run in mock mode without invoking LLM tokens',
181
195
  default: false,
182
196
  },
197
+ backend: {
198
+ type: 'string',
199
+ enum: ['auto', 'claude', 'opencode'],
200
+ description: 'Agent runtime backend (default auto = host-native)',
201
+ },
183
202
  },
184
203
  required: ['target'],
185
204
  },
@@ -200,6 +219,11 @@ const TOOL_CATALOG = [
200
219
  description: 'Run in mock mode without invoking LLM tokens',
201
220
  default: false,
202
221
  },
222
+ backend: {
223
+ type: 'string',
224
+ enum: ['auto', 'claude', 'opencode'],
225
+ description: 'Agent runtime backend (default auto = host-native)',
226
+ },
203
227
  },
204
228
  required: ['feature'],
205
229
  },
@@ -220,6 +244,11 @@ const TOOL_CATALOG = [
220
244
  description: 'Run in mock/dry-run mode without external API charges',
221
245
  default: false,
222
246
  },
247
+ backend: {
248
+ type: 'string',
249
+ enum: ['auto', 'claude', 'opencode'],
250
+ description: 'Agent runtime backend (default auto = host-native)',
251
+ },
223
252
  },
224
253
  required: ['objective'],
225
254
  },
@@ -253,6 +282,11 @@ const TOOL_CATALOG = [
253
282
  description: 'Run in mock mode without invoking LLM tokens',
254
283
  default: false,
255
284
  },
285
+ backend: {
286
+ type: 'string',
287
+ enum: ['auto', 'claude', 'opencode'],
288
+ description: 'Agent runtime backend (default auto = host-native)',
289
+ },
256
290
  },
257
291
  required: ['objective'],
258
292
  },
@@ -852,6 +886,12 @@ async function registerHostCommands(host) {
852
886
 
853
887
  async function registerHostTools(host) {
854
888
  if (typeof host?.tool?.transform !== 'function') return [];
889
+ // Accepted asymmetry (parity spec section 5): this plugin shape
890
+ // (command/tool transform + session hooks + event subscriptions) has no
891
+ // tool.execute.before registration point, so host webfetch/websearch calls
892
+ // cannot be intercepted the way Claude Code's hooks/hooks.json does.
893
+ // Steering lives in command templates (epistemic search first); a host API
894
+ // for pre-execution guards would close this for real.
855
895
  const registration = await host.tool.transform((draft) => {
856
896
  for (const [name, spec] of Object.entries(buildToolMap())) {
857
897
  draft.add({
@@ -48,6 +48,68 @@ function readJson(file) {
48
48
  }
49
49
  }
50
50
 
51
+ /** Config-derived status fields; nulls when config is absent/invalid. */
52
+ function configFields(research) {
53
+ const out = { mode: null, searchEngine: null, maxIterations: null };
54
+ const cfg = readJson(path.join(research, 'config.json'));
55
+ if (cfg && typeof cfg === 'object') {
56
+ out.mode = cfg.mode ?? null;
57
+ out.searchEngine = cfg.search_engine ?? null;
58
+ out.maxIterations = cfg.max_iterations ?? null;
59
+ }
60
+ return out;
61
+ }
62
+
63
+ const REPORT_NAMES = [
64
+ 'final_synthesis.md',
65
+ 'brainstorm_report.md',
66
+ 'code_audit_report.md',
67
+ 'oss_scout_report.md',
68
+ ];
69
+
70
+ /** Report files present in the workspace, with mtimes. */
71
+ function presentReports(research) {
72
+ const reports = [];
73
+ for (const name of REPORT_NAMES) {
74
+ try {
75
+ const st = statSync(path.join(research, name));
76
+ reports.push({ name, mtimeMs: st.mtimeMs });
77
+ } catch {
78
+ /* absent report */
79
+ }
80
+ }
81
+ return reports;
82
+ }
83
+
84
+ /** Count of frontier nodes not yet settled/closed, or null when absent. */
85
+ function openFrontierCount(research) {
86
+ const frontier = readJson(path.join(research, 'frontier.json'));
87
+ if (!frontier || typeof frontier !== 'object') return null;
88
+ const nodes = Array.isArray(frontier.nodes)
89
+ ? frontier.nodes
90
+ : Object.values(frontier.nodes || {});
91
+ return nodes.filter((n) => n && n.status !== 'settled' && n.status !== 'closed').length;
92
+ }
93
+
94
+ /** Degraded-claim counts from the ledger + requeue sidecar. */
95
+ function degradedCounts(research) {
96
+ const out = { stale: 0, suspect: 0, requeued: 0 };
97
+ const ledger = readJson(path.join(research, 'ledger', 'claim_status.json'));
98
+ if (Array.isArray(ledger)) {
99
+ const last = new Map();
100
+ for (const e of ledger) {
101
+ if (e && typeof e.claim_id === 'string') last.set(e.claim_id, e.to_status);
102
+ }
103
+ for (const s of last.values()) {
104
+ if (s === 'STALE') out.stale += 1;
105
+ else if (s === 'SUSPECT') out.suspect += 1;
106
+ }
107
+ }
108
+ const requeue = readJson(path.join(research, 'requeue.json'));
109
+ if (Array.isArray(requeue)) out.requeued = requeue.length;
110
+ return out;
111
+ }
112
+
51
113
  /**
52
114
  * Snapshot the workspace status. Pure fs reads; safe on missing workspace.
53
115
  * Exported for tests.
@@ -71,44 +133,10 @@ export function readSwarmStatus(root) {
71
133
  } catch {
72
134
  return status;
73
135
  }
74
- const cfg = readJson(path.join(research, 'config.json'));
75
- if (cfg && typeof cfg === 'object') {
76
- status.mode = cfg.mode ?? null;
77
- status.searchEngine = cfg.search_engine ?? null;
78
- status.maxIterations = cfg.max_iterations ?? null;
79
- }
80
- for (const name of [
81
- 'final_synthesis.md',
82
- 'brainstorm_report.md',
83
- 'code_audit_report.md',
84
- 'oss_scout_report.md',
85
- ]) {
86
- try {
87
- const st = statSync(path.join(research, name));
88
- status.reports.push({ name, mtimeMs: st.mtimeMs });
89
- } catch {
90
- /* absent report */
91
- }
92
- }
93
- const frontier = readJson(path.join(research, 'frontier.json'));
94
- if (frontier && typeof frontier === 'object') {
95
- const nodes = Array.isArray(frontier.nodes) ? frontier.nodes : Object.values(frontier.nodes || {});
96
- const open = nodes.filter((n) => n && n.status !== 'settled' && n.status !== 'closed');
97
- status.frontierOpen = open.length;
98
- }
99
- const ledger = readJson(path.join(research, 'ledger', 'claim_status.json'));
100
- if (Array.isArray(ledger)) {
101
- const last = new Map();
102
- for (const e of ledger) {
103
- if (e && typeof e.claim_id === 'string') last.set(e.claim_id, e.to_status);
104
- }
105
- for (const s of last.values()) {
106
- if (s === 'STALE') status.stale += 1;
107
- else if (s === 'SUSPECT') status.suspect += 1;
108
- }
109
- }
110
- const requeue = readJson(path.join(research, 'requeue.json'));
111
- if (Array.isArray(requeue)) status.requeued = requeue.length;
136
+ Object.assign(status, configFields(research));
137
+ status.reports = presentReports(research);
138
+ status.frontierOpen = openFrontierCount(research);
139
+ Object.assign(status, degradedCounts(research));
112
140
  return status;
113
141
  }
114
142
 
@@ -52,9 +52,15 @@ def _has_fts5(conn: sqlite3.Connection) -> bool:
52
52
  class ClaimStore:
53
53
  def __init__(self, base_dir: Path, db_path: Optional[Path] = None):
54
54
  self.base_dir = Path(os.path.realpath(str(base_dir)))
55
- self.db_path = Path(os.path.realpath(str(db_path))) if db_path else (self.base_dir / "claims.sqlite")
55
+ self.db_path = (
56
+ Path(os.path.realpath(str(db_path)))
57
+ if db_path
58
+ else (self.base_dir / "claims.sqlite")
59
+ )
56
60
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
57
- self.conn = sqlite3.connect(str(self.db_path))
61
+ # uri=False is explicit: a plain filesystem path must never be
62
+ # interpreted as a SQLite URI connection string (S8706).
63
+ self.conn = sqlite3.connect(str(self.db_path), uri=False)
58
64
  self.conn.row_factory = sqlite3.Row
59
65
  self.fts5 = _has_fts5(self.conn)
60
66
  self._create_schema()
@@ -115,7 +121,10 @@ class ClaimStore:
115
121
  );
116
122
  """
117
123
  )
118
- cur.execute("INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)", (str(SCHEMA_VERSION),))
124
+ cur.execute(
125
+ "INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)",
126
+ (str(SCHEMA_VERSION),),
127
+ )
119
128
  if self.fts5:
120
129
  cur.execute(
121
130
  """
@@ -338,7 +347,9 @@ def reindex(base_dir: Path, store: Optional[ClaimStore] = None) -> Dict[str, Any
338
347
 
339
348
  def main(argv: Optional[List[str]] = None) -> int:
340
349
  parser = argparse.ArgumentParser(description="IUMBTEMS derived claim index")
341
- parser.add_argument("--dir", default=".research", help="Path to .research workspace")
350
+ parser.add_argument(
351
+ "--dir", default=".research", help="Path to .research workspace"
352
+ )
342
353
  sub = parser.add_subparsers(dest="command")
343
354
  sub.add_parser("reindex", help="Rebuild claims.sqlite from flat files")
344
355
  search_p = sub.add_parser("search", help="Search claim statements")