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.
- epistemic_engine/__init__.py +19 -0
- epistemic_engine/analyze.py +433 -0
- epistemic_engine/beliefs/__init__.py +24 -0
- epistemic_engine/beliefs/extract.py +495 -0
- epistemic_engine/beliefs/mapper.py +109 -0
- epistemic_engine/beliefs/model.py +107 -0
- epistemic_engine/bench.py +141 -0
- epistemic_engine/cli.py +1425 -0
- epistemic_engine/compare.py +133 -0
- epistemic_engine/config.py +250 -0
- epistemic_engine/dashboard/__init__.py +5 -0
- epistemic_engine/dashboard/server.py +533 -0
- epistemic_engine/doctor.py +151 -0
- epistemic_engine/forecast.py +102 -0
- epistemic_engine/forge/__init__.py +12 -0
- epistemic_engine/forge/cache.py +50 -0
- epistemic_engine/forge/client.py +139 -0
- epistemic_engine/forge/sync.py +100 -0
- epistemic_engine/guard.py +487 -0
- epistemic_engine/guardbench.py +332 -0
- epistemic_engine/ingestion/__init__.py +17 -0
- epistemic_engine/ingestion/change_events.py +85 -0
- epistemic_engine/ingestion/git_extractor.py +265 -0
- epistemic_engine/ingestion/ingest.py +315 -0
- epistemic_engine/ingestion/parsers.py +250 -0
- epistemic_engine/justifications.py +229 -0
- epistemic_engine/net.py +142 -0
- epistemic_engine/ontology/__init__.py +17 -0
- epistemic_engine/ontology/data/po1-1.0.yaml +79 -0
- epistemic_engine/ontology/po1.py +325 -0
- epistemic_engine/packaging/__init__.py +5 -0
- epistemic_engine/packaging/eepkg.py +130 -0
- epistemic_engine/predict/__init__.py +25 -0
- epistemic_engine/predict/calibration.py +186 -0
- epistemic_engine/predict/causal.py +400 -0
- epistemic_engine/predict/patterns.py +283 -0
- epistemic_engine/progress.py +64 -0
- epistemic_engine/repo.py +33 -0
- epistemic_engine/report.py +343 -0
- epistemic_engine/reproducibility.py +63 -0
- epistemic_engine/revision/__init__.py +27 -0
- epistemic_engine/revision/agm.py +116 -0
- epistemic_engine/revision/confidence.py +106 -0
- epistemic_engine/revision/engine.py +565 -0
- epistemic_engine/revision/entrenchment.py +92 -0
- epistemic_engine/revision/falsification.py +105 -0
- epistemic_engine/security/__init__.py +14 -0
- epistemic_engine/security/cve.py +209 -0
- epistemic_engine/security/secrets.py +112 -0
- epistemic_engine/storage/__init__.py +9 -0
- epistemic_engine/storage/graph.py +290 -0
- epistemic_engine/temporal/__init__.py +17 -0
- epistemic_engine/temporal/engine.py +212 -0
- epistemic_engine/validation/__init__.py +27 -0
- epistemic_engine/validation/corpus.py +142 -0
- epistemic_engine/validation/gates.py +98 -0
- epistemic_engine/validation/kappa.py +59 -0
- epistemic_engine/validation/manifest.py +135 -0
- epistemic_engine/validation/release_gates.py +251 -0
- epistemic_engine/validation/stats.py +97 -0
- epistemic_engine/validation/validate.py +306 -0
- epistemic_engine/verify.py +81 -0
- epistemic_engine-1.2.0.dist-info/METADATA +213 -0
- epistemic_engine-1.2.0.dist-info/RECORD +69 -0
- epistemic_engine-1.2.0.dist-info/WHEEL +5 -0
- epistemic_engine-1.2.0.dist-info/entry_points.txt +2 -0
- epistemic_engine-1.2.0.dist-info/licenses/LICENSE +201 -0
- epistemic_engine-1.2.0.dist-info/licenses/NOTICE +7 -0
- 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
|
+
]
|