Coverage for sdks / stigmem-py / src / stigmem / models.py: 96%
202 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Stigmem data models — spec v0.4/v0.5 fact model."""
3from __future__ import annotations
5from typing import Any, Literal
7from pydantic import BaseModel, Field
9# ---------------------------------------------------------------------------
10# FactValue
11# ---------------------------------------------------------------------------
13class StringValue(BaseModel):
14 type: Literal["string"]
15 v: str
18class TextValue(BaseModel):
19 type: Literal["text"]
20 v: str
23class NumberValue(BaseModel):
24 type: Literal["number"]
25 v: float
28class BooleanValue(BaseModel):
29 type: Literal["boolean"]
30 v: bool
33class DatetimeValue(BaseModel):
34 type: Literal["datetime"]
35 v: str # ISO 8601
38class RefValue(BaseModel):
39 type: Literal["ref"]
40 v: str # URI
43class NullValue(BaseModel):
44 type: Literal["null"]
46FactValue = (
47 StringValue
48 | TextValue
49 | NumberValue
50 | BooleanValue
51 | DatetimeValue
52 | RefValue
53 | NullValue
54)
56FactScope = Literal["local", "team", "company", "public"]
59def string_value(v: str) -> StringValue:
60 return StringValue(type="string", v=v)
63def text_value(v: str) -> TextValue:
64 return TextValue(type="text", v=v)
67def number_value(v: float) -> NumberValue:
68 return NumberValue(type="number", v=v)
71def boolean_value(v: bool) -> BooleanValue:
72 return BooleanValue(type="boolean", v=v)
75def datetime_value(v: str) -> DatetimeValue:
76 return DatetimeValue(type="datetime", v=v)
79def ref_value(v: str) -> RefValue:
80 return RefValue(type="ref", v=v)
83def null_value() -> NullValue:
84 return NullValue(type="null")
87# ---------------------------------------------------------------------------
88# Fact
89# ---------------------------------------------------------------------------
91class Fact(BaseModel):
92 id: str
93 entity: str
94 relation: str
95 value: FactValue
96 source: str
97 timestamp: str
98 hlc: str | None = None
99 valid_until: str | None = None
100 confidence: float
101 scope: FactScope
102 contradicted: bool = False
103 received_from: str | None = None
104 cid: str | None = None
106 model_config = {"extra": "allow"}
109class FactPage(BaseModel):
110 facts: list[Fact]
111 total: int
112 cursor: str | None = None
115# ---------------------------------------------------------------------------
116# Peer / Federation
117# ---------------------------------------------------------------------------
119PeerStatus = Literal["pending_verification", "active", "rejected", "revoked"]
122class Peer(BaseModel):
123 peer_id: str
124 node_id: str
125 node_url: str
126 status: PeerStatus
127 allowed_scopes: list[FactScope]
128 established_at: str | None = None
130 model_config = {"extra": "allow"}
133class PeerPage(BaseModel):
134 peers: list[Peer]
137# ---------------------------------------------------------------------------
138# Node info (/.well-known/stigmem)
139# ---------------------------------------------------------------------------
141class FederationEndpoints(BaseModel):
142 peers: str
143 facts: str
144 push: str | None = None
147class NodeInfo(BaseModel):
148 version: str
149 node_id: str
150 node_url: str
151 auth: Literal["none", "required"]
152 federation: Literal["disabled", "enabled"]
153 federation_pubkey: str | None = None
154 federation_version: str | None = None
155 federation_endpoints: FederationEndpoints | None = None
156 namespaces: list[str] = Field(default_factory=list)
157 spec: str | None = None
159 model_config = {"extra": "allow"}
162# ---------------------------------------------------------------------------
163# Conflicts
164# ---------------------------------------------------------------------------
166ConflictStatus = Literal["unresolved", "resolved"]
169class Conflict(BaseModel):
170 conflict_id: str
171 fact_a: Fact
172 fact_b: Fact
173 status: ConflictStatus
174 resolved_by: str | None = None
175 detected_at: str
177 model_config = {"extra": "allow"}
180class ConflictPage(BaseModel):
181 conflicts: list[Conflict]
182 cursor: str | None = None
183 has_more: bool = False
186class ConflictResolution(BaseModel):
187 resolution_fact_id: str
188 conflict_status: Literal["resolved"]
190 model_config = {"extra": "allow"}
193# ---------------------------------------------------------------------------
194# Assert / Query request shapes (convenience)
195# ---------------------------------------------------------------------------
197class AssertRequest(BaseModel):
198 entity: str
199 relation: str
200 value: FactValue
201 source: str
202 confidence: float = 1.0
203 scope: FactScope = "company"
204 valid_until: str | None = None
205 write_mode: str = "assert"
206 derived_from: list[dict[str, Any]] = Field(default_factory=list)
208 model_config = {"extra": "allow"}
211class ResolveRequest(BaseModel):
212 winning_fact_id: str | None = None
213 resolution_note: str = ""
214 new_value: FactValue | None = None
216 def model_dump_api(self) -> dict[str, Any]:
217 d: dict[str, Any] = {"resolution_note": self.resolution_note}
218 if self.winning_fact_id is not None: 218 ↛ 220line 218 didn't jump to line 220 because the condition on line 218 was always true
219 d["winning_fact_id"] = self.winning_fact_id
220 if self.new_value is not None: 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true
221 d["new_value"] = self.new_value.model_dump()
222 return d
225# ---------------------------------------------------------------------------
226# Recall (Phase 9 — spec §20)
227# ---------------------------------------------------------------------------
229class RecallWeights(BaseModel):
230 """Per-signal weights for the hybrid ranker."""
232 lexical: float = 0.35
233 semantic: float = 0.35
234 graph: float = 0.15
235 source_trust: float = 0.10
236 recency: float = 0.05
238 model_config = {"extra": "allow"}
241class RecallRequest(BaseModel):
242 query: str
243 scope: FactScope = "local"
244 token_budget: int = 4000
245 depth: int = 2
246 weights: RecallWeights = Field(default_factory=RecallWeights)
247 min_confidence: float = 0.1
248 include_neighbors: bool = True
249 limit: int = 100
251 model_config = {"extra": "allow"}
254class ScoreBreakdown(BaseModel):
255 lexical: float = 0.0
256 semantic: float = 0.0
257 graph: float = 0.0
258 source_trust: float = 0.0
259 recency: float = 0.0
260 weighted_total: float = 0.0
262 model_config = {"extra": "allow"}
265class ScoredFact(BaseModel):
266 fact: Fact
267 score: float
268 score_breakdown: ScoreBreakdown
269 hop_distance: int = 0
270 token_estimate: int
272 model_config = {"extra": "allow"}
275class FactChainCheckpointProof(BaseModel):
276 id: str
277 tenant_id: str
278 covered_chain_seq: int
279 chain_hash: str
280 status: str
281 attempt_count: int
282 created_at: str
283 submitted_at: str | None = None
284 last_error: str | None = None
285 tl_backend: str
286 tl_log_id: str | None = None
287 tl_leaf_hash: str | None = None
288 tl_log_index: int | None = None
289 tl_integrated_time: int | None = None
290 tl_inclusion_proof: dict[str, Any] = Field(default_factory=dict)
291 tl_raw: dict[str, Any] = Field(default_factory=dict)
293 model_config = {"extra": "allow"}
296class FactChainProof(BaseModel):
297 tenant_id: str
298 checked_entries: int
299 head_hash: str | None = None
300 checkpoint: FactChainCheckpointProof | None = None
302 model_config = {"extra": "allow"}
305class RecallResponse(BaseModel):
306 recall_id: str
307 query_hash: str
308 facts: list[ScoredFact]
309 content: list[ScoredFact] = Field(default_factory=list)
310 instructions: list[ScoredFact] = Field(default_factory=list)
311 total_scored: int | None
312 token_budget: int
313 tokens_used: int
314 truncated: bool
315 chain_proof: FactChainProof | None = None
317 model_config = {"extra": "allow"}
320# ---------------------------------------------------------------------------
321# Memory cards (Phase 9 — spec §20)
322# ---------------------------------------------------------------------------
324class MemoryCard(BaseModel):
325 """Per-entity synthesized summary card (spec §20)."""
327 entity_uri: str
328 scope: str
329 summary: str
330 fact_hashes: list[str]
331 avg_confidence: float
332 refreshed_at: str | None = None
333 is_stale: bool = False
334 has_contradictions: bool = False
336 model_config = {"extra": "allow"}