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

1"""Stigmem data models — spec v0.4/v0.5 fact model.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any, Literal 

6 

7from pydantic import BaseModel, Field 

8 

9# --------------------------------------------------------------------------- 

10# FactValue 

11# --------------------------------------------------------------------------- 

12 

13class StringValue(BaseModel): 

14 type: Literal["string"] 

15 v: str 

16 

17 

18class TextValue(BaseModel): 

19 type: Literal["text"] 

20 v: str 

21 

22 

23class NumberValue(BaseModel): 

24 type: Literal["number"] 

25 v: float 

26 

27 

28class BooleanValue(BaseModel): 

29 type: Literal["boolean"] 

30 v: bool 

31 

32 

33class DatetimeValue(BaseModel): 

34 type: Literal["datetime"] 

35 v: str # ISO 8601 

36 

37 

38class RefValue(BaseModel): 

39 type: Literal["ref"] 

40 v: str # URI 

41 

42 

43class NullValue(BaseModel): 

44 type: Literal["null"] 

45 

46FactValue = ( 

47 StringValue 

48 | TextValue 

49 | NumberValue 

50 | BooleanValue 

51 | DatetimeValue 

52 | RefValue 

53 | NullValue 

54) 

55 

56FactScope = Literal["local", "team", "company", "public"] 

57 

58 

59def string_value(v: str) -> StringValue: 

60 return StringValue(type="string", v=v) 

61 

62 

63def text_value(v: str) -> TextValue: 

64 return TextValue(type="text", v=v) 

65 

66 

67def number_value(v: float) -> NumberValue: 

68 return NumberValue(type="number", v=v) 

69 

70 

71def boolean_value(v: bool) -> BooleanValue: 

72 return BooleanValue(type="boolean", v=v) 

73 

74 

75def datetime_value(v: str) -> DatetimeValue: 

76 return DatetimeValue(type="datetime", v=v) 

77 

78 

79def ref_value(v: str) -> RefValue: 

80 return RefValue(type="ref", v=v) 

81 

82 

83def null_value() -> NullValue: 

84 return NullValue(type="null") 

85 

86 

87# --------------------------------------------------------------------------- 

88# Fact 

89# --------------------------------------------------------------------------- 

90 

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 

105 

106 model_config = {"extra": "allow"} 

107 

108 

109class FactPage(BaseModel): 

110 facts: list[Fact] 

111 total: int 

112 cursor: str | None = None 

113 

114 

115# --------------------------------------------------------------------------- 

116# Peer / Federation 

117# --------------------------------------------------------------------------- 

118 

119PeerStatus = Literal["pending_verification", "active", "rejected", "revoked"] 

120 

121 

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 

129 

130 model_config = {"extra": "allow"} 

131 

132 

133class PeerPage(BaseModel): 

134 peers: list[Peer] 

135 

136 

137# --------------------------------------------------------------------------- 

138# Node info (/.well-known/stigmem) 

139# --------------------------------------------------------------------------- 

140 

141class FederationEndpoints(BaseModel): 

142 peers: str 

143 facts: str 

144 push: str | None = None 

145 

146 

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 

158 

159 model_config = {"extra": "allow"} 

160 

161 

162# --------------------------------------------------------------------------- 

163# Conflicts 

164# --------------------------------------------------------------------------- 

165 

166ConflictStatus = Literal["unresolved", "resolved"] 

167 

168 

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 

176 

177 model_config = {"extra": "allow"} 

178 

179 

180class ConflictPage(BaseModel): 

181 conflicts: list[Conflict] 

182 cursor: str | None = None 

183 has_more: bool = False 

184 

185 

186class ConflictResolution(BaseModel): 

187 resolution_fact_id: str 

188 conflict_status: Literal["resolved"] 

189 

190 model_config = {"extra": "allow"} 

191 

192 

193# --------------------------------------------------------------------------- 

194# Assert / Query request shapes (convenience) 

195# --------------------------------------------------------------------------- 

196 

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) 

207 

208 model_config = {"extra": "allow"} 

209 

210 

211class ResolveRequest(BaseModel): 

212 winning_fact_id: str | None = None 

213 resolution_note: str = "" 

214 new_value: FactValue | None = None 

215 

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 

223 

224 

225# --------------------------------------------------------------------------- 

226# Recall (Phase 9 — spec §20) 

227# --------------------------------------------------------------------------- 

228 

229class RecallWeights(BaseModel): 

230 """Per-signal weights for the hybrid ranker.""" 

231 

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 

237 

238 model_config = {"extra": "allow"} 

239 

240 

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 

250 

251 model_config = {"extra": "allow"} 

252 

253 

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 

261 

262 model_config = {"extra": "allow"} 

263 

264 

265class ScoredFact(BaseModel): 

266 fact: Fact 

267 score: float 

268 score_breakdown: ScoreBreakdown 

269 hop_distance: int = 0 

270 token_estimate: int 

271 

272 model_config = {"extra": "allow"} 

273 

274 

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) 

292 

293 model_config = {"extra": "allow"} 

294 

295 

296class FactChainProof(BaseModel): 

297 tenant_id: str 

298 checked_entries: int 

299 head_hash: str | None = None 

300 checkpoint: FactChainCheckpointProof | None = None 

301 

302 model_config = {"extra": "allow"} 

303 

304 

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 

316 

317 model_config = {"extra": "allow"} 

318 

319 

320# --------------------------------------------------------------------------- 

321# Memory cards (Phase 9 — spec §20) 

322# --------------------------------------------------------------------------- 

323 

324class MemoryCard(BaseModel): 

325 """Per-entity synthesized summary card (spec §20).""" 

326 

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 

335 

336 model_config = {"extra": "allow"}