epistemic-engine 1.2.0__py3-none-any.whl

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 (69) hide show
  1. epistemic_engine/__init__.py +19 -0
  2. epistemic_engine/analyze.py +433 -0
  3. epistemic_engine/beliefs/__init__.py +24 -0
  4. epistemic_engine/beliefs/extract.py +495 -0
  5. epistemic_engine/beliefs/mapper.py +109 -0
  6. epistemic_engine/beliefs/model.py +107 -0
  7. epistemic_engine/bench.py +141 -0
  8. epistemic_engine/cli.py +1425 -0
  9. epistemic_engine/compare.py +133 -0
  10. epistemic_engine/config.py +250 -0
  11. epistemic_engine/dashboard/__init__.py +5 -0
  12. epistemic_engine/dashboard/server.py +533 -0
  13. epistemic_engine/doctor.py +151 -0
  14. epistemic_engine/forecast.py +102 -0
  15. epistemic_engine/forge/__init__.py +12 -0
  16. epistemic_engine/forge/cache.py +50 -0
  17. epistemic_engine/forge/client.py +139 -0
  18. epistemic_engine/forge/sync.py +100 -0
  19. epistemic_engine/guard.py +487 -0
  20. epistemic_engine/guardbench.py +332 -0
  21. epistemic_engine/ingestion/__init__.py +17 -0
  22. epistemic_engine/ingestion/change_events.py +85 -0
  23. epistemic_engine/ingestion/git_extractor.py +265 -0
  24. epistemic_engine/ingestion/ingest.py +315 -0
  25. epistemic_engine/ingestion/parsers.py +250 -0
  26. epistemic_engine/justifications.py +229 -0
  27. epistemic_engine/net.py +142 -0
  28. epistemic_engine/ontology/__init__.py +17 -0
  29. epistemic_engine/ontology/data/po1-1.0.yaml +79 -0
  30. epistemic_engine/ontology/po1.py +325 -0
  31. epistemic_engine/packaging/__init__.py +5 -0
  32. epistemic_engine/packaging/eepkg.py +130 -0
  33. epistemic_engine/predict/__init__.py +25 -0
  34. epistemic_engine/predict/calibration.py +186 -0
  35. epistemic_engine/predict/causal.py +400 -0
  36. epistemic_engine/predict/patterns.py +283 -0
  37. epistemic_engine/progress.py +64 -0
  38. epistemic_engine/repo.py +33 -0
  39. epistemic_engine/report.py +343 -0
  40. epistemic_engine/reproducibility.py +63 -0
  41. epistemic_engine/revision/__init__.py +27 -0
  42. epistemic_engine/revision/agm.py +116 -0
  43. epistemic_engine/revision/confidence.py +106 -0
  44. epistemic_engine/revision/engine.py +565 -0
  45. epistemic_engine/revision/entrenchment.py +92 -0
  46. epistemic_engine/revision/falsification.py +105 -0
  47. epistemic_engine/security/__init__.py +14 -0
  48. epistemic_engine/security/cve.py +209 -0
  49. epistemic_engine/security/secrets.py +112 -0
  50. epistemic_engine/storage/__init__.py +9 -0
  51. epistemic_engine/storage/graph.py +290 -0
  52. epistemic_engine/temporal/__init__.py +17 -0
  53. epistemic_engine/temporal/engine.py +212 -0
  54. epistemic_engine/validation/__init__.py +27 -0
  55. epistemic_engine/validation/corpus.py +142 -0
  56. epistemic_engine/validation/gates.py +98 -0
  57. epistemic_engine/validation/kappa.py +59 -0
  58. epistemic_engine/validation/manifest.py +135 -0
  59. epistemic_engine/validation/release_gates.py +251 -0
  60. epistemic_engine/validation/stats.py +97 -0
  61. epistemic_engine/validation/validate.py +306 -0
  62. epistemic_engine/verify.py +81 -0
  63. epistemic_engine-1.2.0.dist-info/METADATA +213 -0
  64. epistemic_engine-1.2.0.dist-info/RECORD +69 -0
  65. epistemic_engine-1.2.0.dist-info/WHEEL +5 -0
  66. epistemic_engine-1.2.0.dist-info/entry_points.txt +2 -0
  67. epistemic_engine-1.2.0.dist-info/licenses/LICENSE +201 -0
  68. epistemic_engine-1.2.0.dist-info/licenses/NOTICE +7 -0
  69. epistemic_engine-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,19 @@
1
+ """The Epistemic Engine — computational epistemology for software engineering.
2
+
3
+ Implements SRS v1.1. See README.md for the phased build plan.
4
+ """
5
+
6
+ __version__ = "1.2.0"
7
+
8
+ # Component/model versions participate in the reproducibility record (RGE-F10,
9
+ # REL-2). Bump a component version when its computation changes; a change
10
+ # invalidates cached reports (REL-4).
11
+ MODEL_VERSIONS = {
12
+ "belief_extraction": "1.1",
13
+ "justification_mining": "1.1",
14
+ "agm_revision": "1.1",
15
+ "confidence_model": "noisy_or-1.0",
16
+ "erosion_model": "change_driven-1.0",
17
+ }
18
+
19
+ __all__ = ["__version__", "MODEL_VERSIONS"]
@@ -0,0 +1,433 @@
1
+ """`ee analyze` — the Phase-2 epistemic pass (BEM + JRM + RIM-F15).
2
+
3
+ Runs after ingestion. Reads the HEAD tree from the graph, extracts and
4
+ formalizes beliefs onto PO-1, mines justifications, emits the change-event
5
+ stream, and writes it all back as a deterministic, idempotent subgraph
6
+ (Belief / Justification / ChangeEvent / Predicate / MetricSnapshot nodes and
7
+ their edges). Re-running reconciles to the same result.
8
+
9
+ Confidence, entrenchment, and falsification are intentionally left to the BRE
10
+ in Phase 3; beliefs are stored with ``confidence = None`` until then.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections import defaultdict
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from .beliefs import BeliefExtractor
21
+ from .beliefs.extract import aggregate
22
+ from .config import Config
23
+ from .ingestion.change_events import events_for_file, is_manifest
24
+ from .ingestion.git_extractor import GitExtractor
25
+ from .justifications import JustificationMiner, build_chain_metrics
26
+ from .ontology import Ontology
27
+ from .storage.graph import GraphStore
28
+
29
+ # node/edge types (7.1/7.2)
30
+ N_BELIEF, N_JUST, N_PREDICATE = "Belief", "Justification", "Predicate"
31
+ N_CHANGE, N_METRIC = "ChangeEvent", "MetricSnapshot"
32
+ E_ABOUT, E_HASPRED = "BELIEF_ABOUT", "BELIEF_PREDICATE"
33
+ E_SUBSUMES, E_INCOMPAT = "PREDICATE_SUBSUMES", "PREDICATE_INCOMPATIBLE"
34
+ E_JUSTIFIES, E_EVIDENCE = "JUSTIFIES", "JUSTIFICATION_EVIDENCE"
35
+ E_ERODES = "ERODES"
36
+
37
+ _PHASE2_TYPES = [N_BELIEF, N_JUST, N_PREDICATE, N_CHANGE, N_METRIC, "Falsifier",
38
+ "PullRequest", "Issue", "Comment"]
39
+
40
+
41
+ @dataclass
42
+ class AnalyzeResult:
43
+ repo_id: str
44
+ head_commit: str
45
+ belief_count: int
46
+ formalised: int
47
+ unformalised: int
48
+ ontology_coverage: float
49
+ coverage_by_language: dict[str, dict[str, int]]
50
+ coverage_by_domain: dict[str, dict[str, int]]
51
+ justification_count: int
52
+ justification_coverage: float
53
+ broken_chains: int
54
+ change_event_count: int
55
+ belief_density: float
56
+ forge_mode: str
57
+ graph_digest: str
58
+ overall_confidence: float | None = None
59
+ health: str | None = None
60
+ epistemic_debt: float | None = None
61
+ falsifier_count: int = 0
62
+ band_counts: dict[str, int] = field(default_factory=dict)
63
+ erosion_patterns: dict[str, int] = field(default_factory=dict)
64
+ orphaned_count: int = 0
65
+ epistemic_velocity: float = 0.0
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ return {
69
+ "repo_id": self.repo_id, "head_commit": self.head_commit,
70
+ "belief_count": self.belief_count, "formalised": self.formalised,
71
+ "unformalised": self.unformalised,
72
+ "ontology_coverage": round(self.ontology_coverage, 4),
73
+ "coverage_by_language": self.coverage_by_language,
74
+ "coverage_by_domain": self.coverage_by_domain,
75
+ "justification_count": self.justification_count,
76
+ "justification_coverage": round(self.justification_coverage, 4),
77
+ "broken_chains": self.broken_chains,
78
+ "change_event_count": self.change_event_count,
79
+ "belief_density": round(self.belief_density, 4),
80
+ "forge_mode": self.forge_mode,
81
+ "overall_confidence": round(self.overall_confidence, 4) if self.overall_confidence is not None else None,
82
+ "health": self.health,
83
+ "epistemic_debt": round(self.epistemic_debt, 2) if self.epistemic_debt is not None else None,
84
+ "falsifier_count": self.falsifier_count,
85
+ "band_counts": self.band_counts,
86
+ "erosion_patterns": self.erosion_patterns,
87
+ "orphaned_count": self.orphaned_count,
88
+ "epistemic_velocity": self.epistemic_velocity,
89
+ "graph_digest": self.graph_digest,
90
+ }
91
+
92
+
93
+ # Recent-history windows: commit_message justifications saturate within a source
94
+ # type (11.A.3) and erosion saturates by ~exp(-5), so a bounded recent window is
95
+ # lossless yet keeps analysis O(files) rather than O(files × full history).
96
+ _JUST_WINDOW = 12
97
+ _CE_WINDOW = 20
98
+
99
+
100
+ class Analyzer:
101
+ def __init__(self, git: GitExtractor, graph: GraphStore, config: Config, ontology: Ontology,
102
+ *, progress: bool = False) -> None:
103
+ self.git = git
104
+ self.graph = graph
105
+ self.config = config
106
+ self.ontology = ontology
107
+ self.progress = progress
108
+ self.extractor = BeliefExtractor(ontology)
109
+ self.miner = JustificationMiner(config)
110
+
111
+ def _read_source(self, path: str, head: str) -> str | None:
112
+ """Read a HEAD file, preferring the working tree (fast) over `git show`.
113
+
114
+ Right after ingest the working tree matches HEAD, so a direct disk read
115
+ avoids one subprocess per file. Falls back to `git show HEAD:path` when
116
+ the file is not on disk (bare repo / detached analysis).
117
+ """
118
+ try:
119
+ disk = Path(self.git.repo_path) / path
120
+ if disk.is_file():
121
+ return disk.read_text(encoding="utf-8", errors="replace")
122
+ except OSError:
123
+ pass
124
+ try:
125
+ return self.git.file_content(path, head)
126
+ except Exception:
127
+ return None
128
+
129
+ def _file_histories(self) -> dict[str, list[tuple[str, int, str]]]:
130
+ """Build per-file commit history from the graph (no per-file git calls).
131
+
132
+ Reuses the COMMIT_CHANGES_FILE edges recorded at ingest time, so a repo
133
+ with thousands of files is analysed without thousands of `git log`
134
+ subprocesses. Each list is (hash, author_date, message), oldest→newest.
135
+ """
136
+ commit_meta = {n.id: (int(n.props.get("author_date", 0)), str(n.props.get("message", "")))
137
+ for n in self.graph.iter_nodes("Commit")}
138
+ hist: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
139
+ for e in self.graph.iter_edges("COMMIT_CHANGES_FILE"):
140
+ if not e.dst.startswith("file:"):
141
+ continue
142
+ path = e.dst[len("file:"):]
143
+ ts, msg = commit_meta.get(e.src, (0, ""))
144
+ hist[path].append((e.src, ts, msg))
145
+ for path in hist:
146
+ hist[path].sort(key=lambda t: (t[1], t[0])) # (ts, hash) — stable/deterministic
147
+ return hist
148
+
149
+ def analyze(self) -> AnalyzeResult:
150
+ head = str(self.graph.get_meta("head_commit") or self.git.head_commit())
151
+ forge_mode = "git-native" # Mode 2 (RIM-F13); connected sync is a later phase
152
+ erosion_deltas = dict(self.config.get("epistemology.erosion_deltas", {}))
153
+
154
+ self._wipe_phase2()
155
+ self._add_ontology_subgraph()
156
+
157
+ entity_lookup = self._entity_lookup()
158
+ files = self._head_files()
159
+ file_history = self._file_histories()
160
+
161
+ from .progress import Progress
162
+ bar = Progress(" analyzing files", len(files), enabled=self.progress)
163
+
164
+ all_beliefs = []
165
+ language_of: dict[str, str | None] = {}
166
+ total_loc = 0
167
+ justifications = []
168
+ change_events = []
169
+
170
+ for path, lang, is_test in files:
171
+ bar.advance()
172
+ language_of[path] = lang
173
+ source = self._read_source(path, head)
174
+ if source is None:
175
+ continue
176
+ total_loc += source.count("\n") + 1
177
+ history = file_history.get(path, [])
178
+ timestamp = history[-1][1] if history else 0
179
+ recent = history[-_JUST_WINDOW:] # bounded window (saturation-lossless)
180
+
181
+ beliefs = self.extractor.extract_file(path, source, lang, timestamp)
182
+ all_beliefs.extend(beliefs)
183
+
184
+ # store beliefs + edges
185
+ for b in beliefs:
186
+ self._store_belief(b, entity_lookup)
187
+ # intrinsic justification (JRM-F10)
188
+ intrinsic = self.miner.mine_intrinsic(b)
189
+ if intrinsic:
190
+ justifications.append(intrinsic)
191
+ self._store_justification(intrinsic, evidence_node=f"file:{path}")
192
+ # commit justifications from recent history (JRM-F1,F7)
193
+ for chash, cts, msg in recent:
194
+ for j in self.miner.mine_commit(b, chash, msg, cts):
195
+ justifications.append(j)
196
+ self._store_justification(j, evidence_node=chash)
197
+
198
+ # change events (RIM-F15) — bounded to the recent window
199
+ evs = events_for_file(path, is_test, history, erosion_deltas)[-_CE_WINDOW:]
200
+ for ev in evs:
201
+ self._store_change_event(ev, [b for b in beliefs if b.timestamp <= ev.timestamp])
202
+ change_events.extend(evs)
203
+ bar.done(f"{len(all_beliefs)} beliefs")
204
+
205
+ # --- Documentation justifications (JRM-F5): design docs / ADRs / RFCs ---
206
+ justifications.extend(self._mine_docs(head, all_beliefs))
207
+
208
+ # --- Forge Mode 1: connected-sync justifications from cached PRs/issues -
209
+ forge_justifications = self._mine_forge(head)
210
+ justifications.extend(forge_justifications)
211
+ if forge_justifications or self._forge_present():
212
+ forge_mode = "connected-sync"
213
+
214
+ result_ext = aggregate(all_beliefs, language_of)
215
+ belief_ids = [b.id for b in all_beliefs]
216
+ chain = build_chain_metrics(belief_ids, justifications, truncated=(forge_mode == "git-native"))
217
+
218
+ density = (len(all_beliefs) / total_loc * 100.0) if total_loc else 0.0
219
+ self._update_repository(head, density, forge_mode)
220
+ self._store_metric_snapshot(head, all_beliefs, justifications, result_ext, head_ts=self._head_ts(head))
221
+
222
+ # --- Belief Revision Engine: entrenchment, confidence, falsification ---
223
+ from .revision.engine import BeliefRevisionEngine
224
+ rev = BeliefRevisionEngine(
225
+ self.git, self.graph, self.config, self.ontology, head, self._head_ts(head),
226
+ file_history=file_history,
227
+ ).run()
228
+
229
+ # --- Temporal Analysis Engine: trajectories, erosion, orphaned beliefs -
230
+ from .temporal.engine import TemporalAnalyzer
231
+ temporal = TemporalAnalyzer(self.graph, self.config, self._head_ts(head)).run()
232
+ self._temporal = temporal
233
+
234
+ self.graph.set_meta("forge_mode", forge_mode)
235
+ self.graph.set_meta("analyzed", True)
236
+ if hasattr(self.graph, "_conn"):
237
+ self.graph._conn.commit() # type: ignore[attr-defined]
238
+
239
+ return AnalyzeResult(
240
+ repo_id=str(self.graph.get_meta("repo_id") or ""),
241
+ head_commit=head,
242
+ belief_count=len(all_beliefs),
243
+ formalised=result_ext.formalised,
244
+ unformalised=result_ext.unformalised,
245
+ ontology_coverage=result_ext.ontology_coverage,
246
+ coverage_by_language=result_ext.coverage_by_language,
247
+ coverage_by_domain=result_ext.coverage_by_domain,
248
+ justification_count=len(justifications),
249
+ justification_coverage=chain.coverage,
250
+ broken_chains=len(chain.broken_belief_ids),
251
+ change_event_count=len(change_events),
252
+ belief_density=density,
253
+ forge_mode=forge_mode,
254
+ overall_confidence=rev.overall_confidence,
255
+ health=rev.health,
256
+ epistemic_debt=rev.epistemic_debt,
257
+ falsifier_count=rev.falsifier_count,
258
+ band_counts=rev.band_counts,
259
+ erosion_patterns=temporal["erosion_patterns"],
260
+ orphaned_count=temporal["orphaned_count"],
261
+ epistemic_velocity=temporal["epistemic_velocity"],
262
+ graph_digest=self.graph.digest(),
263
+ )
264
+
265
+ # -- storage helpers ----------------------------------------------------
266
+ def _store_belief(self, belief, entity_lookup: dict[tuple[str, str], str]) -> None:
267
+ self.graph.add_node(N_BELIEF, belief.id, belief.to_props())
268
+ # BELIEF_ABOUT -> entity if known, else the file node
269
+ subject = entity_lookup.get((belief.path, belief.subject_qname))
270
+ if subject is None:
271
+ subject = f"file:{belief.path}"
272
+ self.graph.add_edge(E_ABOUT, belief.id, subject, {"belief_type": belief.source_kind})
273
+ if belief.predicate_id:
274
+ self.graph.add_edge(E_HASPRED, belief.id, belief.predicate_id, {})
275
+
276
+ def _store_justification(self, j, evidence_node: str) -> None:
277
+ self.graph.add_node(N_JUST, j.id, j.to_props())
278
+ self.graph.add_edge(E_JUSTIFIES, j.id, j.belief_id,
279
+ {"weight": j.weight, "validity": j.validity, "source_type": j.source_type})
280
+ self.graph.add_edge(E_EVIDENCE, j.id, evidence_node, {"source_type": j.source_type})
281
+
282
+ def _store_change_event(self, ev, affected_beliefs) -> None:
283
+ self.graph.add_node(N_CHANGE, ev.id, ev.to_props())
284
+ for b in affected_beliefs:
285
+ self.graph.add_edge(E_ERODES, ev.id, b.id, {"erosion_delta": ev.erosion_delta})
286
+
287
+ def _repo_id(self) -> str:
288
+ return str(self.graph.get_meta("repo_id") or "")
289
+
290
+ def _mine_docs(self, head: str, beliefs) -> list:
291
+ """Justifications from documentation: design docs, ADRs, RFCs (JRM-F5).
292
+
293
+ A belief whose subject is named in a doc file gains a ``doc``
294
+ justification — the documentation stands as evidence for the belief.
295
+ """
296
+ import hashlib as _h
297
+ from .justifications import Justification
298
+
299
+ doc_paths: list[tuple[str, str]] = [] # (path, name)
300
+ for n in self.graph.iter_nodes("File"):
301
+ path, name = n.props.get("path", ""), n.props.get("name", "").lower()
302
+ ext = Path(name).suffix
303
+ is_doc = (ext in {".md", ".rst", ".adoc", ".txt"} or "/docs/" in f"/{path}"
304
+ or name.startswith(("adr", "rfc")) or "design" in name)
305
+ if is_doc:
306
+ doc_paths.append((path, name))
307
+ if not doc_paths:
308
+ return []
309
+
310
+ # index beliefs by subject last-component for cheap membership tests
311
+ subjects: dict[str, list] = defaultdict(list)
312
+ head_ts = self._head_ts(head)
313
+ for b in beliefs:
314
+ key = b.subject_qname.split(".")[-1]
315
+ if key:
316
+ subjects[key].append(b)
317
+
318
+ out = []
319
+ seen: set[str] = set()
320
+ for path, _name in sorted(doc_paths):
321
+ try:
322
+ text = self.git.file_content(path, head)
323
+ except Exception:
324
+ continue
325
+ for key, bs in subjects.items():
326
+ if len(key) < 3 or key not in text:
327
+ continue
328
+ for b in bs:
329
+ jid = "J:" + _h.sha1(f"{b.id}|doc|{path}".encode()).hexdigest()[:16]
330
+ if jid in seen:
331
+ continue
332
+ seen.add(jid)
333
+ out.append(Justification(
334
+ id=jid, belief_id=b.id, source_type="doc", source_id=path,
335
+ evidence_text=f"documented in {path}", weight=0.6, validity=1.0,
336
+ timestamp=head_ts, acquisition_mode="local",
337
+ ))
338
+ self._store_justification(
339
+ out[-1], evidence_node=f"file:{path}")
340
+ return out
341
+
342
+ def _forge_present(self) -> bool:
343
+ from .forge.cache import forge_cache_path
344
+ return forge_cache_path(self.config.data_dir, self._repo_id()).is_file()
345
+
346
+ def _mine_forge(self, head: str) -> list:
347
+ """Load cached forge data and mine Mode-1 justifications (JRM-F2-F4)."""
348
+ from .forge.cache import forge_cache_path, load_forge
349
+ from .forge.sync import mine_forge_justifications
350
+
351
+ data = load_forge(forge_cache_path(self.config.data_dir, self._repo_id()))
352
+ if data is None:
353
+ return []
354
+ # materialize PullRequest / Issue entities (7.1) with acquisition mode
355
+ for pr in data.prs:
356
+ self.graph.add_node("PullRequest", f"PR#{pr.number}", {
357
+ "number": pr.number, "title": pr.title, "acquisition_mode": "connected",
358
+ })
359
+ for issue in data.issues:
360
+ self.graph.add_node("Issue", f"issue#{issue.number}", {
361
+ "number": issue.number, "title": issue.title, "acquisition_mode": "connected",
362
+ })
363
+ justs = mine_forge_justifications(self.graph, data, self.config)
364
+ for j in justs:
365
+ evidence = j.source_id.split(":")[0] # PR#n / issue#n
366
+ self._store_justification(j, evidence_node=evidence)
367
+ return justs
368
+
369
+ def _add_ontology_subgraph(self) -> None:
370
+ for pid, pred in self.ontology.predicates.items():
371
+ self.graph.add_node(N_PREDICATE, pid, {
372
+ "label": pred.label, "domain": pred.domain,
373
+ "complement_id": pred.complement, "ontology_version": self.ontology.version,
374
+ })
375
+ for sub in self.ontology.predicate_ids():
376
+ for sup in self.ontology._direct_supers.get(sub, ()): # type: ignore[attr-defined]
377
+ self.graph.add_edge(E_SUBSUMES, sub, sup, {})
378
+ for a, b in self.ontology._declared_incompat: # type: ignore[attr-defined]
379
+ self.graph.add_edge(E_INCOMPAT, a, b, {})
380
+ self.graph.add_edge(E_INCOMPAT, b, a, {})
381
+
382
+ def _update_repository(self, head: str, density: float, forge_mode: str) -> None:
383
+ node = next(iter(self.graph.iter_nodes("Repository")), None)
384
+ if node is not None:
385
+ props = dict(node.props)
386
+ props["belief_density"] = round(density, 4)
387
+ props["forge_mode"] = forge_mode
388
+ self.graph.add_node("Repository", node.id, props)
389
+
390
+ def _store_metric_snapshot(self, head: str, beliefs, justifications, ext, head_ts: int) -> None:
391
+ falsifier_count = self.graph.count_nodes("Falsifier")
392
+ self.graph.add_node(N_METRIC, f"metric:{head}", {
393
+ "commit_hash": head,
394
+ "overall_confidence": None, # Phase 3
395
+ "belief_count": len(beliefs),
396
+ "formalised": ext.formalised,
397
+ "unformalised": ext.unformalised,
398
+ "justification_count": len(justifications),
399
+ "falsifier_count": falsifier_count,
400
+ "epistemic_debt": None, # Phase 4
401
+ "ontology_coverage": round(ext.ontology_coverage, 4),
402
+ "timestamp": head_ts,
403
+ })
404
+
405
+ # -- lookups ------------------------------------------------------------
406
+ def _entity_lookup(self) -> dict[tuple[str, str], str]:
407
+ lookup: dict[tuple[str, str], str] = {}
408
+ for kind in ("function", "method", "class", "variable", "import"):
409
+ for n in self.graph.iter_nodes(kind):
410
+ key = (n.props.get("path", ""), n.props.get("qualified_name", ""))
411
+ lookup.setdefault(key, n.id)
412
+ return lookup
413
+
414
+ def _head_files(self) -> list[tuple[str, str | None, bool]]:
415
+ files: list[tuple[str, str | None, bool]] = []
416
+ for n in self.graph.iter_nodes("File"):
417
+ files.append((n.props.get("path", ""), n.props.get("language") or None,
418
+ bool(n.props.get("is_test", False))))
419
+ return sorted(files)
420
+
421
+ def _head_ts(self, head: str) -> int:
422
+ node = self.graph.get_node(head)
423
+ return int(node.props.get("author_date", 0)) if node else 0
424
+
425
+ def _wipe_phase2(self) -> None:
426
+ if not hasattr(self.graph, "_conn"):
427
+ return
428
+ conn = self.graph._conn # type: ignore[attr-defined]
429
+ for ntype in _PHASE2_TYPES:
430
+ conn.execute(
431
+ "DELETE FROM edges WHERE src IN (SELECT id FROM nodes WHERE type=?) "
432
+ "OR dst IN (SELECT id FROM nodes WHERE type=?)", (ntype, ntype))
433
+ conn.execute("DELETE FROM nodes WHERE type=?", (ntype,))
@@ -0,0 +1,24 @@
1
+ """Belief Extraction Module (BEM) — SRS 4.2."""
2
+
3
+ from .model import (
4
+ DEFAULT_SCOPE,
5
+ DOMAINS,
6
+ SCOPE_VOCAB,
7
+ Belief,
8
+ BeliefSignal,
9
+ belief_id,
10
+ )
11
+ from .mapper import OntologyMapper
12
+ from .extract import BeliefExtractor, ExtractionResult
13
+
14
+ __all__ = [
15
+ "DEFAULT_SCOPE",
16
+ "DOMAINS",
17
+ "SCOPE_VOCAB",
18
+ "Belief",
19
+ "BeliefSignal",
20
+ "belief_id",
21
+ "OntologyMapper",
22
+ "BeliefExtractor",
23
+ "ExtractionResult",
24
+ ]