Coverage for integrations / social / _models_local.py: 95.3%

1631 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2Local model definitions fallback. 

3Used when sql package (Hevolve_Database) is not installed (e.g. standalone Docker). 

4Schema kept in sync with Hevolve_Database via consolidation verification. 

5 

6This module is ONLY imported by models.py (line ~226) when sql.models is unavailable. 

7It imports Base, _uuid, _sanitize_html from models.py which are already defined by 

8the time this module is reached (no circular import issue). 

9""" 

10# noqa: E501 

11# ruff: noqa 

12 

13import uuid 

14from datetime import datetime 

15 

16from sqlalchemy import ( 

17 Column, String, Text, Integer, Float, Boolean, 

18 DateTime, JSON, ForeignKey, UniqueConstraint, Index, func 

19) 

20from sqlalchemy.orm import relationship 

21 

22# Import from parent models.py — these are already defined before models.py 

23# reaches the `from _models_local import ...` line, so partial-module import works. 

24from integrations.social.models import Base, _uuid, _sanitize_html 

25 

26 

27class User(Base): 

28 __tablename__ = 'users' 

29 

30 id = Column(String(64), primary_key=True, default=_uuid) 

31 username = Column(String(50), unique=True, nullable=False, index=True) 

32 display_name = Column(String(100), default='') 

33 email = Column(String(255), unique=True, nullable=True) 

34 password_hash = Column(String(255), nullable=True) 

35 bio = Column(Text, default='') 

36 avatar_url = Column(String(500), default='') 

37 user_type = Column(String(20), nullable=False) # 'human' | 'agent' 

38 agent_id = Column(String(100), nullable=True) # prompt_id_flow_id for agents 

39 api_token = Column(String(128), unique=True, index=True) 

40 is_verified = Column(Boolean, default=False) 

41 is_admin = Column(Boolean, default=False) 

42 is_moderator = Column(Boolean, default=False) 

43 is_banned = Column(Boolean, default=False) 

44 role = Column(String(20), default='flat') # 'central' | 'regional' | 'flat' 

45 karma_score = Column(Integer, default=0) 

46 task_karma = Column(Integer, default=0) 

47 post_count = Column(Integer, default=0) 

48 comment_count = Column(Integer, default=0) 

49 created_at = Column(DateTime, default=func.now()) 

50 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

51 last_active_at = Column(DateTime, default=func.now()) 

52 settings = Column(JSON, default=dict) 

53 owner_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) # human who owns this agent 

54 handle = Column(String(30), unique=True, nullable=True, index=True) # unique creator tag for humans 

55 local_name = Column(String(35), nullable=True) # 2-word local name for agents (before handle appended) 

56 referral_code = Column(String(20), unique=True, nullable=True, index=True) 

57 referred_by_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

58 region_id = Column(String(64), ForeignKey('regions.id', use_alter=True), nullable=True) 

59 level = Column(Integer, default=1) 

60 level_title = Column(String(30), default='Newcomer') 

61 location_sharing_enabled = Column(Boolean, default=False) 

62 last_location_lat = Column(Float, nullable=True) 

63 last_location_lon = Column(Float, nullable=True) 

64 last_location_at = Column(DateTime, nullable=True) 

65 idle_compute_opt_in = Column(Boolean, default=False) 

66 # Voice profile for agents (TTS preset, sample path, speaker embedding id). 

67 # JSON shape is engine-specific — kept as a free-form JSON blob so F5/Piper/ 

68 # Kokoro/Indic-Parler can each stash their own fields without a migration 

69 # per engine. Added v37. 

70 voice_profile = Column(JSON, nullable=True) 

71 

72 posts = relationship('Post', back_populates='author', lazy='dynamic') 

73 comments = relationship('Comment', back_populates='author', lazy='dynamic') 

74 notifications = relationship('Notification', back_populates='user', lazy='dynamic') 

75 skill_badges = relationship('AgentSkillBadge', back_populates='user', lazy='dynamic') 

76 owned_agents = relationship('User', foreign_keys=[owner_id], 

77 remote_side='User.id', backref='owner', 

78 lazy='select') 

79 

80 __table_args__ = ( 

81 UniqueConstraint('owner_id', 'local_name', name='uq_local_name_per_owner'), 

82 ) 

83 

84 def to_dict(self, include_token=False): 

85 d = { 

86 'id': self.id, 'username': self.username, 

87 'display_name': _sanitize_html(self.display_name) if self.display_name else self.display_name, 

88 'bio': _sanitize_html(self.bio) if self.bio else self.bio, 

89 'avatar_url': self.avatar_url, 'user_type': self.user_type, 

90 'agent_id': self.agent_id, 'handle': self.handle, 

91 'local_name': self.local_name, 'is_verified': self.is_verified, 

92 'role': self.role or 'flat', 

93 'is_admin': self.is_admin, 'is_moderator': self.is_moderator, 

94 'karma_score': self.karma_score, 'task_karma': self.task_karma, 

95 'post_count': self.post_count, 'comment_count': self.comment_count, 

96 'level': self.level, 'level_title': self.level_title, 

97 'referral_code': self.referral_code, 'region_id': self.region_id, 

98 'location_sharing_enabled': self.location_sharing_enabled, 

99 'idle_compute_opt_in': self.idle_compute_opt_in, 

100 'voice_profile': self.voice_profile, 

101 'created_at': self.created_at.isoformat() if self.created_at else None, 

102 'last_active_at': self.last_active_at.isoformat() if self.last_active_at else None, 

103 } 

104 if include_token: 

105 d['api_token'] = self.api_token 

106 return d 

107 

108 

109# ─── TABLE 2: communities ─── 

110 

111class Community(Base): 

112 __tablename__ = 'communities' 

113 

114 id = Column(String(64), primary_key=True, default=_uuid) 

115 name = Column(String(50), unique=True, nullable=False, index=True) 

116 display_name = Column(String(100), default='') 

117 description = Column(Text, default='') 

118 rules = Column(Text, default='') 

119 icon_url = Column(String(500), default='') 

120 banner_url = Column(String(500), default='') 

121 creator_id = Column(String(64), ForeignKey('users.id'), index=True) 

122 is_default = Column(Boolean, default=False) 

123 is_private = Column(Boolean, default=False) 

124 member_count = Column(Integer, default=0) 

125 post_count = Column(Integer, default=0) 

126 created_at = Column(DateTime, default=func.now()) 

127 

128 creator = relationship('User', foreign_keys=[creator_id]) 

129 posts = relationship('Post', back_populates='community', lazy='dynamic') 

130 memberships = relationship('CommunityMembership', back_populates='community', lazy='dynamic') 

131 

132 def to_dict(self): 

133 return { 

134 'id': self.id, 'name': self.name, 

135 'display_name': _sanitize_html(self.display_name) if self.display_name else self.display_name, 

136 'description': _sanitize_html(self.description) if self.description else self.description, 

137 'rules': _sanitize_html(self.rules) if self.rules else self.rules, 

138 'icon_url': self.icon_url, 

139 'banner_url': self.banner_url, 'creator_id': self.creator_id, 

140 'is_default': self.is_default, 'is_private': self.is_private, 

141 'member_count': self.member_count, 'post_count': self.post_count, 

142 'created_at': self.created_at.isoformat() if self.created_at else None, 

143 } 

144 

145 

146# ─── TABLE 3: posts ─── 

147 

148class Post(Base): 

149 __tablename__ = 'posts' 

150 

151 id = Column(String(64), primary_key=True, default=_uuid) 

152 author_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

153 community_id = Column(String(64), ForeignKey('communities.id', ondelete='CASCADE'), nullable=True, index=True) 

154 title = Column(String(300), nullable=False) 

155 content = Column(Text, default='') 

156 content_type = Column(String(20), default='text') # text|code|recipe|media|task_request 

157 code_language = Column(String(30), nullable=True) 

158 recipe_ref = Column(String(200), nullable=True) 

159 media_urls = Column(JSON, default=list) 

160 link_url = Column(String(1000), nullable=True) 

161 upvotes = Column(Integer, default=0) 

162 downvotes = Column(Integer, default=0) 

163 score = Column(Integer, default=0) 

164 comment_count = Column(Integer, default=0) 

165 view_count = Column(Integer, default=0) 

166 is_pinned = Column(Boolean, default=False) 

167 is_locked = Column(Boolean, default=False) 

168 is_deleted = Column(Boolean, default=False) 

169 is_hidden = Column(Boolean, default=False) 

170 embedding_id = Column(String(64), nullable=True) 

171 source_channel = Column(String(50), nullable=True) 

172 source_message_id = Column(String(200), nullable=True) 

173 boost_score = Column(Float, default=0.0) 

174 region_id = Column(String(64), ForeignKey('regions.id', use_alter=True), nullable=True) 

175 # Thought Experiment fields 

176 intent_category = Column(String(30), nullable=True) # community|environment|education|health|equity|technology 

177 hypothesis = Column(Text, nullable=True) # "If we do X, then Y" 

178 expected_outcome = Column(Text, nullable=True) # Expected net positive 

179 is_thought_experiment = Column(Boolean, default=False) 

180 dynamic_layout = Column(JSON, nullable=True) # Liquid UI layout JSON 

181 # Phase 7c.5 — per-post privacy. Mirrors sql.models.SocialPost. 

182 privacy = Column(String(16), nullable=True, index=True) 

183 created_at = Column(DateTime, default=func.now(), index=True) 

184 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

185 

186 author = relationship('User', back_populates='posts') 

187 community = relationship('Community', back_populates='posts') 

188 comments = relationship('Comment', back_populates='post', lazy='dynamic') 

189 

190 __table_args__ = ( 

191 Index('ix_posts_score_created', 'score', 'created_at'), 

192 ) 

193 

194 def to_dict(self, include_author=False): 

195 d = { 

196 'id': self.id, 'author_id': self.author_id, 

197 'community_id': self.community_id, 

198 'title': _sanitize_html(self.title) if self.title else self.title, 

199 'content': _sanitize_html(self.content) if self.content else self.content, 

200 'content_type': self.content_type, 

201 'code_language': self.code_language, 'recipe_ref': self.recipe_ref, 

202 'media_urls': self.media_urls or [], 'link_url': self.link_url, 

203 'upvotes': self.upvotes, 'downvotes': self.downvotes, 

204 'score': self.score, 'comment_count': self.comment_count, 

205 'view_count': self.view_count, 'is_pinned': self.is_pinned, 

206 'is_locked': self.is_locked, 'source_channel': self.source_channel, 

207 'boost_score': self.boost_score, 'region_id': self.region_id, 

208 'intent_category': self.intent_category, 

209 'hypothesis': self.hypothesis, 

210 'expected_outcome': self.expected_outcome, 

211 'is_thought_experiment': self.is_thought_experiment or False, 

212 'is_hidden': self.is_hidden or False, 

213 'dynamic_layout': self.dynamic_layout, 

214 'created_at': self.created_at.isoformat() if self.created_at else None, 

215 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

216 } 

217 # Phase 7c.5 — surface 'privacy' only when set; NULL stays absent 

218 # so flag-off deploys retain pre-migration JSON shape. 

219 if getattr(self, 'privacy', None): 

220 d['privacy'] = self.privacy 

221 if include_author and self.author: 

222 d['author'] = self.author.to_dict() 

223 return d 

224 

225 

226# ─── TABLE 4: comments ─── 

227 

228class Comment(Base): 

229 __tablename__ = 'comments' 

230 

231 id = Column(String(64), primary_key=True, default=_uuid) 

232 post_id = Column(String(64), ForeignKey('posts.id', ondelete='CASCADE'), nullable=False, index=True) 

233 author_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

234 parent_id = Column(String(64), ForeignKey('comments.id', ondelete='SET NULL'), nullable=True, index=True) 

235 content = Column(Text, nullable=False) 

236 upvotes = Column(Integer, default=0) 

237 downvotes = Column(Integer, default=0) 

238 score = Column(Integer, default=0) 

239 depth = Column(Integer, default=0) 

240 is_deleted = Column(Boolean, default=False) 

241 is_hidden = Column(Boolean, default=False) 

242 created_at = Column(DateTime, default=func.now()) 

243 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

244 

245 post = relationship('Post', back_populates='comments') 

246 author = relationship('User', back_populates='comments') 

247 parent = relationship('Comment', remote_side=[id], backref='replies') 

248 

249 def to_dict(self, include_author=False, include_replies=False): 

250 _content = self.content if not self.is_deleted else '[deleted]' 

251 d = { 

252 'id': self.id, 'post_id': self.post_id, 

253 'author_id': self.author_id, 'parent_id': self.parent_id, 

254 'content': _sanitize_html(_content) if _content else _content, 

255 'upvotes': self.upvotes, 'downvotes': self.downvotes, 

256 'score': self.score, 'depth': self.depth, 

257 'is_deleted': self.is_deleted, 

258 'is_hidden': self.is_hidden or False, 

259 'created_at': self.created_at.isoformat() if self.created_at else None, 

260 } 

261 if include_author and self.author: 

262 d['author'] = self.author.to_dict() 

263 if include_replies: 

264 d['replies'] = [r.to_dict(include_author=include_author, include_replies=True) 

265 for r in (self.replies or [])] 

266 return d 

267 

268 

269# ─── TABLE 5: votes ─── 

270 

271class Vote(Base): 

272 __tablename__ = 'votes' 

273 

274 id = Column(String(64), primary_key=True, default=_uuid) 

275 user_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

276 target_type = Column(String(10), nullable=False) # 'post' | 'comment' 

277 target_id = Column(String(64), nullable=False) 

278 value = Column(Integer, nullable=False) # +1 or -1 

279 created_at = Column(DateTime, default=func.now()) 

280 

281 __table_args__ = ( 

282 UniqueConstraint('user_id', 'target_type', 'target_id', name='uq_vote_user_target'), 

283 Index('ix_votes_target', 'target_type', 'target_id'), 

284 ) 

285 

286 

287# ─── TABLE 6: follows ─── 

288 

289class Follow(Base): 

290 __tablename__ = 'follows' 

291 

292 id = Column(String(64), primary_key=True, default=_uuid) 

293 follower_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

294 following_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

295 created_at = Column(DateTime, default=func.now()) 

296 

297 __table_args__ = ( 

298 UniqueConstraint('follower_id', 'following_id', name='uq_follow'), 

299 ) 

300 

301 

302# ─── TABLE 7: community_memberships ─── 

303 

304class CommunityMembership(Base): 

305 __tablename__ = 'community_memberships' 

306 

307 id = Column(String(64), primary_key=True, default=_uuid) 

308 user_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

309 community_id = Column(String(64), ForeignKey('communities.id', ondelete='CASCADE'), nullable=False, index=True) 

310 role = Column(String(20), default='member') # member|moderator|admin 

311 created_at = Column(DateTime, default=func.now()) 

312 

313 user = relationship('User') 

314 community = relationship('Community', back_populates='memberships') 

315 

316 __table_args__ = ( 

317 UniqueConstraint('user_id', 'community_id', name='uq_community_member'), 

318 ) 

319 

320 

321# ─── TABLE 8: agent_skill_badges ─── 

322 

323class AgentSkillBadge(Base): 

324 __tablename__ = 'agent_skill_badges' 

325 

326 id = Column(String(64), primary_key=True, default=_uuid) 

327 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

328 skill_name = Column(String(100), nullable=False) 

329 proficiency = Column(Float, default=1.0) 

330 usage_count = Column(Integer, default=0) 

331 success_rate = Column(Float, default=0.0) 

332 badge_level = Column(String(20), default='bronze') # bronze|silver|gold|platinum 

333 awarded_at = Column(DateTime, default=func.now()) 

334 

335 user = relationship('User', back_populates='skill_badges') 

336 

337 def to_dict(self): 

338 return { 

339 'id': self.id, 'skill_name': self.skill_name, 

340 'proficiency': self.proficiency, 'usage_count': self.usage_count, 

341 'success_rate': self.success_rate, 'badge_level': self.badge_level, 

342 'awarded_at': self.awarded_at.isoformat() if self.awarded_at else None, 

343 } 

344 

345 

346# ─── TABLE 9: task_requests ─── 

347 

348class TaskRequest(Base): 

349 __tablename__ = 'task_requests' 

350 

351 id = Column(String(64), primary_key=True, default=_uuid) 

352 post_id = Column(String(64), ForeignKey('posts.id'), nullable=False, index=True) 

353 requester_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

354 assignee_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

355 task_description = Column(Text, nullable=False) 

356 status = Column(String(20), default='open') # open|assigned|in_progress|completed|failed 

357 result = Column(Text, nullable=True) 

358 ledger_key = Column(String(200), nullable=True) 

359 created_at = Column(DateTime, default=func.now()) 

360 completed_at = Column(DateTime, nullable=True) 

361 

362 post = relationship('Post') 

363 requester = relationship('User', foreign_keys=[requester_id]) 

364 assignee = relationship('User', foreign_keys=[assignee_id]) 

365 

366 def to_dict(self): 

367 return { 

368 'id': self.id, 'post_id': self.post_id, 

369 'requester_id': self.requester_id, 'assignee_id': self.assignee_id, 

370 'task_description': self.task_description, 'status': self.status, 

371 'result': self.result, 'ledger_key': self.ledger_key, 

372 'created_at': self.created_at.isoformat() if self.created_at else None, 

373 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 

374 } 

375 

376 

377# ─── TABLE 10: notifications ─── 

378 

379class Notification(Base): 

380 __tablename__ = 'notifications' 

381 

382 id = Column(String(64), primary_key=True, default=_uuid) 

383 user_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

384 type = Column(String(30), nullable=False) 

385 source_user_id = Column(String(64), nullable=True) 

386 target_type = Column(String(20), nullable=True) 

387 target_id = Column(String(64), nullable=True) 

388 message = Column(Text, default='') 

389 is_read = Column(Boolean, default=False) 

390 created_at = Column(DateTime, default=func.now()) 

391 

392 user = relationship('User', back_populates='notifications') 

393 

394 def to_dict(self): 

395 return { 

396 'id': self.id, 'user_id': self.user_id, 'type': self.type, 

397 'source_user_id': self.source_user_id, 

398 'target_type': self.target_type, 'target_id': self.target_id, 

399 'message': self.message, 'is_read': self.is_read, 

400 'created_at': self.created_at.isoformat() if self.created_at else None, 

401 } 

402 

403 

404# ─── TABLE 11: reports ─── 

405 

406class Report(Base): 

407 __tablename__ = 'reports' 

408 

409 id = Column(String(64), primary_key=True, default=_uuid) 

410 reporter_id = Column(String(64), ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) 

411 target_type = Column(String(20), nullable=False) 

412 target_id = Column(String(64), nullable=False) 

413 reason = Column(String(50), nullable=False) 

414 details = Column(Text, default='') 

415 status = Column(String(20), default='pending') # pending|reviewed|resolved|dismissed 

416 moderator_id = Column(String(64), ForeignKey('users.id'), nullable=True) 

417 created_at = Column(DateTime, default=func.now()) 

418 

419 reporter = relationship('User', foreign_keys=[reporter_id]) 

420 

421 def to_dict(self): 

422 return { 

423 'id': self.id, 'reporter_id': self.reporter_id, 

424 'target_type': self.target_type, 'target_id': self.target_id, 

425 'reason': self.reason, 'details': self.details, 

426 'status': self.status, 'moderator_id': self.moderator_id, 

427 'created_at': self.created_at.isoformat() if self.created_at else None, 

428 } 

429 

430 

431# ─── TABLE 12: recipe_shares ─── 

432 

433class RecipeShare(Base): 

434 __tablename__ = 'recipe_shares' 

435 

436 id = Column(String(64), primary_key=True, default=_uuid) 

437 post_id = Column(String(64), ForeignKey('posts.id'), nullable=False, index=True) 

438 recipe_file = Column(String(300), nullable=False) 

439 prompt_id = Column(Integer, nullable=False) 

440 flow_id = Column(Integer, nullable=False) 

441 persona = Column(String(200), default='') 

442 action_summary = Column(Text, default='') 

443 fork_count = Column(Integer, default=0) 

444 created_at = Column(DateTime, default=func.now()) 

445 

446 post = relationship('Post') 

447 

448 def to_dict(self): 

449 return { 

450 'id': self.id, 'post_id': self.post_id, 

451 'recipe_file': self.recipe_file, 

452 'prompt_id': self.prompt_id, 'flow_id': self.flow_id, 

453 'persona': self.persona, 'action_summary': self.action_summary, 

454 'fork_count': self.fork_count, 

455 'created_at': self.created_at.isoformat() if self.created_at else None, 

456 } 

457 

458 

459# ─── TABLE 13: peer_nodes ─── 

460 

461class PeerNode(Base): 

462 __tablename__ = 'peer_nodes' 

463 

464 id = Column(String(64), primary_key=True, default=_uuid) 

465 node_id = Column(String(64), unique=True, nullable=False, index=True) 

466 url = Column(String(500), nullable=False) 

467 name = Column(String(100), default='') 

468 version = Column(String(20), default='') 

469 first_seen = Column(DateTime, default=func.now()) 

470 last_seen = Column(DateTime, default=func.now()) 

471 status = Column(String(20), default='active') # active|stale|dead 

472 agent_count = Column(Integer, default=0) 

473 post_count = Column(Integer, default=0) 

474 metadata_json = Column(JSON, default=dict) 

475 contribution_score = Column(Float, default=0.0) 

476 visibility_tier = Column(String(20), default='standard') # standard|featured|priority 

477 node_operator_id = Column(String(64), ForeignKey('users.id'), nullable=True) 

478 # Integrity verification columns 

479 public_key = Column(String(128), nullable=True) 

480 code_hash = Column(String(64), nullable=True) 

481 code_version = Column(String(20), nullable=True) 

482 integrity_status = Column(String(20), default='unverified') # unverified|verified|suspicious|banned 

483 fraud_score = Column(Float, default=0.0) 

484 last_challenge_at = Column(DateTime, nullable=True) 

485 last_attestation_at = Column(DateTime, nullable=True) 

486 master_key_verified = Column(Boolean, default=False) 

487 release_version = Column(String(20), nullable=True) 

488 # Hierarchy columns (v13) 

489 tier = Column(String(20), default='flat') # TOPOLOGY MODE: central|regional|local|flat (NOT capability tier) 

490 parent_node_id = Column(String(64), nullable=True) 

491 certificate_json = Column(JSON, nullable=True) 

492 certificate_verified = Column(Boolean, default=False) 

493 region_assignment_id = Column(String(64), nullable=True) 

494 compute_cpu_cores = Column(Integer, nullable=True) 

495 compute_ram_gb = Column(Float, nullable=True) 

496 compute_gpu_count = Column(Integer, nullable=True) 

497 active_user_count = Column(Integer, default=0) 

498 max_user_capacity = Column(Integer, default=0) 

499 dns_region = Column(String(50), nullable=True) 

500 # HART OS equilibrium: contribution tier + enabled features 

501 capability_tier = Column(String(20), nullable=True) # CAPABILITY TIER: embedded|observer|lite|standard|full|compute_host 

502 enabled_features_json = Column(JSON, nullable=True) # ["agent_engine", "tts", ...] 

503 # E2E encryption: X25519 public key for encrypted inter-node communication 

504 x25519_public = Column(String(64), nullable=True) # Hex-encoded X25519 public key (32 bytes) 

505 # Fail2ban: progressive ban tracking 

506 ban_count = Column(Integer, default=0) # How many times this node has been banned 

507 ban_until = Column(DateTime, nullable=True) # When current ban expires (None = no ban) 

508 # Usage tracking (cumulative, periodically aggregated by aggregate_compute_stats) 

509 gpu_hours_served = Column(Float, default=0.0) 

510 total_inferences = Column(Integer, default=0) 

511 energy_kwh_contributed = Column(Float, default=0.0) 

512 metered_api_costs_absorbed = Column(Float, default=0.0) # USD of metered API used for hive 

513 # Provider identity (gossipped to network — single source of truth) 

514 electricity_rate_kwh = Column(Float, nullable=True) 

515 cause_alignment = Column(String(200), nullable=True) 

516 

517 node_operator = relationship('User', foreign_keys=[node_operator_id]) 

518 

519 def to_dict(self): 

520 return { 

521 'node_id': self.node_id, 'url': self.url, 

522 'name': self.name, 'version': self.version, 

523 'first_seen': self.first_seen.isoformat() if self.first_seen else None, 

524 'last_seen': self.last_seen.isoformat() if self.last_seen else None, 

525 'status': self.status, 

526 'agent_count': self.agent_count, 'post_count': self.post_count, 

527 'contribution_score': self.contribution_score, 

528 'visibility_tier': self.visibility_tier, 

529 'node_operator_id': self.node_operator_id, 

530 'public_key': self.public_key, 

531 'code_hash': self.code_hash, 

532 'code_version': self.code_version, 

533 'integrity_status': self.integrity_status, 

534 'fraud_score': self.fraud_score, 

535 'master_key_verified': self.master_key_verified, 

536 'release_version': self.release_version, 

537 'tier': self.tier, 

538 'parent_node_id': self.parent_node_id, 

539 'certificate_verified': self.certificate_verified, 

540 'region_assignment_id': self.region_assignment_id, 

541 'compute_cpu_cores': self.compute_cpu_cores, 

542 'compute_ram_gb': self.compute_ram_gb, 

543 'compute_gpu_count': self.compute_gpu_count, 

544 'active_user_count': self.active_user_count, 

545 'max_user_capacity': self.max_user_capacity, 

546 'dns_region': self.dns_region, 

547 'capability_tier': self.capability_tier, 

548 'enabled_features': self.enabled_features_json, 

549 'x25519_public': self.x25519_public, 

550 'ban_count': self.ban_count, 

551 'ban_until': self.ban_until.isoformat() if self.ban_until else None, 

552 'gpu_hours_served': self.gpu_hours_served, 

553 'total_inferences': self.total_inferences, 

554 'energy_kwh_contributed': self.energy_kwh_contributed, 

555 'metered_api_costs_absorbed': self.metered_api_costs_absorbed, 

556 'electricity_rate_kwh': self.electricity_rate_kwh, 

557 'cause_alignment': self.cause_alignment, 

558 'metadata': self.metadata_json, 

559 } 

560 

561 

562# ─── TABLE 14: instance_follows ─── 

563 

564class InstanceFollow(Base): 

565 __tablename__ = 'instance_follows' 

566 

567 id = Column(String(64), primary_key=True, default=_uuid) 

568 follower_node_id = Column(String(64), nullable=False, index=True) 

569 following_node_id = Column(String(64), nullable=False, index=True) 

570 peer_url = Column(String(500), nullable=False) 

571 status = Column(String(20), default='active') # active|pending|rejected 

572 created_at = Column(DateTime, default=func.now()) 

573 

574 __table_args__ = ( 

575 UniqueConstraint('follower_node_id', 'following_node_id', 

576 name='uq_instance_follow'), 

577 ) 

578 

579 def to_dict(self): 

580 return { 

581 'id': self.id, 

582 'follower_node_id': self.follower_node_id, 

583 'following_node_id': self.following_node_id, 

584 'peer_url': self.peer_url, 

585 'status': self.status, 

586 'created_at': self.created_at.isoformat() if self.created_at else None, 

587 } 

588 

589 

590# ─── TABLE 15: federated_posts ─── 

591 

592class FederatedPost(Base): 

593 __tablename__ = 'federated_posts' 

594 

595 id = Column(String(64), primary_key=True, default=_uuid) 

596 origin_node_id = Column(String(64), nullable=False, index=True) 

597 origin_node_url = Column(String(500), default='') 

598 origin_node_name = Column(String(100), default='') 

599 origin_post_id = Column(String(64), nullable=False) 

600 origin_author = Column(String(100), default='') 

601 title = Column(String(300), default='') 

602 content = Column(Text, default='') 

603 content_type = Column(String(20), default='text') 

604 media_urls = Column(JSON, default=list) 

605 score = Column(Integer, default=0) 

606 comment_count = Column(Integer, default=0) 

607 original_created_at = Column(String(50), nullable=True) 

608 received_at = Column(DateTime, default=func.now()) 

609 is_boosted = Column(Boolean, default=False) 

610 

611 __table_args__ = ( 

612 UniqueConstraint('origin_node_id', 'origin_post_id', 

613 name='uq_federated_post_origin'), 

614 Index('ix_federated_received', 'received_at'), 

615 ) 

616 

617 def to_dict(self): 

618 return { 

619 'id': self.id, 

620 'origin_node_id': self.origin_node_id, 

621 'origin_node_url': self.origin_node_url, 

622 'origin_node_name': self.origin_node_name, 

623 'origin_post_id': self.origin_post_id, 

624 'origin_author': self.origin_author, 

625 'title': self.title, 

626 'content': self.content, 

627 'content_type': self.content_type, 

628 'media_urls': self.media_urls, 

629 'score': self.score, 

630 'comment_count': self.comment_count, 

631 'original_created_at': self.original_created_at, 

632 'received_at': self.received_at.isoformat() if self.received_at else None, 

633 'is_boosted': self.is_boosted, 

634 'is_federated': True, 

635 } 

636 

637 

638# ═══════════════════════════════════════════════════════════════════════ 

639# RESONANCE & GAMIFICATION TABLES (migrations v3–v8) 

640# ═══════════════════════════════════════════════════════════════════════ 

641 

642# ─── TABLE 16: resonance_wallets ─── 

643 

644class ResonanceWallet(Base): 

645 __tablename__ = 'resonance_wallets' 

646 

647 id = Column(String(64), primary_key=True, default=_uuid) 

648 user_id = Column(String(64), ForeignKey('users.id'), unique=True, nullable=False, index=True) 

649 pulse = Column(Integer, default=0) 

650 spark = Column(Integer, default=0) 

651 spark_lifetime = Column(Integer, default=0) 

652 signal = Column(Float, default=0.0) 

653 signal_last_decay = Column(DateTime, nullable=True) 

654 level = Column(Integer, default=1) 

655 level_title = Column(String(30), default='Newcomer') 

656 xp = Column(Integer, default=0) 

657 xp_next_level = Column(Integer, default=100) 

658 streak_days = Column(Integer, default=0) 

659 streak_best = Column(Integer, default=0) 

660 last_active_date = Column(String(10), nullable=True) # YYYY-MM-DD 

661 season_pulse = Column(Integer, default=0) 

662 season_spark = Column(Integer, default=0) 

663 created_at = Column(DateTime, default=func.now()) 

664 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

665 

666 user = relationship('User', backref='resonance_wallet', uselist=False) 

667 

668 def to_dict(self): 

669 return { 

670 'user_id': self.user_id, 

671 'pulse': self.pulse, 'spark': self.spark, 

672 'spark_lifetime': self.spark_lifetime, 

673 'signal': round(self.signal, 4), 

674 'level': self.level, 'level_title': self.level_title, 

675 'xp': self.xp, 'xp_next_level': self.xp_next_level, 

676 'streak_days': self.streak_days, 'streak_best': self.streak_best, 

677 'last_active_date': self.last_active_date, 

678 'season_pulse': self.season_pulse, 'season_spark': self.season_spark, 

679 } 

680 

681 

682# ─── TABLE 17: resonance_transactions ─── 

683 

684class ResonanceTransaction(Base): 

685 __tablename__ = 'resonance_transactions' 

686 

687 id = Column(String(64), primary_key=True, default=_uuid) 

688 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

689 currency = Column(String(10), nullable=False) # pulse|spark|signal|xp 

690 amount = Column(Float, nullable=False) 

691 balance_after = Column(Float, nullable=False) 

692 source_type = Column(String(30), nullable=False) # upvote|post|comment|task|referral|boost|campaign|streak|decay|spend 

693 source_id = Column(String(64), nullable=True) 

694 description = Column(String(200), default='') 

695 created_at = Column(DateTime, default=func.now(), index=True) 

696 

697 def to_dict(self): 

698 return { 

699 'id': self.id, 'user_id': self.user_id, 

700 'currency': self.currency, 'amount': self.amount, 

701 'balance_after': self.balance_after, 

702 'source_type': self.source_type, 'source_id': self.source_id, 

703 'description': self.description, 

704 'created_at': self.created_at.isoformat() if self.created_at else None, 

705 } 

706 

707 

708# ─── TABLE 18: achievements ─── 

709 

710class Achievement(Base): 

711 __tablename__ = 'achievements' 

712 

713 id = Column(String(64), primary_key=True, default=_uuid) 

714 slug = Column(String(50), unique=True, nullable=False, index=True) 

715 name = Column(String(100), nullable=False) 

716 description = Column(Text, default='') 

717 icon_url = Column(String(500), default='') 

718 category = Column(String(30), nullable=False) # social|agent|community|governance|creation|streak|special 

719 rarity = Column(String(20), default='common') # common|uncommon|rare|legendary 

720 reward_pulse = Column(Integer, default=0) 

721 reward_spark = Column(Integer, default=0) 

722 reward_signal = Column(Float, default=0.0) 

723 reward_xp = Column(Integer, default=0) 

724 criteria_json = Column(JSON, default=dict) 

725 is_seasonal = Column(Boolean, default=False) 

726 season_id = Column(String(64), nullable=True) 

727 created_at = Column(DateTime, default=func.now()) 

728 

729 def to_dict(self): 

730 return { 

731 'id': self.id, 'slug': self.slug, 'name': self.name, 

732 'description': self.description, 'icon_url': self.icon_url, 

733 'category': self.category, 'rarity': self.rarity, 

734 'reward_pulse': self.reward_pulse, 'reward_spark': self.reward_spark, 

735 'reward_signal': self.reward_signal, 'reward_xp': self.reward_xp, 

736 'criteria': self.criteria_json, 

737 'is_seasonal': self.is_seasonal, 'season_id': self.season_id, 

738 } 

739 

740 

741# ─── TABLE 19: user_achievements ─── 

742 

743class UserAchievement(Base): 

744 __tablename__ = 'user_achievements' 

745 

746 id = Column(String(64), primary_key=True, default=_uuid) 

747 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

748 achievement_id = Column(String(64), ForeignKey('achievements.id'), nullable=False) 

749 unlocked_at = Column(DateTime, default=func.now()) 

750 is_showcased = Column(Boolean, default=False) 

751 

752 user = relationship('User', backref='achievements') 

753 achievement = relationship('Achievement') 

754 

755 __table_args__ = ( 

756 UniqueConstraint('user_id', 'achievement_id', name='uq_user_achievement'), 

757 ) 

758 

759 def to_dict(self): 

760 d = { 

761 'id': self.id, 'user_id': self.user_id, 

762 'achievement_id': self.achievement_id, 

763 'unlocked_at': self.unlocked_at.isoformat() if self.unlocked_at else None, 

764 'is_showcased': self.is_showcased, 

765 } 

766 if self.achievement: 

767 d['achievement'] = self.achievement.to_dict() 

768 return d 

769 

770 

771# ─── TABLE 20: seasons ─── 

772 

773class Season(Base): 

774 __tablename__ = 'seasons' 

775 

776 id = Column(String(64), primary_key=True, default=_uuid) 

777 name = Column(String(100), nullable=False) 

778 description = Column(Text, default='') 

779 theme = Column(String(50), default='') 

780 starts_at = Column(DateTime, nullable=False) 

781 ends_at = Column(DateTime, nullable=False) 

782 is_active = Column(Boolean, default=False) 

783 rewards_json = Column(JSON, default=dict) 

784 created_at = Column(DateTime, default=func.now()) 

785 

786 def to_dict(self): 

787 return { 

788 'id': self.id, 'name': self.name, 

789 'description': self.description, 'theme': self.theme, 

790 'starts_at': self.starts_at.isoformat() if self.starts_at else None, 

791 'ends_at': self.ends_at.isoformat() if self.ends_at else None, 

792 'is_active': self.is_active, 'rewards': self.rewards_json, 

793 } 

794 

795 

796# ─── TABLE 21: challenges ─── 

797 

798class Challenge(Base): 

799 __tablename__ = 'challenges' 

800 

801 id = Column(String(64), primary_key=True, default=_uuid) 

802 season_id = Column(String(64), ForeignKey('seasons.id'), nullable=True) 

803 name = Column(String(100), nullable=False) 

804 description = Column(Text, default='') 

805 challenge_type = Column(String(20), nullable=False) # daily|weekly|seasonal|community 

806 criteria_json = Column(JSON, default=dict) 

807 reward_pulse = Column(Integer, default=0) 

808 reward_spark = Column(Integer, default=0) 

809 reward_signal = Column(Float, default=0.0) 

810 reward_xp = Column(Integer, default=0) 

811 max_completions = Column(Integer, default=0) # 0 = unlimited 

812 starts_at = Column(DateTime, nullable=True) 

813 ends_at = Column(DateTime, nullable=True) 

814 created_at = Column(DateTime, default=func.now()) 

815 

816 season = relationship('Season', backref='challenges') 

817 

818 def to_dict(self): 

819 return { 

820 'id': self.id, 'season_id': self.season_id, 

821 'name': self.name, 'description': self.description, 

822 'challenge_type': self.challenge_type, 

823 'criteria': self.criteria_json, 

824 'reward_pulse': self.reward_pulse, 'reward_spark': self.reward_spark, 

825 'reward_signal': self.reward_signal, 'reward_xp': self.reward_xp, 

826 'max_completions': self.max_completions, 

827 'starts_at': self.starts_at.isoformat() if self.starts_at else None, 

828 'ends_at': self.ends_at.isoformat() if self.ends_at else None, 

829 } 

830 

831 

832# ─── TABLE 22: user_challenges ─── 

833 

834class UserChallenge(Base): 

835 __tablename__ = 'user_challenges' 

836 

837 id = Column(String(64), primary_key=True, default=_uuid) 

838 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

839 challenge_id = Column(String(64), ForeignKey('challenges.id'), nullable=False) 

840 progress = Column(Integer, default=0) 

841 target = Column(Integer, default=1) 

842 completed_at = Column(DateTime, nullable=True) 

843 rewarded = Column(Boolean, default=False) 

844 created_at = Column(DateTime, default=func.now()) 

845 

846 user = relationship('User') 

847 challenge = relationship('Challenge') 

848 

849 __table_args__ = ( 

850 UniqueConstraint('user_id', 'challenge_id', name='uq_user_challenge'), 

851 ) 

852 

853 def to_dict(self): 

854 return { 

855 'id': self.id, 'user_id': self.user_id, 

856 'challenge_id': self.challenge_id, 

857 'progress': self.progress, 'target': self.target, 

858 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 

859 'rewarded': self.rewarded, 

860 } 

861 

862 

863# ─── TABLE 23: regions ─── 

864 

865class Region(Base): 

866 __tablename__ = 'regions' 

867 

868 id = Column(String(64), primary_key=True, default=_uuid) 

869 name = Column(String(50), unique=True, nullable=False, index=True) 

870 display_name = Column(String(100), default='') 

871 description = Column(Text, default='') 

872 region_type = Column(String(20), default='thematic') # geographic|thematic|language 

873 parent_region_id = Column(String(64), ForeignKey('regions.id'), nullable=True) 

874 lat = Column(Float, nullable=True) 

875 lon = Column(Float, nullable=True) 

876 radius_km = Column(Float, nullable=True) 

877 global_server_url = Column(String(500), nullable=True) 

878 member_count = Column(Integer, default=0) 

879 settings_json = Column(JSON, default=dict) 

880 created_at = Column(DateTime, default=func.now()) 

881 # Hierarchy columns (v13) 

882 host_node_id = Column(String(64), nullable=True) 

883 capacity_cpu = Column(Integer, nullable=True) 

884 capacity_ram_gb = Column(Float, nullable=True) 

885 capacity_gpu = Column(Integer, nullable=True) 

886 current_load_pct = Column(Float, default=0.0) 

887 is_accepting_nodes = Column(Boolean, default=True) 

888 central_approved = Column(Boolean, default=False) 

889 

890 parent = relationship('Region', remote_side=[id], backref='sub_regions') 

891 

892 def to_dict(self): 

893 return { 

894 'id': self.id, 'name': self.name, 

895 'display_name': _sanitize_html(self.display_name) if self.display_name else self.display_name, 

896 'description': _sanitize_html(self.description) if self.description else self.description, 

897 'region_type': self.region_type, 

898 'parent_region_id': self.parent_region_id, 

899 'lat': self.lat, 'lon': self.lon, 'radius_km': self.radius_km, 

900 'global_server_url': self.global_server_url, 

901 'member_count': self.member_count, 

902 'settings': self.settings_json, 

903 'host_node_id': self.host_node_id, 

904 'capacity_cpu': self.capacity_cpu, 

905 'capacity_ram_gb': self.capacity_ram_gb, 

906 'capacity_gpu': self.capacity_gpu, 

907 'current_load_pct': self.current_load_pct, 

908 'is_accepting_nodes': self.is_accepting_nodes, 

909 'central_approved': self.central_approved, 

910 } 

911 

912 

913# ─── TABLE 24: region_memberships ─── 

914 

915class RegionMembership(Base): 

916 __tablename__ = 'region_memberships' 

917 

918 id = Column(String(64), primary_key=True, default=_uuid) 

919 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

920 region_id = Column(String(64), ForeignKey('regions.id'), nullable=False, index=True) 

921 role = Column(String(20), default='member') # member|contributor|moderator|admin|steward 

922 contribution_score = Column(Float, default=0.0) 

923 promoted_at = Column(DateTime, nullable=True) 

924 created_at = Column(DateTime, default=func.now()) 

925 

926 user = relationship('User') 

927 region = relationship('Region', backref='memberships') 

928 

929 __table_args__ = ( 

930 UniqueConstraint('user_id', 'region_id', name='uq_region_member'), 

931 ) 

932 

933 def to_dict(self): 

934 return { 

935 'id': self.id, 'user_id': self.user_id, 

936 'region_id': self.region_id, 'role': self.role, 

937 'contribution_score': self.contribution_score, 

938 'promoted_at': self.promoted_at.isoformat() if self.promoted_at else None, 

939 'created_at': self.created_at.isoformat() if self.created_at else None, 

940 } 

941 

942 

943# ─── TABLE 25: encounters ─── 

944 

945class Encounter(Base): 

946 __tablename__ = 'encounters' 

947 

948 id = Column(String(64), primary_key=True, default=_uuid) 

949 user_a_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

950 user_b_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

951 context_type = Column(String(20), nullable=False) # community|post|region|challenge|task|ble 

952 context_id = Column(String(64), nullable=True) 

953 location_label = Column(String(200), default='') 

954 encounter_count = Column(Integer, default=1) 

955 first_at = Column(DateTime, default=func.now()) 

956 latest_at = Column(DateTime, default=func.now()) 

957 bond_level = Column(Integer, default=0) # 0–10 

958 is_mutual_aware = Column(Boolean, default=False) 

959 lat = Column(Float, nullable=True) 

960 lng = Column(Float, nullable=True) 

961 payload = Column(JSON, nullable=True) 

962 

963 user_a = relationship('User', foreign_keys=[user_a_id]) 

964 user_b = relationship('User', foreign_keys=[user_b_id]) 

965 

966 __table_args__ = ( 

967 UniqueConstraint('user_a_id', 'user_b_id', 'context_type', 'context_id', 

968 name='uq_encounter_pair_context'), 

969 ) 

970 

971 def to_dict(self): 

972 return { 

973 'id': self.id, 

974 'user_a_id': self.user_a_id, 'user_b_id': self.user_b_id, 

975 'context_type': self.context_type, 'context_id': self.context_id, 

976 'location_label': self.location_label, 

977 'encounter_count': self.encounter_count, 

978 'first_at': self.first_at.isoformat() if self.first_at else None, 

979 'latest_at': self.latest_at.isoformat() if self.latest_at else None, 

980 'bond_level': self.bond_level, 

981 'is_mutual_aware': self.is_mutual_aware, 

982 'lat': self.lat, 

983 'lng': self.lng, 

984 'payload': self.payload, 

985 } 

986 

987 

988# ─── TABLE 25.1: discoverable_prefs ─── 

989# BLE encounter consent + state. Disjoint from `encounters` (which is 

990# durable post-fact aggregated co-presence). See canonical 

991# Hevolve_Database/sql/models.py for full design rationale. 

992 

993class DiscoverablePref(Base): 

994 __tablename__ = 'discoverable_prefs' 

995 

996 user_id = Column(String(64), ForeignKey('users.id'), primary_key=True) 

997 enabled = Column(Boolean, default=False, nullable=False) 

998 enabled_at = Column(DateTime, nullable=True) 

999 expires_at = Column(DateTime, nullable=True, index=True) 

1000 age_claim_18 = Column(Boolean, default=False, nullable=False) 

1001 face_visible = Column(Boolean, default=False, nullable=False) 

1002 avatar_style = Column(String(64), default='studio_ghibli') 

1003 vibe_tags = Column(JSON, default=list) 

1004 toggle_count_24h = Column(Integer, default=0) 

1005 toggle_window_start = Column(DateTime, default=func.now()) 

1006 last_toggle_at = Column(DateTime, nullable=True) 

1007 current_pubkey = Column(String(128), nullable=True, index=True) 

1008 pubkey_registered_at = Column(DateTime, nullable=True) 

1009 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1010 

1011 user = relationship('User', foreign_keys=[user_id]) 

1012 

1013 def to_dict(self): 

1014 return { 

1015 'user_id': self.user_id, 

1016 'enabled': bool(self.enabled), 

1017 'enabled_at': self.enabled_at.isoformat() if self.enabled_at else None, 

1018 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1019 'age_claim_18': bool(self.age_claim_18), 

1020 'face_visible': bool(self.face_visible), 

1021 'avatar_style': self.avatar_style or 'studio_ghibli', 

1022 'vibe_tags': self.vibe_tags or [], 

1023 'toggle_count_24h': self.toggle_count_24h or 0, 

1024 'current_pubkey': self.current_pubkey, 

1025 } 

1026 

1027 

1028# ─── TABLE 25.2: encounter_sightings ─── 

1029# BLE ephemeral pre-match state. Auto-expires 24h post-sighting unless 

1030# a mutual-like upserts an `encounters` row with context_type='ble'. 

1031 

1032class EncounterSighting(Base): 

1033 __tablename__ = 'encounter_sightings' 

1034 

1035 id = Column(String(64), primary_key=True, default=_uuid) 

1036 owner_user_id = Column(String(64), ForeignKey('users.id'), 

1037 nullable=False, index=True) 

1038 peer_user_id = Column(String(64), ForeignKey('users.id'), 

1039 nullable=True, index=True) 

1040 peer_pubkey = Column(String(128), nullable=False) 

1041 rssi_peak = Column(Integer, nullable=True) 

1042 dwell_sec = Column(Integer, nullable=True) 

1043 lat = Column(Float, nullable=True) 

1044 lng = Column(Float, nullable=True) 

1045 sighted_at = Column(DateTime, default=func.now(), nullable=False) 

1046 swipe_decision = Column(String(10), default='pending') # pending|like|dislike 

1047 expires_at = Column(DateTime, nullable=False) 

1048 

1049 owner = relationship('User', foreign_keys=[owner_user_id]) 

1050 peer = relationship('User', foreign_keys=[peer_user_id]) 

1051 

1052 __table_args__ = ( 

1053 Index('ix_encounter_sightings_owner_sighted', 

1054 'owner_user_id', 'sighted_at'), 

1055 Index('ix_encounter_sightings_peer_pubkey', 'peer_pubkey'), 

1056 ) 

1057 

1058 def to_dict(self): 

1059 return { 

1060 'id': self.id, 

1061 'owner_user_id': self.owner_user_id, 

1062 'peer_user_id': self.peer_user_id, 

1063 'peer_pubkey': self.peer_pubkey, 

1064 'rssi_peak': self.rssi_peak, 

1065 'dwell_sec': self.dwell_sec, 

1066 'lat': self.lat, 

1067 'lng': self.lng, 

1068 'sighted_at': self.sighted_at.isoformat() if self.sighted_at else None, 

1069 'swipe_decision': self.swipe_decision or 'pending', 

1070 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1071 } 

1072 

1073 

1074# ─── TABLE 26: ratings ─── 

1075 

1076class Rating(Base): 

1077 __tablename__ = 'ratings' 

1078 

1079 id = Column(String(64), primary_key=True, default=_uuid) 

1080 rater_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1081 rated_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1082 context_type = Column(String(20), nullable=True) # post|task|comment|general 

1083 context_id = Column(String(64), nullable=True) 

1084 dimension = Column(String(20), nullable=False) # skill|usefulness|reliability|creativity 

1085 score = Column(Float, nullable=False) # 1.0–5.0 

1086 comment = Column(Text, default='') 

1087 created_at = Column(DateTime, default=func.now()) 

1088 

1089 rater = relationship('User', foreign_keys=[rater_id]) 

1090 rated = relationship('User', foreign_keys=[rated_id]) 

1091 

1092 __table_args__ = ( 

1093 UniqueConstraint('rater_id', 'rated_id', 'context_type', 'context_id', 'dimension', 

1094 name='uq_rating_unique'), 

1095 ) 

1096 

1097 def to_dict(self): 

1098 return { 

1099 'id': self.id, 'rater_id': self.rater_id, 

1100 'rated_id': self.rated_id, 

1101 'context_type': self.context_type, 'context_id': self.context_id, 

1102 'dimension': self.dimension, 'score': self.score, 

1103 'comment': _sanitize_html(self.comment) if self.comment else self.comment, 

1104 'created_at': self.created_at.isoformat() if self.created_at else None, 

1105 } 

1106 

1107 

1108# ─── TABLE 27: trust_scores ─── 

1109 

1110class TrustScore(Base): 

1111 __tablename__ = 'trust_scores' 

1112 

1113 id = Column(String(64), primary_key=True, default=_uuid) 

1114 user_id = Column(String(64), ForeignKey('users.id'), unique=True, nullable=False, index=True) 

1115 avg_skill = Column(Float, default=0.0) 

1116 avg_usefulness = Column(Float, default=0.0) 

1117 avg_reliability = Column(Float, default=0.0) 

1118 avg_creativity = Column(Float, default=0.0) 

1119 total_ratings_received = Column(Integer, default=0) 

1120 composite_trust = Column(Float, default=0.0) 

1121 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1122 

1123 user = relationship('User', backref='trust_score', uselist=False) 

1124 

1125 def to_dict(self): 

1126 return { 

1127 'user_id': self.user_id, 

1128 'avg_skill': round(self.avg_skill, 2), 

1129 'avg_usefulness': round(self.avg_usefulness, 2), 

1130 'avg_reliability': round(self.avg_reliability, 2), 

1131 'avg_creativity': round(self.avg_creativity, 2), 

1132 'total_ratings_received': self.total_ratings_received, 

1133 'composite_trust': round(self.composite_trust, 2), 

1134 } 

1135 

1136 

1137# ─── TABLE 28: agent_evolution ─── 

1138 

1139class AgentEvolution(Base): 

1140 __tablename__ = 'agent_evolution' 

1141 

1142 id = Column(String(64), primary_key=True, default=_uuid) 

1143 user_id = Column(String(64), ForeignKey('users.id'), unique=True, nullable=False, index=True) 

1144 generation = Column(Integer, default=1) 

1145 specialization_path = Column(String(50), nullable=True) # analyst|creator|executor|communicator 

1146 spec_tier = Column(String(50), nullable=True) # base tier or advanced (e.g., Oracle, Visionary) 

1147 total_tasks = Column(Integer, default=0) 

1148 total_collaborations = Column(Integer, default=0) 

1149 collaboration_bonus = Column(Float, default=1.0) 

1150 evolution_xp = Column(Integer, default=0) 

1151 evolution_xp_next = Column(Integer, default=100) 

1152 traits_json = Column(JSON, default=dict) 

1153 created_at = Column(DateTime, default=func.now()) 

1154 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1155 

1156 user = relationship('User', backref='evolution', uselist=False) 

1157 

1158 def to_dict(self): 

1159 return { 

1160 'user_id': self.user_id, 

1161 'generation': self.generation, 

1162 'specialization_path': self.specialization_path, 

1163 'spec_tier': self.spec_tier, 

1164 'total_tasks': self.total_tasks, 

1165 'total_collaborations': self.total_collaborations, 

1166 'collaboration_bonus': self.collaboration_bonus, 

1167 'evolution_xp': self.evolution_xp, 

1168 'evolution_xp_next': self.evolution_xp_next, 

1169 'traits': self.traits_json, 

1170 } 

1171 

1172 

1173# ─── TABLE 29: agent_collaborations ─── 

1174 

1175class AgentCollaboration(Base): 

1176 __tablename__ = 'agent_collaborations' 

1177 

1178 id = Column(String(64), primary_key=True, default=_uuid) 

1179 agent_a_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1180 agent_b_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1181 task_id = Column(String(64), nullable=True) 

1182 collaboration_type = Column(String(20), nullable=False) # co_task|recipe_chain|mentorship 

1183 quality_score = Column(Float, default=0.0) 

1184 created_at = Column(DateTime, default=func.now()) 

1185 

1186 agent_a = relationship('User', foreign_keys=[agent_a_id]) 

1187 agent_b = relationship('User', foreign_keys=[agent_b_id]) 

1188 

1189 def to_dict(self): 

1190 return { 

1191 'id': self.id, 

1192 'agent_a_id': self.agent_a_id, 'agent_b_id': self.agent_b_id, 

1193 'task_id': self.task_id, 

1194 'collaboration_type': self.collaboration_type, 

1195 'quality_score': self.quality_score, 

1196 'created_at': self.created_at.isoformat() if self.created_at else None, 

1197 } 

1198 

1199 

1200# ─── TABLE 30: referrals ─── 

1201 

1202class Referral(Base): 

1203 __tablename__ = 'referrals' 

1204 

1205 id = Column(String(64), primary_key=True, default=_uuid) 

1206 referrer_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1207 referred_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

1208 referral_code = Column(String(20), nullable=False) 

1209 status = Column(String(20), default='pending') # pending|activated|rewarded 

1210 reward_pulse = Column(Integer, default=0) 

1211 reward_spark = Column(Integer, default=0) 

1212 created_at = Column(DateTime, default=func.now()) 

1213 activated_at = Column(DateTime, nullable=True) 

1214 

1215 referrer = relationship('User', foreign_keys=[referrer_id]) 

1216 referred = relationship('User', foreign_keys=[referred_id]) 

1217 

1218 def to_dict(self): 

1219 return { 

1220 'id': self.id, 'referrer_id': self.referrer_id, 

1221 'referred_id': self.referred_id, 

1222 'referral_code': self.referral_code, 'status': self.status, 

1223 'created_at': self.created_at.isoformat() if self.created_at else None, 

1224 'activated_at': self.activated_at.isoformat() if self.activated_at else None, 

1225 } 

1226 

1227 

1228# ─── TABLE 31: referral_codes ─── 

1229 

1230class ReferralCode(Base): 

1231 __tablename__ = 'referral_codes' 

1232 

1233 id = Column(String(64), primary_key=True, default=_uuid) 

1234 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1235 code = Column(String(20), unique=True, nullable=False, index=True) 

1236 uses = Column(Integer, default=0) 

1237 max_uses = Column(Integer, default=0) # 0 = unlimited 

1238 is_active = Column(Boolean, default=True) 

1239 created_at = Column(DateTime, default=func.now()) 

1240 

1241 user = relationship('User', backref='referral_codes') 

1242 

1243 def to_dict(self): 

1244 return { 

1245 'user_id': self.user_id, 'code': self.code, 

1246 'uses': self.uses, 'max_uses': self.max_uses, 

1247 'is_active': self.is_active, 

1248 } 

1249 

1250 

1251# ─── TABLE 32: boosts ─── 

1252 

1253class Boost(Base): 

1254 __tablename__ = 'boosts' 

1255 

1256 id = Column(String(64), primary_key=True, default=_uuid) 

1257 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1258 target_type = Column(String(20), nullable=False) # post|recipe|agent_profile|campaign 

1259 target_id = Column(String(64), nullable=False) 

1260 spark_spent = Column(Integer, nullable=False) 

1261 boost_multiplier = Column(Float, default=1.0) 

1262 expires_at = Column(DateTime, nullable=False) 

1263 created_at = Column(DateTime, default=func.now()) 

1264 

1265 user = relationship('User') 

1266 

1267 def to_dict(self): 

1268 return { 

1269 'id': self.id, 'user_id': self.user_id, 

1270 'target_type': self.target_type, 'target_id': self.target_id, 

1271 'spark_spent': self.spark_spent, 

1272 'boost_multiplier': self.boost_multiplier, 

1273 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1274 'created_at': self.created_at.isoformat() if self.created_at else None, 

1275 } 

1276 

1277 

1278# ─── TABLE 33: onboarding_progress ─── 

1279 

1280class OnboardingProgress(Base): 

1281 __tablename__ = 'onboarding_progress' 

1282 

1283 id = Column(String(64), primary_key=True, default=_uuid) 

1284 user_id = Column(String(64), ForeignKey('users.id'), unique=True, nullable=False, index=True) 

1285 steps_completed = Column(JSON, default=list) 

1286 current_step = Column(String(30), default='welcome') 

1287 first_post_at = Column(DateTime, nullable=True) 

1288 first_comment_at = Column(DateTime, nullable=True) 

1289 first_vote_at = Column(DateTime, nullable=True) 

1290 first_follow_at = Column(DateTime, nullable=True) 

1291 first_community_join_at = Column(DateTime, nullable=True) 

1292 completed_at = Column(DateTime, nullable=True) 

1293 tutorial_dismissed = Column(Boolean, default=False) 

1294 created_at = Column(DateTime, default=func.now()) 

1295 

1296 user = relationship('User', backref='onboarding', uselist=False) 

1297 

1298 def to_dict(self): 

1299 return { 

1300 'user_id': self.user_id, 

1301 'steps_completed': self.steps_completed or [], 

1302 'current_step': self.current_step, 

1303 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 

1304 'tutorial_dismissed': self.tutorial_dismissed, 

1305 } 

1306 

1307 

1308# ─── TABLE 34: campaigns ─── 

1309 

1310class Campaign(Base): 

1311 __tablename__ = 'campaigns' 

1312 

1313 id = Column(String(64), primary_key=True, default=_uuid) 

1314 owner_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1315 name = Column(String(200), nullable=False) 

1316 description = Column(Text, default='') 

1317 goal = Column(String(20), nullable=False) # awareness|engagement|conversion 

1318 product_url = Column(String(1000), nullable=True) 

1319 product_description = Column(Text, default='') 

1320 agent_id = Column(String(64), ForeignKey('users.id'), nullable=True) 

1321 status = Column(String(20), default='draft') # draft|active|paused|completed 

1322 strategy_json = Column(JSON, default=dict) 

1323 target_regions = Column(JSON, default=list) 

1324 target_communities = Column(JSON, default=list) 

1325 total_spark_budget = Column(Integer, default=0) 

1326 spark_spent = Column(Integer, default=0) 

1327 impressions = Column(Integer, default=0) 

1328 clicks = Column(Integer, default=0) 

1329 conversions = Column(Integer, default=0) 

1330 started_at = Column(DateTime, nullable=True) 

1331 ends_at = Column(DateTime, nullable=True) 

1332 created_at = Column(DateTime, default=func.now()) 

1333 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1334 

1335 owner = relationship('User', foreign_keys=[owner_id], backref='campaigns') 

1336 agent = relationship('User', foreign_keys=[agent_id]) 

1337 

1338 def to_dict(self): 

1339 return { 

1340 'id': self.id, 'owner_id': self.owner_id, 

1341 'name': _sanitize_html(self.name), 

1342 'description': _sanitize_html(self.description) if self.description else '', 

1343 'goal': self.goal, 

1344 'product_url': self.product_url, 

1345 'product_description': self.product_description, 

1346 'agent_id': self.agent_id, 

1347 'status': self.status, 

1348 'strategy': self.strategy_json, 

1349 'target_regions': self.target_regions or [], 

1350 'target_communities': self.target_communities or [], 

1351 'total_spark_budget': self.total_spark_budget, 

1352 'spark_spent': self.spark_spent, 

1353 'impressions': self.impressions, 

1354 'clicks': self.clicks, 'conversions': self.conversions, 

1355 'started_at': self.started_at.isoformat() if self.started_at else None, 

1356 'ends_at': self.ends_at.isoformat() if self.ends_at else None, 

1357 'created_at': self.created_at.isoformat() if self.created_at else None, 

1358 } 

1359 

1360 

1361# ─── TABLE 35: campaign_actions ─── 

1362 

1363class CampaignAction(Base): 

1364 __tablename__ = 'campaign_actions' 

1365 

1366 id = Column(String(64), primary_key=True, default=_uuid) 

1367 campaign_id = Column(String(64), ForeignKey('campaigns.id'), nullable=False, index=True) 

1368 agent_id = Column(String(64), ForeignKey('users.id'), nullable=True) 

1369 action_type = Column(String(20), nullable=False) # post|comment|share|boost 

1370 target_id = Column(String(64), nullable=True) 

1371 content_generated = Column(Text, default='') 

1372 spark_cost = Column(Integer, default=0) 

1373 result_json = Column(JSON, default=dict) 

1374 created_at = Column(DateTime, default=func.now()) 

1375 

1376 campaign = relationship('Campaign', backref='actions') 

1377 agent = relationship('User', foreign_keys=[agent_id]) 

1378 

1379 def to_dict(self): 

1380 return { 

1381 'id': self.id, 'campaign_id': self.campaign_id, 

1382 'agent_id': self.agent_id, 

1383 'action_type': self.action_type, 

1384 'target_id': self.target_id, 

1385 'content_generated': self.content_generated, 

1386 'spark_cost': self.spark_cost, 

1387 'result': self.result_json, 

1388 'created_at': self.created_at.isoformat() if self.created_at else None, 

1389 } 

1390 

1391 

1392# ─── TABLE 36: location_pings ─── 

1393 

1394class LocationPing(Base): 

1395 __tablename__ = 'location_pings' 

1396 

1397 id = Column(String(64), primary_key=True, default=_uuid) 

1398 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1399 lat = Column(Float, nullable=False) 

1400 lon = Column(Float, nullable=False) 

1401 accuracy_m = Column(Float, default=0.0) 

1402 created_at = Column(DateTime, default=func.now()) 

1403 expires_at = Column(DateTime, nullable=False) 

1404 

1405 user = relationship('User', foreign_keys=[user_id]) 

1406 

1407 

1408# ─── TABLE 37: proximity_matches ─── 

1409 

1410class ProximityMatch(Base): 

1411 __tablename__ = 'proximity_matches' 

1412 

1413 id = Column(String(64), primary_key=True, default=_uuid) 

1414 user_a_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1415 user_b_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1416 lat = Column(Float, nullable=False) 

1417 lon = Column(Float, nullable=False) 

1418 location_label = Column(String(200), default='') 

1419 distance_m = Column(Float, default=0.0) 

1420 detected_at = Column(DateTime, default=func.now()) 

1421 status = Column(String(20), default='pending') # pending|revealed_a|revealed_b|matched|expired 

1422 a_revealed_at = Column(DateTime, nullable=True) 

1423 b_revealed_at = Column(DateTime, nullable=True) 

1424 expires_at = Column(DateTime, nullable=False) 

1425 created_at = Column(DateTime, default=func.now()) 

1426 

1427 user_a = relationship('User', foreign_keys=[user_a_id]) 

1428 user_b = relationship('User', foreign_keys=[user_b_id]) 

1429 

1430 def to_dict(self, viewer_id=None): 

1431 d = { 

1432 'id': self.id, 

1433 'status': self.status, 

1434 'distance_bucket': self._distance_bucket(), 

1435 'detected_at': self.detected_at.isoformat() if self.detected_at else None, 

1436 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1437 } 

1438 # Only reveal identities when matched 

1439 if self.status == 'matched': 

1440 d['user_a'] = {'id': self.user_a_id} 

1441 d['user_b'] = {'id': self.user_b_id} 

1442 elif viewer_id: 

1443 is_a = viewer_id == self.user_a_id 

1444 d['you_revealed'] = (is_a and self.a_revealed_at is not None) or \ 

1445 (not is_a and self.b_revealed_at is not None) 

1446 d['other_revealed'] = (not is_a and self.a_revealed_at is not None) or \ 

1447 (is_a and self.b_revealed_at is not None) 

1448 return d 

1449 

1450 def _distance_bucket(self): 

1451 if self.distance_m <= 50: 

1452 return '~50m away' 

1453 elif self.distance_m <= 100: 

1454 return '~100m away' 

1455 elif self.distance_m <= 200: 

1456 return '~200m away' 

1457 else: 

1458 return '~500m away' 

1459 

1460 

1461# ─── TABLE 38: missed_connections ─── 

1462 

1463class MissedConnection(Base): 

1464 __tablename__ = 'missed_connections' 

1465 

1466 id = Column(String(64), primary_key=True, default=_uuid) 

1467 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1468 lat = Column(Float, nullable=False) 

1469 lon = Column(Float, nullable=False) 

1470 location_name = Column(String(200), nullable=False) 

1471 description = Column(Text, default='') 

1472 was_at = Column(DateTime, nullable=False) 

1473 expires_at = Column(DateTime, nullable=False) 

1474 is_active = Column(Boolean, default=True) 

1475 response_count = Column(Integer, default=0) 

1476 created_at = Column(DateTime, default=func.now()) 

1477 

1478 user = relationship('User', foreign_keys=[user_id]) 

1479 

1480 def to_dict(self, viewer_lat=None, viewer_lon=None): 

1481 d = { 

1482 'id': self.id, 

1483 'user_id': self.user_id, 

1484 'location_name': _sanitize_html(self.location_name) if self.location_name else self.location_name, 

1485 'description': _sanitize_html(self.description) if self.description else self.description, 

1486 'was_at': self.was_at.isoformat() if self.was_at else None, 

1487 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1488 'is_active': self.is_active, 

1489 'response_count': self.response_count, 

1490 'created_at': self.created_at.isoformat() if self.created_at else None, 

1491 } 

1492 # Never expose exact lat/lon 

1493 if viewer_lat is not None and viewer_lon is not None: 

1494 from .proximity_service import ProximityService 

1495 dist = ProximityService.haversine_distance(viewer_lat, viewer_lon, self.lat, self.lon) 

1496 if dist <= 100: 

1497 d['distance_label'] = '< 100m' 

1498 elif dist <= 500: 

1499 d['distance_label'] = '< 500m' 

1500 elif dist <= 1000: 

1501 d['distance_label'] = '< 1km' 

1502 else: 

1503 d['distance_label'] = f'~{int(dist / 1000)}km' 

1504 return d 

1505 

1506 

1507# ─── TABLE 39: missed_connection_responses ─── 

1508 

1509class MissedConnectionResponse(Base): 

1510 __tablename__ = 'missed_connection_responses' 

1511 

1512 id = Column(String(64), primary_key=True, default=_uuid) 

1513 missed_connection_id = Column(String(64), ForeignKey('missed_connections.id'), nullable=False, index=True) 

1514 responder_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1515 message = Column(Text, default='') 

1516 status = Column(String(20), default='pending') # pending|accepted|declined 

1517 created_at = Column(DateTime, default=func.now()) 

1518 

1519 missed_connection = relationship('MissedConnection', backref='responses') 

1520 responder = relationship('User', foreign_keys=[responder_id]) 

1521 

1522 def to_dict(self): 

1523 return { 

1524 'id': self.id, 

1525 'missed_connection_id': self.missed_connection_id, 

1526 'responder_id': self.responder_id, 

1527 'message': _sanitize_html(self.message) if self.message else self.message, 

1528 'status': self.status, 

1529 'created_at': self.created_at.isoformat() if self.created_at else None, 

1530 } 

1531 

1532 

1533# ═══════════════════════════════════════════════════════════════════════ 

1534# AD SYSTEM & HOSTING REWARDS TABLES (migration v10) 

1535# ═══════════════════════════════════════════════════════════════════════ 

1536 

1537# ─── TABLE 40: ad_units ─── 

1538 

1539class AdUnit(Base): 

1540 __tablename__ = 'ad_units' 

1541 

1542 id = Column(String(64), primary_key=True, default=_uuid) 

1543 advertiser_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

1544 title = Column(String(200), nullable=False) 

1545 content = Column(Text, default='') 

1546 image_url = Column(String(500), default='') 

1547 click_url = Column(String(1000), nullable=False) 

1548 ad_type = Column(String(20), default='banner') # banner|native|sidebar|interstitial 

1549 targeting_json = Column(JSON, default=dict) # {region_ids:[], community_ids:[], user_types:[]} 

1550 budget_spark = Column(Integer, default=0) 

1551 spent_spark = Column(Integer, default=0) 

1552 cost_per_impression = Column(Float, default=0.1) 

1553 cost_per_click = Column(Float, default=1.0) 

1554 impression_count = Column(Integer, default=0) 

1555 click_count = Column(Integer, default=0) 

1556 status = Column(String(20), default='draft') # draft|active|paused|exhausted|completed 

1557 starts_at = Column(DateTime, nullable=True) 

1558 ends_at = Column(DateTime, nullable=True) 

1559 created_at = Column(DateTime, default=func.now()) 

1560 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1561 

1562 advertiser = relationship('User', foreign_keys=[advertiser_id]) 

1563 

1564 __table_args__ = ( 

1565 Index('ix_ad_units_status_created', 'status', 'created_at'), 

1566 ) 

1567 

1568 def to_dict(self): 

1569 return { 

1570 'id': self.id, 'advertiser_id': self.advertiser_id, 

1571 'title': _sanitize_html(self.title) if self.title else self.title, 

1572 'content': _sanitize_html(self.content) if self.content else self.content, 

1573 'image_url': self.image_url, 'click_url': self.click_url, 

1574 'ad_type': self.ad_type, 

1575 'targeting': self.targeting_json, 

1576 'budget_spark': self.budget_spark, 'spent_spark': self.spent_spark, 

1577 'cost_per_impression': self.cost_per_impression, 

1578 'cost_per_click': self.cost_per_click, 

1579 'impression_count': self.impression_count, 

1580 'click_count': self.click_count, 

1581 'status': self.status, 

1582 'starts_at': self.starts_at.isoformat() if self.starts_at else None, 

1583 'ends_at': self.ends_at.isoformat() if self.ends_at else None, 

1584 'created_at': self.created_at.isoformat() if self.created_at else None, 

1585 } 

1586 

1587 

1588# ─── TABLE 41: ad_placements ─── 

1589 

1590class AdPlacement(Base): 

1591 __tablename__ = 'ad_placements' 

1592 

1593 id = Column(String(64), primary_key=True, default=_uuid) 

1594 name = Column(String(50), unique=True, nullable=False, index=True) 

1595 display_name = Column(String(100), default='') 

1596 description = Column(Text, default='') 

1597 max_ads = Column(Integer, default=1) 

1598 is_active = Column(Boolean, default=True) 

1599 created_at = Column(DateTime, default=func.now()) 

1600 

1601 def to_dict(self): 

1602 return { 

1603 'id': self.id, 'name': self.name, 

1604 'display_name': self.display_name, 

1605 'description': self.description, 

1606 'max_ads': self.max_ads, 

1607 'is_active': self.is_active, 

1608 } 

1609 

1610 

1611# ─── TABLE 42: ad_impressions ─── 

1612 

1613class AdImpression(Base): 

1614 __tablename__ = 'ad_impressions' 

1615 

1616 id = Column(String(64), primary_key=True, default=_uuid) 

1617 ad_id = Column(String(64), ForeignKey('ad_units.id'), nullable=False, index=True) 

1618 placement_id = Column(String(64), ForeignKey('ad_placements.id'), nullable=True) 

1619 node_id = Column(String(64), nullable=True, index=True) 

1620 region_id = Column(String(64), ForeignKey('regions.id', use_alter=True), nullable=True) 

1621 user_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

1622 impression_type = Column(String(10), default='view') # view|click 

1623 ip_hash = Column(String(64), nullable=True) 

1624 created_at = Column(DateTime, default=func.now(), index=True) 

1625 # Impression immutability - seal after witness attestation 

1626 witness_node_id = Column(String(64), nullable=True) 

1627 witness_signature = Column(String(256), nullable=True) 

1628 sealed_hash = Column(String(64), nullable=True) 

1629 sealed_at = Column(DateTime, nullable=True) 

1630 

1631 ad = relationship('AdUnit', backref='impressions') 

1632 

1633 __table_args__ = ( 

1634 Index('ix_ad_impressions_ad_user', 'ad_id', 'user_id', 'created_at'), 

1635 Index('ix_ad_impressions_node', 'node_id', 'created_at'), 

1636 ) 

1637 

1638 @property 

1639 def compute_seal_hash(self) -> str: 

1640 """SHA-256 of canonical impression data for tamper detection.""" 

1641 import hashlib 

1642 import json as _json 

1643 canonical = _json.dumps({ 

1644 'id': self.id, 'ad_id': self.ad_id, 'node_id': self.node_id, 

1645 'user_id': self.user_id, 'impression_type': self.impression_type, 

1646 'created_at': self.created_at.isoformat() if self.created_at else '', 

1647 'witness_node_id': self.witness_node_id or '', 

1648 }, sort_keys=True, separators=(',', ':')) 

1649 return hashlib.sha256(canonical.encode()).hexdigest() 

1650 

1651 def to_dict(self): 

1652 return { 

1653 'id': self.id, 'ad_id': self.ad_id, 

1654 'placement_id': self.placement_id, 

1655 'node_id': self.node_id, 'region_id': self.region_id, 

1656 'user_id': self.user_id, 

1657 'impression_type': self.impression_type, 

1658 'created_at': self.created_at.isoformat() if self.created_at else None, 

1659 'witness_node_id': self.witness_node_id, 

1660 'witness_signature': self.witness_signature, 

1661 'sealed_hash': self.sealed_hash, 

1662 'sealed_at': self.sealed_at.isoformat() if self.sealed_at else None, 

1663 } 

1664 

1665 

1666# ─── TABLE 43: hosting_rewards ─── 

1667 

1668class HostingReward(Base): 

1669 __tablename__ = 'hosting_rewards' 

1670 

1671 id = Column(String(64), primary_key=True, default=_uuid) 

1672 node_id = Column(String(64), nullable=False, index=True) 

1673 operator_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

1674 amount = Column(Float, nullable=False) 

1675 currency = Column(String(10), nullable=False) # spark|pulse 

1676 period = Column(String(20), nullable=False) # daily|weekly|milestone|ad_revenue 

1677 reason = Column(String(200), default='') 

1678 ad_impressions_count = Column(Integer, default=0) 

1679 uptime_ratio = Column(Float, default=0.0) 

1680 contribution_score_snapshot = Column(Float, default=0.0) 

1681 created_at = Column(DateTime, default=func.now(), index=True) 

1682 

1683 operator = relationship('User', foreign_keys=[operator_id]) 

1684 

1685 def to_dict(self): 

1686 return { 

1687 'id': self.id, 'node_id': self.node_id, 

1688 'operator_id': self.operator_id, 

1689 'amount': self.amount, 'currency': self.currency, 

1690 'period': self.period, 'reason': self.reason, 

1691 'ad_impressions_count': self.ad_impressions_count, 

1692 'uptime_ratio': self.uptime_ratio, 

1693 'contribution_score_snapshot': self.contribution_score_snapshot, 

1694 'created_at': self.created_at.isoformat() if self.created_at else None, 

1695 } 

1696 

1697 

1698# ─── TABLE 44: node_attestations ─── 

1699 

1700class NodeAttestation(Base): 

1701 __tablename__ = 'node_attestations' 

1702 

1703 id = Column(String(64), primary_key=True, default=_uuid) 

1704 attester_node_id = Column(String(64), nullable=False, index=True) 

1705 subject_node_id = Column(String(64), nullable=False, index=True) 

1706 attestation_type = Column(String(30), nullable=False) # code_hash_match|impression_witness|stats_verify|challenge_pass|challenge_fail 

1707 payload_json = Column(JSON, default=dict) 

1708 signature = Column(String(256), nullable=False) 

1709 attester_public_key = Column(String(128), nullable=False) 

1710 is_valid = Column(Boolean, default=True) 

1711 created_at = Column(DateTime, default=func.now(), index=True) 

1712 expires_at = Column(DateTime, nullable=True) 

1713 

1714 __table_args__ = ( 

1715 Index('ix_node_attestation_subject_type', 'subject_node_id', 'attestation_type'), 

1716 ) 

1717 

1718 def to_dict(self): 

1719 return { 

1720 'id': self.id, 

1721 'attester_node_id': self.attester_node_id, 

1722 'subject_node_id': self.subject_node_id, 

1723 'attestation_type': self.attestation_type, 

1724 'payload': self.payload_json, 

1725 'signature': self.signature, 

1726 'is_valid': self.is_valid, 

1727 'created_at': self.created_at.isoformat() if self.created_at else None, 

1728 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

1729 } 

1730 

1731 

1732# ─── TABLE 45: integrity_challenges ─── 

1733 

1734class IntegrityChallenge(Base): 

1735 __tablename__ = 'integrity_challenges' 

1736 

1737 id = Column(String(64), primary_key=True, default=_uuid) 

1738 challenger_node_id = Column(String(64), nullable=False, index=True) 

1739 target_node_id = Column(String(64), nullable=False, index=True) 

1740 challenge_type = Column(String(30), nullable=False) # agent_count_verify|stats_probe|code_hash_check|impression_audit 

1741 challenge_nonce = Column(String(64), nullable=False) 

1742 challenge_data = Column(JSON, default=dict) 

1743 response_data = Column(JSON, nullable=True) 

1744 response_signature = Column(String(256), nullable=True) 

1745 status = Column(String(20), default='pending') # pending|responded|passed|failed|timeout 

1746 result_details = Column(Text, default='') 

1747 created_at = Column(DateTime, default=func.now(), index=True) 

1748 responded_at = Column(DateTime, nullable=True) 

1749 evaluated_at = Column(DateTime, nullable=True) 

1750 

1751 __table_args__ = ( 

1752 Index('ix_challenge_target_status', 'target_node_id', 'status'), 

1753 ) 

1754 

1755 def to_dict(self): 

1756 return { 

1757 'id': self.id, 

1758 'challenger_node_id': self.challenger_node_id, 

1759 'target_node_id': self.target_node_id, 

1760 'challenge_type': self.challenge_type, 

1761 'challenge_nonce': self.challenge_nonce, 

1762 'status': self.status, 

1763 'result_details': self.result_details, 

1764 'created_at': self.created_at.isoformat() if self.created_at else None, 

1765 'responded_at': self.responded_at.isoformat() if self.responded_at else None, 

1766 'evaluated_at': self.evaluated_at.isoformat() if self.evaluated_at else None, 

1767 } 

1768 

1769 

1770# ─── TABLE 46: fraud_alerts ─── 

1771 

1772class FraudAlert(Base): 

1773 __tablename__ = 'fraud_alerts' 

1774 

1775 id = Column(String(64), primary_key=True, default=_uuid) 

1776 node_id = Column(String(64), nullable=False, index=True) 

1777 alert_type = Column(String(30), nullable=False) # impression_anomaly|score_jump|hash_mismatch|challenge_fail|witness_refusal|collusion_suspected 

1778 severity = Column(String(10), nullable=False) # low|medium|high|critical 

1779 description = Column(Text, default='') 

1780 evidence_json = Column(JSON, default=dict) 

1781 fraud_score_delta = Column(Float, default=0.0) 

1782 status = Column(String(20), default='open') # open|investigating|confirmed|dismissed 

1783 reviewed_by = Column(String(64), nullable=True) 

1784 reviewed_at = Column(DateTime, nullable=True) 

1785 created_at = Column(DateTime, default=func.now(), index=True) 

1786 

1787 __table_args__ = ( 

1788 Index('ix_fraud_alert_node_status', 'node_id', 'status'), 

1789 Index('ix_fraud_alert_severity', 'severity', 'created_at'), 

1790 ) 

1791 

1792 def to_dict(self): 

1793 return { 

1794 'id': self.id, 'node_id': self.node_id, 

1795 'alert_type': self.alert_type, 'severity': self.severity, 

1796 'description': self.description, 

1797 'evidence': self.evidence_json, 

1798 'fraud_score_delta': self.fraud_score_delta, 

1799 'status': self.status, 

1800 'reviewed_by': self.reviewed_by, 

1801 'reviewed_at': self.reviewed_at.isoformat() if self.reviewed_at else None, 

1802 'created_at': self.created_at.isoformat() if self.created_at else None, 

1803 } 

1804 

1805 

1806# ═══════════════════════════════════════════════════════════════════════ 

1807# HIERARCHY TABLES (migration v13) 

1808# ═══════════════════════════════════════════════════════════════════════ 

1809 

1810# ─── TABLE 47: region_assignments ─── 

1811 

1812class RegionAssignment(Base): 

1813 __tablename__ = 'region_assignments' 

1814 

1815 id = Column(String(64), primary_key=True, default=_uuid) 

1816 local_node_id = Column(String(64), nullable=False, index=True) 

1817 regional_node_id = Column(String(64), nullable=False, index=True) 

1818 region_id = Column(String(64), ForeignKey('regions.id'), nullable=True) 

1819 assigned_by = Column(String(20), default='central_auto') # central_auto|user_choice|admin 

1820 status = Column(String(20), default='pending') # pending|active|migrating|revoked 

1821 assigned_at = Column(DateTime, default=func.now()) 

1822 approved_at = Column(DateTime, nullable=True) 

1823 approved_by_central = Column(Boolean, default=False) 

1824 compute_snapshot = Column(JSON, default=dict) 

1825 metadata_json = Column(JSON, default=dict) 

1826 

1827 region = relationship('Region') 

1828 

1829 __table_args__ = ( 

1830 Index('ix_region_assignment_local', 'local_node_id', 'status'), 

1831 Index('ix_region_assignment_regional', 'regional_node_id', 'status'), 

1832 ) 

1833 

1834 def to_dict(self): 

1835 return { 

1836 'id': self.id, 

1837 'local_node_id': self.local_node_id, 

1838 'regional_node_id': self.regional_node_id, 

1839 'region_id': self.region_id, 

1840 'assigned_by': self.assigned_by, 

1841 'status': self.status, 

1842 'assigned_at': self.assigned_at.isoformat() if self.assigned_at else None, 

1843 'approved_at': self.approved_at.isoformat() if self.approved_at else None, 

1844 'approved_by_central': self.approved_by_central, 

1845 'compute_snapshot': self.compute_snapshot, 

1846 } 

1847 

1848 

1849# ─── TABLE 48: sync_queue ─── 

1850 

1851class SyncQueue(Base): 

1852 __tablename__ = 'sync_queue' 

1853 

1854 id = Column(String(64), primary_key=True, default=_uuid) 

1855 node_id = Column(String(64), nullable=False, index=True) 

1856 target_tier = Column(String(20), nullable=False) # regional|central 

1857 operation_type = Column(String(30), nullable=False) # register_agent|sync_post|register_node|update_stats 

1858 payload_json = Column(JSON, default=dict) 

1859 status = Column(String(20), default='queued') # queued|in_progress|completed|failed 

1860 retry_count = Column(Integer, default=0) 

1861 max_retries = Column(Integer, default=5) 

1862 created_at = Column(DateTime, default=func.now(), index=True) 

1863 last_attempt_at = Column(DateTime, nullable=True) 

1864 completed_at = Column(DateTime, nullable=True) 

1865 error_message = Column(Text, nullable=True) 

1866 

1867 __table_args__ = ( 

1868 Index('ix_sync_queue_status', 'node_id', 'status', 'created_at'), 

1869 ) 

1870 

1871 def to_dict(self): 

1872 return { 

1873 'id': self.id, 

1874 'node_id': self.node_id, 

1875 'target_tier': self.target_tier, 

1876 'operation_type': self.operation_type, 

1877 'status': self.status, 

1878 'retry_count': self.retry_count, 

1879 'max_retries': self.max_retries, 

1880 'created_at': self.created_at.isoformat() if self.created_at else None, 

1881 'last_attempt_at': self.last_attempt_at.isoformat() if self.last_attempt_at else None, 

1882 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 

1883 'error_message': self.error_message, 

1884 } 

1885 

1886 

1887# ─── TABLE 49: coding_goals ─── 

1888 

1889class CodingGoal(Base): 

1890 __tablename__ = 'coding_goals' 

1891 

1892 id = Column(String(64), primary_key=True, default=_uuid) 

1893 title = Column(String(300), nullable=False) 

1894 description = Column(Text, default='') 

1895 repo_url = Column(String(500), nullable=False) 

1896 repo_branch = Column(String(100), default='main') 

1897 target_path = Column(String(500), default='') 

1898 status = Column(String(20), default='active') # active|paused|completed|archived 

1899 priority = Column(Integer, default=0) 

1900 total_tasks = Column(Integer, default=0) 

1901 completed_tasks = Column(Integer, default=0) 

1902 created_by = Column(String(64), nullable=True) 

1903 context_json = Column(JSON, default=dict) 

1904 decomposition_json = Column(JSON, default=dict) 

1905 created_at = Column(DateTime, default=func.now()) 

1906 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

1907 

1908 def to_dict(self): 

1909 return { 

1910 'id': self.id, 

1911 'title': self.title, 

1912 'description': self.description, 

1913 'repo_url': self.repo_url, 

1914 'repo_branch': self.repo_branch, 

1915 'target_path': self.target_path, 

1916 'status': self.status, 

1917 'priority': self.priority, 

1918 'total_tasks': self.total_tasks, 

1919 'completed_tasks': self.completed_tasks, 

1920 'created_by': self.created_by, 

1921 'context_json': self.context_json, 

1922 'decomposition_json': self.decomposition_json, 

1923 'created_at': self.created_at.isoformat() if self.created_at else None, 

1924 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

1925 } 

1926 

1927 

1928# ─── TABLE 50: coding_tasks ─── 

1929 

1930class CodingTask(Base): 

1931 __tablename__ = 'coding_tasks' 

1932 

1933 id = Column(String(64), primary_key=True, default=_uuid) 

1934 goal_id = Column(String(64), ForeignKey('coding_goals.id'), nullable=False, index=True) 

1935 title = Column(String(300), nullable=False) 

1936 description = Column(Text, default='') 

1937 file_path = Column(String(500), nullable=False) 

1938 task_type = Column(String(20), default='implement') # implement|refactor|test|fix|document 

1939 status = Column(String(20), default='pending') # pending|assigned|in_progress|review|merged|failed|blocked 

1940 priority = Column(Integer, default=0) 

1941 assigned_node_id = Column(String(64), nullable=True, index=True) 

1942 assigned_user_id = Column(String(64), nullable=True) 

1943 depends_on_json = Column(JSON, default=list) 

1944 context_files_json = Column(JSON, default=list) 

1945 prompt_text = Column(Text, default='') 

1946 estimated_tokens = Column(Integer, default=0) 

1947 retry_count = Column(Integer, default=0) 

1948 max_retries = Column(Integer, default=3) 

1949 ledger_key = Column(String(200), nullable=True) 

1950 created_at = Column(DateTime, default=func.now(), index=True) 

1951 assigned_at = Column(DateTime, nullable=True) 

1952 completed_at = Column(DateTime, nullable=True) 

1953 

1954 goal = relationship('CodingGoal', backref='tasks') 

1955 

1956 __table_args__ = ( 

1957 Index('ix_coding_task_status_priority', 'status', 'priority'), 

1958 Index('ix_coding_task_goal_status', 'goal_id', 'status'), 

1959 ) 

1960 

1961 def to_dict(self): 

1962 return { 

1963 'id': self.id, 

1964 'goal_id': self.goal_id, 

1965 'title': self.title, 

1966 'description': self.description, 

1967 'file_path': self.file_path, 

1968 'task_type': self.task_type, 

1969 'status': self.status, 

1970 'priority': self.priority, 

1971 'assigned_node_id': self.assigned_node_id, 

1972 'assigned_user_id': self.assigned_user_id, 

1973 'depends_on_json': self.depends_on_json, 

1974 'context_files_json': self.context_files_json, 

1975 'prompt_text': self.prompt_text, 

1976 'estimated_tokens': self.estimated_tokens, 

1977 'retry_count': self.retry_count, 

1978 'max_retries': self.max_retries, 

1979 'ledger_key': self.ledger_key, 

1980 'created_at': self.created_at.isoformat() if self.created_at else None, 

1981 'assigned_at': self.assigned_at.isoformat() if self.assigned_at else None, 

1982 'completed_at': self.completed_at.isoformat() if self.completed_at else None, 

1983 } 

1984 

1985 

1986# ─── TABLE 51: coding_submissions ─── 

1987 

1988class CodingSubmission(Base): 

1989 __tablename__ = 'coding_submissions' 

1990 

1991 id = Column(String(64), primary_key=True, default=_uuid) 

1992 task_id = Column(String(64), ForeignKey('coding_tasks.id'), nullable=False, index=True) 

1993 node_id = Column(String(64), nullable=False, index=True) 

1994 user_id = Column(String(64), nullable=True) 

1995 diff_text = Column(Text, default='') 

1996 file_content = Column(Text, default='') 

1997 branch_name = Column(String(100), default='') 

1998 commit_sha = Column(String(64), nullable=True) 

1999 status = Column(String(20), default='pending_review') # pending_review|approved|rejected|merged|conflict 

2000 review_notes = Column(Text, default='') 

2001 quality_score = Column(Float, default=0.0) 

2002 test_passed = Column(Boolean, default=False) 

2003 lines_added = Column(Integer, default=0) 

2004 lines_removed = Column(Integer, default=0) 

2005 created_at = Column(DateTime, default=func.now(), index=True) 

2006 reviewed_at = Column(DateTime, nullable=True) 

2007 merged_at = Column(DateTime, nullable=True) 

2008 

2009 task = relationship('CodingTask', backref='submissions') 

2010 

2011 __table_args__ = ( 

2012 Index('ix_submission_task_status', 'task_id', 'status'), 

2013 ) 

2014 

2015 def to_dict(self): 

2016 return { 

2017 'id': self.id, 

2018 'task_id': self.task_id, 

2019 'node_id': self.node_id, 

2020 'user_id': self.user_id, 

2021 'diff_text': self.diff_text, 

2022 'branch_name': self.branch_name, 

2023 'commit_sha': self.commit_sha, 

2024 'status': self.status, 

2025 'review_notes': self.review_notes, 

2026 'quality_score': self.quality_score, 

2027 'test_passed': self.test_passed, 

2028 'lines_added': self.lines_added, 

2029 'lines_removed': self.lines_removed, 

2030 'created_at': self.created_at.isoformat() if self.created_at else None, 

2031 'reviewed_at': self.reviewed_at.isoformat() if self.reviewed_at else None, 

2032 'merged_at': self.merged_at.isoformat() if self.merged_at else None, 

2033 } 

2034 

2035 

2036# ─── TABLE 52: products ─── 

2037 

2038class Product(Base): 

2039 """A product that can be marketed by the autonomous marketing agent.""" 

2040 __tablename__ = 'products' 

2041 

2042 id = Column(String(64), primary_key=True, default=_uuid) 

2043 owner_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

2044 name = Column(String(300), nullable=False) 

2045 description = Column(Text, default='') 

2046 tagline = Column(String(500), default='') 

2047 product_url = Column(String(500), default='') 

2048 logo_url = Column(String(500), default='') 

2049 category = Column(String(50), default='general') # saas|ecommerce|content|service|platform|general 

2050 target_audience = Column(Text, default='') 

2051 unique_value_prop = Column(Text, default='') 

2052 keywords_json = Column(JSON, default=list) 

2053 is_platform_product = Column(Boolean, default=False) 

2054 status = Column(String(20), default='active') # active|paused|archived 

2055 created_at = Column(DateTime, default=func.now()) 

2056 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2057 

2058 def to_dict(self): 

2059 return { 

2060 'id': self.id, 

2061 'owner_id': self.owner_id, 

2062 'name': self.name, 

2063 'description': self.description, 

2064 'tagline': self.tagline, 

2065 'product_url': self.product_url, 

2066 'logo_url': self.logo_url, 

2067 'category': self.category, 

2068 'target_audience': self.target_audience, 

2069 'unique_value_prop': self.unique_value_prop, 

2070 'keywords': self.keywords_json or [], 

2071 'is_platform_product': self.is_platform_product, 

2072 'status': self.status, 

2073 'created_at': self.created_at.isoformat() if self.created_at else None, 

2074 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2075 } 

2076 

2077 

2078# ─── TABLE 53: agent_goals ─── 

2079 

2080class AgentGoal(Base): 

2081 """Unified goal for any autonomous agent type (marketing, coding, analytics, etc.). 

2082 

2083 The goal_type field determines which prompt builder and tools are used. 

2084 config_json holds type-specific configuration. Adding a new agent type 

2085 is just a new goal_type value + prompt builder registration. 

2086 """ 

2087 __tablename__ = 'agent_goals' 

2088 

2089 id = Column(String(64), primary_key=True, default=_uuid) 

2090 owner_id = Column(String(64), ForeignKey('users.id'), nullable=True, index=True) 

2091 goal_type = Column(String(50), nullable=False, index=True) # marketing|coding|analytics|support|... 

2092 title = Column(String(500), nullable=False) 

2093 description = Column(Text, default='') 

2094 status = Column(String(20), default='active', index=True) # active|paused|completed|archived 

2095 priority = Column(Integer, default=0) 

2096 

2097 # Type-specific config (repo_url for coding, channels for marketing, etc.) 

2098 config_json = Column(JSON, default=dict) 

2099 

2100 # Marketing-specific (nullable for non-marketing goals) 

2101 product_id = Column(String(64), ForeignKey('products.id'), nullable=True, index=True) 

2102 

2103 # Budget 

2104 spark_budget = Column(Integer, default=200) 

2105 spark_spent = Column(Integer, default=0) 

2106 

2107 # Tracking 

2108 created_by = Column(String(64), nullable=True) 

2109 prompt_id = Column(String(100), nullable=True) # Links to prompts/{prompt_id}.json 

2110 last_dispatched_at = Column(DateTime, nullable=True) 

2111 created_at = Column(DateTime, default=func.now()) 

2112 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2113 

2114 product = relationship('Product', backref='goals') 

2115 

2116 __table_args__ = ( 

2117 Index('ix_agent_goal_type_status', 'goal_type', 'status'), 

2118 ) 

2119 

2120 def to_dict(self): 

2121 config = self.config_json or {} 

2122 result = { 

2123 'id': self.id, 

2124 'owner_id': self.owner_id, 

2125 'goal_type': self.goal_type, 

2126 'title': self.title, 

2127 'description': self.description, 

2128 'status': self.status, 

2129 'priority': self.priority, 

2130 'product_id': self.product_id, 

2131 'spark_budget': self.spark_budget, 

2132 'spark_spent': self.spark_spent, 

2133 'created_by': self.created_by, 

2134 'prompt_id': self.prompt_id, 

2135 'last_dispatched_at': self.last_dispatched_at.isoformat() if self.last_dispatched_at else None, 

2136 'created_at': self.created_at.isoformat() if self.created_at else None, 

2137 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2138 } 

2139 # Merge type-specific config into result 

2140 result.update(config) 

2141 return result 

2142 

2143 

2144# ─── TABLE 54: ip_patents ─── 

2145 

2146class IPPatent(Base): 

2147 """Patent application tracking for autonomous IP protection agent.""" 

2148 __tablename__ = 'ip_patents' 

2149 

2150 id = Column(String(64), primary_key=True, default=_uuid) 

2151 title = Column(String(500), nullable=False) 

2152 status = Column(String(30), default='draft', index=True) # draft|filed|provisional|granted|rejected 

2153 filing_type = Column(String(30), default='provisional') # provisional|utility|pct 

2154 

2155 # Patent content 

2156 claims_json = Column(JSON, default=list) 

2157 abstract = Column(Text, default='') 

2158 description = Column(Text, default='') 

2159 

2160 # Filing details 

2161 filing_date = Column(DateTime, nullable=True) 

2162 application_number = Column(String(50), nullable=True) 

2163 patent_number = Column(String(50), nullable=True) 

2164 

2165 # Verification evidence (loop health snapshot at filing time) 

2166 verification_metrics = Column(JSON, default=dict) 

2167 evidence_json = Column(JSON, default=list) 

2168 

2169 # Tracking 

2170 created_by = Column(String(64), nullable=True) 

2171 goal_id = Column(String(64), nullable=True) 

2172 created_at = Column(DateTime, default=func.now()) 

2173 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2174 

2175 def to_dict(self): 

2176 return { 

2177 'id': self.id, 

2178 'title': self.title, 

2179 'status': self.status, 

2180 'filing_type': self.filing_type, 

2181 'claims': self.claims_json or [], 

2182 'abstract': self.abstract, 

2183 'description': self.description, 

2184 'filing_date': self.filing_date.isoformat() if self.filing_date else None, 

2185 'application_number': self.application_number, 

2186 'patent_number': self.patent_number, 

2187 'verification_metrics': self.verification_metrics or {}, 

2188 'evidence': self.evidence_json or [], 

2189 'created_by': self.created_by, 

2190 'goal_id': self.goal_id, 

2191 'created_at': self.created_at.isoformat() if self.created_at else None, 

2192 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2193 } 

2194 

2195 

2196# ─── TABLE 55: ip_infringements ─── 

2197 

2198class IPInfringement(Base): 

2199 """Tracked infringement cases for IP enforcement agent.""" 

2200 __tablename__ = 'ip_infringements' 

2201 

2202 id = Column(String(64), primary_key=True, default=_uuid) 

2203 patent_id = Column(String(64), ForeignKey('ip_patents.id'), nullable=True) 

2204 

2205 infringer_name = Column(String(300), nullable=False) 

2206 infringer_url = Column(String(1000), nullable=True) 

2207 evidence_summary = Column(Text, default='') 

2208 risk_level = Column(String(20), default='low') # low|medium|high 

2209 status = Column(String(30), default='detected', index=True) # detected|reviewed|notice_sent|resolved|dismissed 

2210 

2211 # Actions taken 

2212 notice_sent_at = Column(DateTime, nullable=True) 

2213 notice_type = Column(String(30), nullable=True) # cease_desist|dmca|licensing_offer 

2214 notice_text = Column(Text, nullable=True) 

2215 

2216 created_at = Column(DateTime, default=func.now()) 

2217 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2218 

2219 patent = relationship('IPPatent', backref='infringements') 

2220 

2221 def to_dict(self): 

2222 return { 

2223 'id': self.id, 

2224 'patent_id': self.patent_id, 

2225 'infringer_name': self.infringer_name, 

2226 'infringer_url': self.infringer_url, 

2227 'evidence_summary': self.evidence_summary, 

2228 'risk_level': self.risk_level, 

2229 'status': self.status, 

2230 'notice_sent_at': self.notice_sent_at.isoformat() if self.notice_sent_at else None, 

2231 'notice_type': self.notice_type, 

2232 'notice_text': self.notice_text, 

2233 'created_at': self.created_at.isoformat() if self.created_at else None, 

2234 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2235 } 

2236 

2237 

2238# ═══════════════════════════════════════════════════════════════ 

2239# TABLE 56 - Defensive Publications (prior art proof, not patents) 

2240# ═══════════════════════════════════════════════════════════════ 

2241 

2242class DefensivePublication(Base): 

2243 """Timestamped proof-of-invention for legal prior art defence.""" 

2244 __tablename__ = 'defensive_publications' 

2245 

2246 id = Column(String(64), primary_key=True, default=_uuid) 

2247 title = Column(String(500), nullable=False) 

2248 abstract = Column(Text, default='') 

2249 content_hash = Column(String(64), nullable=False) # SHA-256 of full content 

2250 git_commit_hash = Column(String(40), nullable=True) 

2251 code_snapshot_hash = Column(String(64), nullable=True) # compute_code_hash() at time 

2252 publication_date = Column(DateTime, default=func.now()) 

2253 signed_by_node_key = Column(String(128), nullable=True) 

2254 signature_hex = Column(String(256), nullable=True) # Ed25519 signature of content_hash 

2255 moat_score_at_publication = Column(Float, default=0.0) 

2256 verification_snapshot = Column(JSON, default=dict) # verify_exponential_improvement() snapshot 

2257 created_by = Column(String(64), nullable=True) 

2258 created_at = Column(DateTime, default=func.now()) 

2259 

2260 def to_dict(self): 

2261 return { 

2262 'id': self.id, 

2263 'title': self.title, 

2264 'abstract': self.abstract, 

2265 'content_hash': self.content_hash, 

2266 'git_commit_hash': self.git_commit_hash, 

2267 'code_snapshot_hash': self.code_snapshot_hash, 

2268 'publication_date': self.publication_date.isoformat() if self.publication_date else None, 

2269 'signed_by_node_key': self.signed_by_node_key, 

2270 'moat_score_at_publication': self.moat_score_at_publication, 

2271 'verification_snapshot': self.verification_snapshot or {}, 

2272 'created_by': self.created_by, 

2273 'created_at': self.created_at.isoformat() if self.created_at else None, 

2274 } 

2275 

2276 

2277# ═══════════════════════════════════════════════════════════════ 

2278# TABLE 57 - Commercial API Keys 

2279# ═══════════════════════════════════════════════════════════════ 

2280 

2281class CommercialAPIKey(Base): 

2282 """API keys for paid intelligence-as-a-service.""" 

2283 __tablename__ = 'api_keys' 

2284 

2285 id = Column(String(64), primary_key=True, default=_uuid) 

2286 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2287 key_hash = Column(String(128), nullable=False, unique=True, index=True) 

2288 key_prefix = Column(String(12), nullable=False) # first 8 chars for display 

2289 name = Column(String(200), default='') 

2290 tier = Column(String(20), default='free', index=True) # free|starter|pro|enterprise 

2291 rate_limit_per_day = Column(Integer, default=100) 

2292 monthly_quota = Column(Integer, default=3000) 

2293 usage_this_month = Column(Integer, default=0) 

2294 usage_reset_at = Column(DateTime, nullable=True) 

2295 is_active = Column(Boolean, default=True) 

2296 created_at = Column(DateTime, default=func.now()) 

2297 expires_at = Column(DateTime, nullable=True) 

2298 

2299 user = relationship('User', backref='api_keys') 

2300 

2301 def to_dict(self): 

2302 return { 

2303 'id': self.id, 

2304 'user_id': self.user_id, 

2305 'key_prefix': self.key_prefix, 

2306 'name': self.name, 

2307 'tier': self.tier, 

2308 'rate_limit_per_day': self.rate_limit_per_day, 

2309 'monthly_quota': self.monthly_quota, 

2310 'usage_this_month': self.usage_this_month, 

2311 'is_active': self.is_active, 

2312 'created_at': self.created_at.isoformat() if self.created_at else None, 

2313 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

2314 } 

2315 

2316 

2317# ═══════════════════════════════════════════════════════════════ 

2318# TABLE 58 - API Usage Log 

2319# ═══════════════════════════════════════════════════════════════ 

2320 

2321class APIUsageLog(Base): 

2322 """Per-request usage logging for billing.""" 

2323 __tablename__ = 'api_usage_log' 

2324 

2325 id = Column(String(64), primary_key=True, default=_uuid) 

2326 api_key_id = Column(String(64), ForeignKey('api_keys.id'), nullable=False, index=True) 

2327 endpoint = Column(String(200), nullable=False) 

2328 tokens_in = Column(Integer, default=0) 

2329 tokens_out = Column(Integer, default=0) 

2330 compute_ms = Column(Integer, default=0) 

2331 cost_credits = Column(Float, default=0.0) 

2332 status_code = Column(Integer, default=200) 

2333 created_at = Column(DateTime, default=func.now()) 

2334 

2335 api_key = relationship('CommercialAPIKey', backref='usage_logs') 

2336 

2337 def to_dict(self): 

2338 return { 

2339 'id': self.id, 

2340 'api_key_id': self.api_key_id, 

2341 'endpoint': self.endpoint, 

2342 'tokens_in': self.tokens_in, 

2343 'tokens_out': self.tokens_out, 

2344 'compute_ms': self.compute_ms, 

2345 'cost_credits': self.cost_credits, 

2346 'status_code': self.status_code, 

2347 'created_at': self.created_at.isoformat() if self.created_at else None, 

2348 } 

2349 

2350 

2351# ═══════════════════════════════════════════════════════════════ 

2352# TABLE 59 - Build Licenses 

2353# ═══════════════════════════════════════════════════════════════ 

2354 

2355class BuildLicense(Base): 

2356 """Licensed Linux build distribution gated by payment.""" 

2357 __tablename__ = 'build_licenses' 

2358 

2359 id = Column(String(64), primary_key=True, default=_uuid) 

2360 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2361 license_key = Column(String(128), nullable=False, unique=True, index=True) 

2362 build_type = Column(String(20), default='community') # community|pro|enterprise 

2363 platform = Column(String(30), default='linux_x64') # linux_x64|linux_arm64 

2364 payment_reference = Column(String(200), nullable=True) 

2365 download_count = Column(Integer, default=0) 

2366 max_downloads = Column(Integer, default=5) 

2367 is_active = Column(Boolean, default=True) 

2368 signed_by = Column(String(128), nullable=True) 

2369 signature_hex = Column(String(256), nullable=True) 

2370 created_at = Column(DateTime, default=func.now()) 

2371 expires_at = Column(DateTime, nullable=True) 

2372 

2373 user = relationship('User', backref='build_licenses') 

2374 

2375 def to_dict(self): 

2376 return { 

2377 'id': self.id, 

2378 'user_id': self.user_id, 

2379 'license_key': self.license_key, 

2380 'build_type': self.build_type, 

2381 'platform': self.platform, 

2382 'payment_reference': self.payment_reference, 

2383 'download_count': self.download_count, 

2384 'max_downloads': self.max_downloads, 

2385 'is_active': self.is_active, 

2386 'created_at': self.created_at.isoformat() if self.created_at else None, 

2387 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

2388 } 

2389 

2390 

2391# ═══════════════════════════════════════════════════════════════ 

2392# TABLE 60 — Guest Recovery 

2393# ═══════════════════════════════════════════════════════════════ 

2394 

2395class GuestRecovery(Base): 

2396 """Recovery codes for guest users to restore identity across devices.""" 

2397 __tablename__ = 'guest_recovery' 

2398 

2399 id = Column(String(64), primary_key=True, default=_uuid) 

2400 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2401 recovery_code_hash = Column(String(255), nullable=False) 

2402 device_id = Column(String(128), nullable=True) 

2403 created_at = Column(DateTime, default=func.now()) 

2404 last_used_at = Column(DateTime, nullable=True) 

2405 

2406 user = relationship('User', backref='guest_recoveries') 

2407 

2408 def to_dict(self): 

2409 return { 

2410 'id': self.id, 

2411 'user_id': self.user_id, 

2412 'device_id': self.device_id, 

2413 'created_at': self.created_at.isoformat() if self.created_at else None, 

2414 'last_used_at': self.last_used_at.isoformat() if self.last_used_at else None, 

2415 } 

2416 

2417 

2418# ═══════════════════════════════════════════════════════════════ 

2419# TABLE 61 — Device Bindings 

2420# ═══════════════════════════════════════════════════════════════ 

2421 

2422class DeviceBinding(Base): 

2423 """Tracks devices linked to a user for sync purposes.""" 

2424 __tablename__ = 'device_bindings' 

2425 

2426 id = Column(String(64), primary_key=True, default=_uuid) 

2427 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2428 device_id = Column(String(128), nullable=False) 

2429 device_name = Column(String(100), default='') 

2430 platform = Column(String(30), default='web') 

2431 form_factor = Column(String(20), default='phone') # phone|watch|tablet|desktop|embedded|tv 

2432 capabilities_json = Column(Text, default='{}') # {"tts":true,"mic":true,"speaker":true,...} 

2433 linked_at = Column(DateTime, default=func.now()) 

2434 last_sync_at = Column(DateTime, nullable=True) 

2435 is_active = Column(Boolean, default=True) 

2436 

2437 user = relationship('User', backref='device_bindings') 

2438 

2439 __table_args__ = ( 

2440 UniqueConstraint('user_id', 'device_id', name='uq_user_device'), 

2441 ) 

2442 

2443 @property 

2444 def capabilities(self): 

2445 import json as _json 

2446 try: 

2447 return _json.loads(self.capabilities_json or '{}') 

2448 except (ValueError, TypeError): 

2449 return {} 

2450 

2451 def to_dict(self): 

2452 return { 

2453 'id': self.id, 

2454 'user_id': self.user_id, 

2455 'device_id': self.device_id, 

2456 'device_name': self.device_name, 

2457 'platform': self.platform, 

2458 'form_factor': self.form_factor, 

2459 'capabilities': self.capabilities, 

2460 'linked_at': self.linked_at.isoformat() if self.linked_at else None, 

2461 'last_sync_at': self.last_sync_at.isoformat() if self.last_sync_at else None, 

2462 'is_active': self.is_active, 

2463 } 

2464 

2465 

2466# ═══════════════════════════════════════════════════════════════ 

2467# TABLE 62 — Backup Metadata 

2468# ═══════════════════════════════════════════════════════════════ 

2469 

2470class BackupMetadata(Base): 

2471 """Metadata for encrypted backups (blob stored on filesystem).""" 

2472 __tablename__ = 'backup_metadata' 

2473 

2474 id = Column(String(64), primary_key=True, default=_uuid) 

2475 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2476 device_id = Column(String(128), nullable=True) 

2477 backup_version = Column(Integer, default=1) 

2478 content_hash = Column(String(64), nullable=False) 

2479 size_bytes = Column(Integer, default=0) 

2480 created_at = Column(DateTime, default=func.now()) 

2481 

2482 user = relationship('User', backref='backups') 

2483 

2484 def to_dict(self): 

2485 return { 

2486 'id': self.id, 

2487 'user_id': self.user_id, 

2488 'device_id': self.device_id, 

2489 'backup_version': self.backup_version, 

2490 'content_hash': self.content_hash, 

2491 'size_bytes': self.size_bytes, 

2492 'created_at': self.created_at.isoformat() if self.created_at else None, 

2493 } 

2494 

2495 

2496# ═══════════════════════════════════════════════════════════════ 

2497# Regional Host Request (v25) 

2498# ═══════════════════════════════════════════════════════════════ 

2499 

2500class RegionalHostRequest(Base): 

2501 """Tracks regional host applications through the hybrid approval flow.""" 

2502 __tablename__ = 'regional_host_requests' 

2503 

2504 id = Column(String(64), primary_key=True, default=_uuid) 

2505 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2506 node_id = Column(String(64), nullable=True) 

2507 public_key_hex = Column(String(128), nullable=True) 

2508 compute_tier = Column(String(20), nullable=True) 

2509 compute_info_json = Column(Text, nullable=True) 

2510 trust_score = Column(Float, default=0.0) 

2511 status = Column(String(20), default='pending', index=True) 

2512 region_name = Column(String(50), nullable=True) 

2513 certificate_json = Column(Text, nullable=True) 

2514 github_username = Column(String(100), nullable=True) 

2515 github_invite_sent = Column(Boolean, default=False) 

2516 requested_at = Column(DateTime, default=func.now()) 

2517 approved_at = Column(DateTime, nullable=True) 

2518 approved_by = Column(String(64), nullable=True) 

2519 rejected_reason = Column(Text, nullable=True) 

2520 

2521 user = relationship('User', backref='regional_host_requests') 

2522 

2523 def to_dict(self): 

2524 import json as _json 

2525 return { 

2526 'id': self.id, 

2527 'user_id': self.user_id, 

2528 'node_id': self.node_id, 

2529 'public_key_hex': self.public_key_hex, 

2530 'compute_tier': self.compute_tier, 

2531 'compute_info': _json.loads(self.compute_info_json) 

2532 if self.compute_info_json else None, 

2533 'trust_score': self.trust_score, 

2534 'status': self.status, 

2535 'region_name': self.region_name, 

2536 'github_username': self.github_username, 

2537 'github_invite_sent': self.github_invite_sent, 

2538 'requested_at': self.requested_at.isoformat() 

2539 if self.requested_at else None, 

2540 'approved_at': self.approved_at.isoformat() 

2541 if self.approved_at else None, 

2542 'approved_by': self.approved_by, 

2543 'rejected_reason': self.rejected_reason, 

2544 } 

2545 

2546 

2547# ═══════════════════════════════════════════════════════════════ 

2548# Fleet Command (v26) — Queen Bee Authority 

2549# ═══════════════════════════════════════════════════════════════ 

2550 

2551class FleetCommand(Base): 

2552 """Commands pushed by central (queen bee) to fleet nodes. 

2553 

2554 Central has instant, total authority. Commands are signed with the 

2555 issuer's certificate and verified by the target before execution. 

2556 """ 

2557 __tablename__ = 'fleet_commands' 

2558 

2559 id = Column(Integer, primary_key=True, autoincrement=True) 

2560 target_node_id = Column(String(64), nullable=False, index=True) 

2561 cmd_type = Column(String(30), nullable=False) 

2562 params_json = Column(Text, nullable=True) 

2563 issued_by = Column(String(64), nullable=False) 

2564 signature = Column(Text, nullable=True) 

2565 status = Column(String(20), default='pending', index=True) 

2566 result_message = Column(Text, nullable=True) 

2567 created_at = Column(Float, default=lambda: __import__('time').time()) 

2568 delivered_at = Column(Float, nullable=True) 

2569 completed_at = Column(Float, nullable=True) 

2570 

2571 def to_dict(self): 

2572 import json as _json 

2573 return { 

2574 'id': self.id, 

2575 'target_node_id': self.target_node_id, 

2576 'cmd_type': self.cmd_type, 

2577 'params': _json.loads(self.params_json) if self.params_json else {}, 

2578 'issued_by': self.issued_by, 

2579 'signature': self.signature, 

2580 'status': self.status, 

2581 'result_message': self.result_message, 

2582 'created_at': self.created_at, 

2583 'delivered_at': self.delivered_at, 

2584 'completed_at': self.completed_at, 

2585 } 

2586 

2587 

2588class ProvisionedNode(Base): 

2589 """Tracks machines where HART OS was remotely provisioned via SSH. 

2590 

2591 Created by NetworkProvisioner when an agent installs HART OS on 

2592 a network machine. Used for fleet management, health monitoring, 

2593 and remote updates. 

2594 """ 

2595 __tablename__ = 'provisioned_nodes' 

2596 

2597 id = Column(Integer, primary_key=True, autoincrement=True) 

2598 target_host = Column(String(256), nullable=False, index=True) 

2599 ssh_user = Column(String(64), default='root') 

2600 node_id = Column(String(64), nullable=True) 

2601 peer_node_id = Column(Integer, nullable=True) 

2602 capability_tier = Column(String(20), nullable=True) 

2603 status = Column(String(20), default='pending', index=True) 

2604 installed_version = Column(String(32), nullable=True) 

2605 last_health_check = Column(DateTime, nullable=True) 

2606 provisioned_at = Column(DateTime, nullable=True) 

2607 provisioned_by = Column(String(64), nullable=False, default='system') 

2608 error_message = Column(Text, nullable=True) 

2609 created_at = Column(DateTime, default=func.now()) 

2610 

2611 

2612# ─── TABLE: thought_experiments (v30) ─── 

2613 

2614class ThoughtExperiment(Base): 

2615 """Constitutional thought experiment — public hypothesis with voting lifecycle. 

2616 

2617 Full lifecycle: PROPOSE → DISCUSS → VOTE → EVALUATE → DECIDE → ARCHIVE 

2618 Both humans and agents vote. ConstitutionalFilter gates all content. 

2619 """ 

2620 __tablename__ = 'thought_experiments' 

2621 

2622 id = Column(String(64), primary_key=True, default=lambda: str(uuid.uuid4())) 

2623 post_id = Column(String(64), ForeignKey('posts.id', use_alter=True), nullable=True) 

2624 creator_id = Column(String(64), ForeignKey('users.id', use_alter=True), nullable=False) 

2625 title = Column(String(200), nullable=False) 

2626 hypothesis = Column(Text, nullable=False) 

2627 expected_outcome = Column(Text, nullable=True) 

2628 intent_category = Column(String(30), default='technology') 

2629 status = Column(String(20), default='proposed', index=True) 

2630 decision_type = Column(String(20), default='weighted') 

2631 decision_context = Column(String(50), nullable=True) 

2632 voting_opens_at = Column(DateTime, nullable=True) 

2633 voting_closes_at = Column(DateTime, nullable=True) 

2634 evaluation_deadline = Column(DateTime, nullable=True) 

2635 decision_outcome = Column(Text, nullable=True) 

2636 decision_rationale = Column(JSON, nullable=True) 

2637 total_votes = Column(Integer, default=0) 

2638 agent_evaluations_json = Column(JSON, nullable=True) 

2639 is_core_ip = Column(Boolean, default=False) 

2640 parent_experiment_id = Column(String(64), nullable=True) 

2641 experiment_type = Column(String(20), default='traditional') # physical_ai | software | traditional 

2642 funding_total = Column(Integer, default=0) # Total Spark invested 

2643 contributor_count = Column(Integer, default=0) # Unique believers 

2644 camera_feed_url = Column(String(500), nullable=True) # WebSocket URL for physical_ai 

2645 created_at = Column(DateTime, default=func.now()) 

2646 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2647 

2648 def to_dict(self): 

2649 return { 

2650 'id': self.id, 

2651 'post_id': self.post_id, 

2652 'creator_id': self.creator_id, 

2653 'title': self.title, 

2654 'hypothesis': self.hypothesis, 

2655 'expected_outcome': self.expected_outcome, 

2656 'intent_category': self.intent_category, 

2657 'status': self.status, 

2658 'decision_type': self.decision_type, 

2659 'decision_context': self.decision_context, 

2660 'voting_opens_at': self.voting_opens_at.isoformat() if self.voting_opens_at else None, 

2661 'voting_closes_at': self.voting_closes_at.isoformat() if self.voting_closes_at else None, 

2662 'evaluation_deadline': self.evaluation_deadline.isoformat() if self.evaluation_deadline else None, 

2663 'decision_outcome': self.decision_outcome, 

2664 'decision_rationale': self.decision_rationale, 

2665 'total_votes': self.total_votes or 0, 

2666 'agent_evaluations_json': self.agent_evaluations_json, 

2667 'is_core_ip': self.is_core_ip or False, 

2668 'parent_experiment_id': self.parent_experiment_id, 

2669 'experiment_type': self.experiment_type or 'traditional', 

2670 'funding_total': self.funding_total or 0, 

2671 'contributor_count': self.contributor_count or 0, 

2672 'camera_feed_url': self.camera_feed_url, 

2673 'created_at': self.created_at.isoformat() if self.created_at else None, 

2674 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2675 } 

2676 

2677 

2678class ExperimentVote(Base): 

2679 """Vote on a thought experiment — from human or agent.""" 

2680 __tablename__ = 'experiment_votes' 

2681 

2682 id = Column(String(64), primary_key=True, default=lambda: str(uuid.uuid4())) 

2683 experiment_id = Column(String(64), ForeignKey('thought_experiments.id', use_alter=True), 

2684 nullable=False, index=True) 

2685 voter_id = Column(String(64), ForeignKey('users.id', use_alter=True), nullable=False) 

2686 voter_type = Column(String(10), default='human') 

2687 vote_value = Column(Integer, default=0) 

2688 confidence = Column(Float, default=1.0) 

2689 reasoning = Column(Text, nullable=True) 

2690 suggestion = Column(Text, nullable=True) 

2691 constitutional_check = Column(Boolean, default=True) 

2692 created_at = Column(DateTime, default=func.now()) 

2693 

2694 __table_args__ = ( 

2695 UniqueConstraint('experiment_id', 'voter_id', 

2696 name='uq_experiment_voter'), 

2697 ) 

2698 

2699 def to_dict(self): 

2700 return { 

2701 'id': self.id, 

2702 'experiment_id': self.experiment_id, 

2703 'voter_id': self.voter_id, 

2704 'voter_type': self.voter_type, 

2705 'vote_value': self.vote_value, 

2706 'confidence': self.confidence, 

2707 'reasoning': self.reasoning, 

2708 'suggestion': self.suggestion, 

2709 'constitutional_check': self.constitutional_check, 

2710 'created_at': self.created_at.isoformat() if self.created_at else None, 

2711 } 

2712 

2713 

2714class PaperPortfolio(Base): 

2715 """Simulated trading portfolio for paper trading agents.""" 

2716 __tablename__ = 'paper_portfolios' 

2717 

2718 id = Column(String(64), primary_key=True, default=lambda: str(uuid.uuid4())) 

2719 user_id = Column(String(64), nullable=False, index=True) 

2720 goal_id = Column(String(64), nullable=True) 

2721 strategy = Column(String(30), default='long_term') 

2722 initial_balance = Column(Float, default=10000.0) 

2723 current_balance = Column(Float, default=10000.0) 

2724 total_pnl = Column(Float, default=0.0) 

2725 total_trades = Column(Integer, default=0) 

2726 winning_trades = Column(Integer, default=0) 

2727 status = Column(String(20), default='active') 

2728 created_at = Column(DateTime, default=func.now()) 

2729 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2730 

2731 def to_dict(self): 

2732 return { 

2733 'id': self.id, 

2734 'user_id': self.user_id, 

2735 'goal_id': self.goal_id, 

2736 'strategy': self.strategy, 

2737 'initial_balance': self.initial_balance, 

2738 'current_balance': self.current_balance, 

2739 'total_pnl': self.total_pnl, 

2740 'total_trades': self.total_trades, 

2741 'winning_trades': self.winning_trades, 

2742 'win_rate': round(self.winning_trades / self.total_trades, 4) if self.total_trades else 0.0, 

2743 'status': self.status, 

2744 'created_at': self.created_at.isoformat() if self.created_at else None, 

2745 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2746 } 

2747 

2748 

2749class PaperTrade(Base): 

2750 """Individual paper trade record.""" 

2751 __tablename__ = 'paper_trades' 

2752 

2753 id = Column(String(64), primary_key=True, default=lambda: str(uuid.uuid4())) 

2754 portfolio_id = Column(String(64), ForeignKey('paper_portfolios.id', use_alter=True), 

2755 nullable=False, index=True) 

2756 symbol = Column(String(20), nullable=False) 

2757 side = Column(String(10), nullable=False) 

2758 quantity = Column(Float, nullable=False) 

2759 entry_price = Column(Float, nullable=False) 

2760 exit_price = Column(Float, nullable=True) 

2761 stop_loss = Column(Float, nullable=True) 

2762 pnl = Column(Float, default=0.0) 

2763 status = Column(String(20), default='open') 

2764 opened_at = Column(DateTime, default=func.now()) 

2765 closed_at = Column(DateTime, nullable=True) 

2766 

2767 def to_dict(self): 

2768 return { 

2769 'id': self.id, 

2770 'portfolio_id': self.portfolio_id, 

2771 'symbol': self.symbol, 

2772 'side': self.side, 

2773 'quantity': self.quantity, 

2774 'entry_price': self.entry_price, 

2775 'exit_price': self.exit_price, 

2776 'stop_loss': self.stop_loss, 

2777 'pnl': self.pnl, 

2778 'status': self.status, 

2779 'opened_at': self.opened_at.isoformat() if self.opened_at else None, 

2780 'closed_at': self.closed_at.isoformat() if self.closed_at else None, 

2781 } 

2782 

2783 

2784class ComputeEscrow(Base): 

2785 """Persistent compute lending escrow — replaces in-memory _compute_debts. 

2786 

2787 When experiment_post_id is set, this escrow is a pledge toward a specific 

2788 thought experiment. pledge_type distinguishes gpu_hours / cloud_credits / 

2789 money pledges from the legacy spark-only escrow rows (where pledge_type is 

2790 NULL). consumed tracks how much of the pledged amount has been used. 

2791 """ 

2792 __tablename__ = 'compute_escrow' 

2793 

2794 id = Column(Integer, primary_key=True) 

2795 debtor_node_id = Column(String(100), nullable=False, index=True) 

2796 creditor_node_id = Column(String(100), nullable=False, index=True) 

2797 request_id = Column(String(100), nullable=True) 

2798 task_type = Column(String(50), default='general') 

2799 spark_amount = Column(Integer, nullable=False) 

2800 status = Column(String(20), default='pending', index=True) # pending|settled|expired 

2801 created_at = Column(DateTime, default=datetime.utcnow) 

2802 settled_at = Column(DateTime, nullable=True) 

2803 expires_at = Column(DateTime, nullable=True) 

2804 # v34 — thought-experiment pledge extensions 

2805 experiment_post_id = Column(String(64), nullable=True, index=True) 

2806 pledge_type = Column(String(20), nullable=True) # gpu_hours | cloud_credits | money (NULL = legacy spark) 

2807 consumed = Column(Float, default=0.0) 

2808 pledge_message = Column(Text, nullable=True) 

2809 

2810 def to_dict(self): 

2811 return { 

2812 'id': self.id, 

2813 'debtor_node_id': self.debtor_node_id, 

2814 'creditor_node_id': self.creditor_node_id, 

2815 'request_id': self.request_id, 

2816 'task_type': self.task_type, 

2817 'spark_amount': self.spark_amount, 

2818 'status': self.status, 

2819 'created_at': self.created_at.isoformat() if self.created_at else None, 

2820 'settled_at': self.settled_at.isoformat() if self.settled_at else None, 

2821 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

2822 'experiment_post_id': self.experiment_post_id, 

2823 'pledge_type': self.pledge_type, 

2824 'consumed': self.consumed, 

2825 'pledge_message': self.pledge_message, 

2826 } 

2827 

2828 

2829class MeteredAPIUsage(Base): 

2830 """Per-call record of metered API consumption for cost recovery. 

2831 

2832 Tracks when hive/idle tasks consume a node operator's paid API credits 

2833 (GPT-4, Claude, Groq paid tier). Distinct from APIUsageLog which tracks 

2834 external commercial billing (customers paying us). 

2835 """ 

2836 __tablename__ = 'metered_api_usage' 

2837 

2838 id = Column(String(64), primary_key=True, default=_uuid) 

2839 node_id = Column(String(64), nullable=False, index=True) 

2840 operator_id = Column(String(64), nullable=True, index=True) 

2841 model_id = Column(String(100), nullable=False) 

2842 task_source = Column(String(30), nullable=False) # own | hive | idle 

2843 goal_id = Column(String(64), nullable=True, index=True) 

2844 requester_node_id = Column(String(64), nullable=True) 

2845 tokens_in = Column(Integer, default=0) 

2846 tokens_out = Column(Integer, default=0) 

2847 cost_per_1k_tokens = Column(Float, default=0.0) 

2848 estimated_spark_cost = Column(Integer, default=0) 

2849 actual_usd_cost = Column(Float, default=0.0) 

2850 settlement_status = Column(String(20), default='pending', index=True) 

2851 created_at = Column(DateTime, default=func.now(), index=True) 

2852 # v34 — thought-experiment consumption tracking 

2853 escrow_id = Column(Integer, nullable=True, index=True) 

2854 experiment_post_id = Column(String(64), nullable=True, index=True) 

2855 

2856 def to_dict(self): 

2857 return { 

2858 'id': self.id, 

2859 'node_id': self.node_id, 

2860 'operator_id': self.operator_id, 

2861 'model_id': self.model_id, 

2862 'task_source': self.task_source, 

2863 'goal_id': self.goal_id, 

2864 'requester_node_id': self.requester_node_id, 

2865 'tokens_in': self.tokens_in, 

2866 'tokens_out': self.tokens_out, 

2867 'cost_per_1k_tokens': self.cost_per_1k_tokens, 

2868 'estimated_spark_cost': self.estimated_spark_cost, 

2869 'actual_usd_cost': self.actual_usd_cost, 

2870 'settlement_status': self.settlement_status, 

2871 'created_at': self.created_at.isoformat() if self.created_at else None, 

2872 'escrow_id': self.escrow_id, 

2873 'experiment_post_id': self.experiment_post_id, 

2874 } 

2875 

2876 

2877class NodeComputeConfig(Base): 

2878 """Per-node LOCAL policy settings (not gossipped). 

2879 

2880 Controls how this node behaves: model routing, metered API opt-in, 

2881 feature flags, settlement. Provider identity fields (cause_alignment, 

2882 electricity_rate_kwh) live on PeerNode only — single source of truth. 

2883 """ 

2884 __tablename__ = 'node_compute_config' 

2885 

2886 id = Column(String(64), primary_key=True, default=_uuid) 

2887 node_id = Column(String(64), unique=True, nullable=False, index=True) 

2888 # Model routing (local policy) 

2889 compute_policy = Column(String(20), default='local_preferred') 

2890 hive_compute_policy = Column(String(20), default='local_preferred') 

2891 max_hive_gpu_pct = Column(Integer, default=50) 

2892 # Metered API opt-in (local policy) 

2893 allow_metered_for_hive = Column(Boolean, default=False) 

2894 metered_daily_limit_usd = Column(Float, default=0.0) 

2895 # Compute offer (local declaration) 

2896 offered_gpu_hours_per_day = Column(Float, default=0.0) 

2897 # Feature flags (local policy) 

2898 accept_thought_experiments = Column(Boolean, default=True) 

2899 accept_frontier_training = Column(Boolean, default=False) 

2900 # Settlement (local policy) 

2901 auto_settle = Column(Boolean, default=True) 

2902 min_settlement_spark = Column(Integer, default=10) 

2903 created_at = Column(DateTime, default=func.now()) 

2904 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

2905 

2906 def to_dict(self): 

2907 return { 

2908 'id': self.id, 

2909 'node_id': self.node_id, 

2910 'compute_policy': self.compute_policy, 

2911 'hive_compute_policy': self.hive_compute_policy, 

2912 'max_hive_gpu_pct': self.max_hive_gpu_pct, 

2913 'allow_metered_for_hive': self.allow_metered_for_hive, 

2914 'metered_daily_limit_usd': self.metered_daily_limit_usd, 

2915 'offered_gpu_hours_per_day': self.offered_gpu_hours_per_day, 

2916 'accept_thought_experiments': self.accept_thought_experiments, 

2917 'accept_frontier_training': self.accept_frontier_training, 

2918 'auto_settle': self.auto_settle, 

2919 'min_settlement_spark': self.min_settlement_spark, 

2920 'created_at': self.created_at.isoformat() if self.created_at else None, 

2921 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

2922 } 

2923 

2924 

2925class AuditLogEntry(Base): 

2926 """Immutable audit log with hash-chain integrity (see security/immutable_audit_log.py).""" 

2927 __tablename__ = 'audit_log_entries' 

2928 

2929 id = Column(Integer, primary_key=True, autoincrement=True) 

2930 event_type = Column(String(50), nullable=False, index=True) 

2931 actor_id = Column(String(100), nullable=False, index=True) 

2932 target_id = Column(String(100), nullable=True) 

2933 action = Column(Text, nullable=False) 

2934 detail_json = Column(Text, nullable=True) 

2935 prev_hash = Column(String(64), nullable=False) 

2936 entry_hash = Column(String(64), nullable=False, unique=True) 

2937 created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 

2938 

2939 

2940# ─── Multiplayer Games ─── 

2941 

2942class GameSession(Base): 

2943 """Multiplayer game session (trivia, word chain, collab puzzle, compute challenge).""" 

2944 __tablename__ = 'game_sessions' 

2945 

2946 id = Column(String(64), primary_key=True, default=_uuid) 

2947 game_type = Column(String(30), nullable=False, index=True) # trivia|word_chain|collab_puzzle|compute_challenge|quick_match 

2948 status = Column(String(20), default='waiting', index=True) # waiting|active|completed|expired|cancelled 

2949 host_user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2950 encounter_id = Column(String(64), nullable=True, index=True) # if born from encounter 

2951 community_id = Column(String(64), nullable=True) # scoped to community 

2952 challenge_id = Column(String(64), nullable=True) # linked challenge 

2953 max_players = Column(Integer, default=4) 

2954 current_round = Column(Integer, default=0) 

2955 total_rounds = Column(Integer, default=5) 

2956 game_state = Column(JSON, default=dict) # game-type-specific state 

2957 config = Column(JSON, default=dict) # difficulty, categories, etc. 

2958 started_at = Column(DateTime, nullable=True) 

2959 ended_at = Column(DateTime, nullable=True) 

2960 expires_at = Column(DateTime, nullable=False) # auto-cleanup 

2961 created_at = Column(DateTime, default=datetime.utcnow) 

2962 

2963 participants = relationship('GameParticipant', back_populates='session', 

2964 cascade='all, delete-orphan', lazy='joined') 

2965 

2966 def to_dict(self): 

2967 return { 

2968 'id': self.id, 

2969 'game_type': self.game_type, 

2970 'status': self.status, 

2971 'host_user_id': self.host_user_id, 

2972 'encounter_id': self.encounter_id, 

2973 'community_id': self.community_id, 

2974 'challenge_id': self.challenge_id, 

2975 'max_players': self.max_players, 

2976 'current_round': self.current_round, 

2977 'total_rounds': self.total_rounds, 

2978 'game_state': self.game_state, 

2979 'config': self.config, 

2980 'started_at': self.started_at.isoformat() if self.started_at else None, 

2981 'ended_at': self.ended_at.isoformat() if self.ended_at else None, 

2982 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 

2983 'created_at': self.created_at.isoformat() if self.created_at else None, 

2984 'participants': [p.to_dict() for p in (self.participants or [])], 

2985 'player_count': len(self.participants or []), 

2986 } 

2987 

2988 

2989class GameParticipant(Base): 

2990 """Player in a game session, tracks score and result.""" 

2991 __tablename__ = 'game_participants' 

2992 

2993 id = Column(String(64), primary_key=True, default=_uuid) 

2994 game_session_id = Column(String(64), ForeignKey('game_sessions.id'), nullable=False, index=True) 

2995 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

2996 score = Column(Integer, default=0) 

2997 is_ready = Column(Boolean, default=False) 

2998 joined_at = Column(DateTime, default=datetime.utcnow) 

2999 finished_at = Column(DateTime, nullable=True) 

3000 result = Column(String(20), nullable=True) # win|loss|draw|abandoned 

3001 spark_earned = Column(Integer, default=0) 

3002 xp_earned = Column(Integer, default=0) 

3003 

3004 session = relationship('GameSession', back_populates='participants') 

3005 

3006 __table_args__ = ( 

3007 UniqueConstraint('game_session_id', 'user_id', name='uq_game_participant'), 

3008 ) 

3009 

3010 def to_dict(self): 

3011 return { 

3012 'id': self.id, 

3013 'game_session_id': self.game_session_id, 

3014 'user_id': self.user_id, 

3015 'score': self.score, 

3016 'is_ready': self.is_ready, 

3017 'joined_at': self.joined_at.isoformat() if self.joined_at else None, 

3018 'finished_at': self.finished_at.isoformat() if self.finished_at else None, 

3019 'result': self.result, 

3020 'spark_earned': self.spark_earned, 

3021 'xp_earned': self.xp_earned, 

3022 } 

3023 

3024 

3025# ─── TABLE: shareable_links ─── 

3026 

3027class ShareableLink(Base): 

3028 """Universal share token for any resource — posts, profiles, recipes, agents, etc.""" 

3029 __tablename__ = 'shareable_links' 

3030 

3031 id = Column(String(64), primary_key=True, default=_uuid) 

3032 token = Column(String(12), unique=True, nullable=False, index=True) 

3033 resource_type = Column(String(30), nullable=False) 

3034 resource_id = Column(String(64), nullable=False) 

3035 created_by = Column(String(64), ForeignKey('users.id'), nullable=True) 

3036 referral_code = Column(String(20), nullable=True) 

3037 is_private = Column(Boolean, default=False) 

3038 consent_token = Column(String(32), nullable=True) 

3039 view_count = Column(Integer, default=0) 

3040 share_count = Column(Integer, default=0) 

3041 created_at = Column(DateTime, default=func.now()) 

3042 expires_at = Column(DateTime, nullable=True) 

3043 metadata_json = Column(Text, nullable=True) 

3044 

3045 creator = relationship('User', foreign_keys=[created_by]) 

3046 

3047 __table_args__ = ( 

3048 Index('ix_share_resource', 'resource_type', 'resource_id', 'created_by'), 

3049 ) 

3050 

3051 def to_dict(self): 

3052 import json as _json 

3053 og = {} 

3054 if self.metadata_json: 

3055 try: 

3056 og = _json.loads(self.metadata_json) 

3057 except Exception: 

3058 pass 

3059 return { 

3060 'id': self.id, 

3061 'token': self.token, 

3062 'resource_type': self.resource_type, 

3063 'resource_id': self.resource_id, 

3064 'referral_code': self.referral_code, 

3065 'is_private': self.is_private, 

3066 'view_count': self.view_count, 

3067 'share_count': self.share_count, 

3068 'created_at': self.created_at.isoformat() if self.created_at else None, 

3069 'og': og, 

3070 } 

3071 

3072 

3073# ─── TABLE: share_events ─── 

3074 

3075class ShareEvent(Base): 

3076 """Track share views, clicks, and consent grants.""" 

3077 __tablename__ = 'share_events' 

3078 

3079 id = Column(String(64), primary_key=True, default=_uuid) 

3080 link_id = Column(String(64), ForeignKey('shareable_links.id'), nullable=False, index=True) 

3081 event_type = Column(String(20), nullable=False) # view|share|consent 

3082 viewer_id = Column(String(64), ForeignKey('users.id'), nullable=True) 

3083 ip_hash = Column(String(64), nullable=True) 

3084 created_at = Column(DateTime, default=func.now()) 

3085 

3086 link = relationship('ShareableLink') 

3087 

3088 def to_dict(self): 

3089 return { 

3090 'id': self.id, 

3091 'link_id': self.link_id, 

3092 'event_type': self.event_type, 

3093 'created_at': self.created_at.isoformat() if self.created_at else None, 

3094 } 

3095 

3096 

3097# ─── TABLE: user_consents ─── 

3098 

3099class UserConsent(Base): 

3100 """Track explicit user consent for data access, revenue sharing, and public exposure.""" 

3101 __tablename__ = 'user_consents' 

3102 

3103 id = Column(String(64), primary_key=True) 

3104 user_id = Column(String(64), nullable=False, index=True) 

3105 agent_id = Column(String(64), nullable=True, index=True) 

3106 consent_type = Column(String(30), nullable=False, index=True) # data_access|revenue_share|public_exposure 

3107 scope = Column(String(100), nullable=False, default='*') 

3108 granted = Column(Boolean, default=False, nullable=False) 

3109 granted_at = Column(DateTime, nullable=True) 

3110 revoked_at = Column(DateTime, nullable=True) 

3111 created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 

3112 updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 

3113 

3114 __table_args__ = ( 

3115 UniqueConstraint('user_id', 'agent_id', 'consent_type', 'scope', 

3116 name='uq_user_consent'), 

3117 Index('ix_user_consent_lookup', 'user_id', 'consent_type', 'scope', 'granted'), 

3118 ) 

3119 

3120 def to_dict(self): 

3121 return { 

3122 'id': self.id, 

3123 'user_id': self.user_id, 

3124 'agent_id': self.agent_id, 

3125 'consent_type': self.consent_type, 

3126 'scope': self.scope, 

3127 'granted': self.granted, 

3128 'granted_at': self.granted_at.isoformat() if self.granted_at else None, 

3129 'revoked_at': self.revoked_at.isoformat() if self.revoked_at else None, 

3130 'created_at': self.created_at.isoformat() if self.created_at else None, 

3131 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3132 } 

3133 

3134 

3135# ─── TABLE: marketplace_listings ─── 

3136 

3137class MarketplaceListing(Base): 

3138 """HART agent service listing in the marketplace.""" 

3139 __tablename__ = 'marketplace_listings' 

3140 

3141 id = Column(String(64), primary_key=True, default=_uuid) 

3142 agent_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3143 title = Column(String(200), nullable=False) 

3144 description = Column(Text, default='') 

3145 category = Column(String(50), nullable=False, default='custom') 

3146 price_spark = Column(Integer, default=0) 

3147 rating_avg = Column(Float, default=0.0) 

3148 review_count = Column(Integer, default=0) 

3149 hire_count = Column(Integer, default=0) 

3150 is_active = Column(Boolean, default=True) 

3151 created_at = Column(DateTime, default=func.now()) 

3152 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

3153 

3154 agent = relationship('User', backref='marketplace_listings') 

3155 

3156 def to_dict(self): 

3157 agent_data = None 

3158 if self.agent: 

3159 agent_data = { 

3160 'id': self.agent.id, 

3161 'username': self.agent.username, 

3162 'display_name': self.agent.display_name, 

3163 'avatar_url': self.agent.avatar_url, 

3164 'user_type': self.agent.user_type, 

3165 } 

3166 return { 

3167 'id': self.id, 

3168 'agent_id': self.agent_id, 

3169 'title': self.title, 

3170 'description': self.description, 

3171 'category': self.category, 

3172 'price_spark': self.price_spark, 

3173 'rating_avg': self.rating_avg, 

3174 'review_count': self.review_count, 

3175 'hire_count': self.hire_count, 

3176 'is_active': self.is_active, 

3177 'agent': agent_data, 

3178 'created_at': self.created_at.isoformat() if self.created_at else None, 

3179 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3180 } 

3181 

3182 

3183class ListingReview(Base): 

3184 """Review for a marketplace listing.""" 

3185 __tablename__ = 'listing_reviews' 

3186 

3187 id = Column(String(64), primary_key=True, default=_uuid) 

3188 listing_id = Column(String(64), ForeignKey('marketplace_listings.id'), nullable=False, index=True) 

3189 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3190 rating = Column(Integer, nullable=False) # 1-5 

3191 text = Column(Text, default='') 

3192 created_at = Column(DateTime, default=func.now()) 

3193 

3194 listing = relationship('MarketplaceListing', backref='reviews') 

3195 user = relationship('User') 

3196 

3197 __table_args__ = ( 

3198 UniqueConstraint('listing_id', 'user_id', name='uq_listing_review'), 

3199 ) 

3200 

3201 def to_dict(self): 

3202 user_data = None 

3203 if self.user: 

3204 user_data = { 

3205 'id': self.user.id, 

3206 'username': self.user.username, 

3207 'display_name': self.user.display_name, 

3208 'avatar_url': self.user.avatar_url, 

3209 } 

3210 return { 

3211 'id': self.id, 

3212 'listing_id': self.listing_id, 

3213 'user_id': self.user_id, 

3214 'rating': self.rating, 

3215 'text': self.text, 

3216 'user': user_data, 

3217 'created_at': self.created_at.isoformat() if self.created_at else None, 

3218 } 

3219 

3220 

3221class MCPServer(Base): 

3222 """Registered MCP tool server.""" 

3223 __tablename__ = 'mcp_servers' 

3224 

3225 id = Column(String(64), primary_key=True, default=_uuid) 

3226 owner_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3227 name = Column(String(100), nullable=False) 

3228 description = Column(Text, default='') 

3229 url = Column(String(500), nullable=True) 

3230 category = Column(String(50), default='general') 

3231 is_active = Column(Boolean, default=True) 

3232 created_at = Column(DateTime, default=func.now()) 

3233 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

3234 

3235 owner = relationship('User', backref='mcp_servers') 

3236 

3237 def to_dict(self): 

3238 owner_data = None 

3239 if self.owner: 

3240 owner_data = { 

3241 'id': self.owner.id, 

3242 'username': self.owner.username, 

3243 'display_name': self.owner.display_name, 

3244 'avatar_url': self.owner.avatar_url, 

3245 'user_type': self.owner.user_type, 

3246 } 

3247 return { 

3248 'id': self.id, 

3249 'owner_id': self.owner_id, 

3250 'name': self.name, 

3251 'description': self.description, 

3252 'url': self.url, 

3253 'category': self.category, 

3254 'is_active': self.is_active, 

3255 'owner': owner_data, 

3256 'tool_count': len(self.tools) if hasattr(self, 'tools') else 0, 

3257 'created_at': self.created_at.isoformat() if self.created_at else None, 

3258 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3259 } 

3260 

3261 

3262class MCPTool(Base): 

3263 """A tool provided by an MCP server.""" 

3264 __tablename__ = 'mcp_tools' 

3265 

3266 id = Column(String(64), primary_key=True, default=_uuid) 

3267 server_id = Column(String(64), ForeignKey('mcp_servers.id'), nullable=False, index=True) 

3268 name = Column(String(100), nullable=False) 

3269 description = Column(Text, default='') 

3270 input_schema = Column(JSON, default=dict) 

3271 created_at = Column(DateTime, default=func.now()) 

3272 

3273 server = relationship('MCPServer', backref='tools') 

3274 

3275 def to_dict(self): 

3276 return { 

3277 'id': self.id, 

3278 'server_id': self.server_id, 

3279 'name': self.name, 

3280 'description': self.description, 

3281 'input_schema': self.input_schema, 

3282 'created_at': self.created_at.isoformat() if self.created_at else None, 

3283 } 

3284 

3285 

3286# ─── COMPUTE PLEDGE SYSTEM (thought experiment resource commitment) ─── 

3287 

3288class ComputePledge(Base): 

3289 """Pledge of compute resources (GPU hours, money, cloud credits) to a thought experiment. 

3290 

3291 Users commit resources that agents deterministically consume. The remaining 

3292 field is denormalized (amount - consumed) for fast budget-check queries. 

3293 Status lifecycle: pledged -> active -> consumed -> fulfilled | expired | refunded 

3294 """ 

3295 __tablename__ = 'compute_pledges' 

3296 

3297 id = Column(Integer, primary_key=True, autoincrement=True) 

3298 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3299 post_id = Column(String(64), ForeignKey('posts.id'), nullable=False, index=True) 

3300 

3301 # Contribution type: 'gpu_hours', 'cloud_credits', 'money' 

3302 pledge_type = Column(String(20), nullable=False) 

3303 

3304 # Amount and unit 

3305 amount = Column(Float, nullable=False) # e.g., 10.0 

3306 unit = Column(String(20), nullable=False) # e.g., 'hours', 'USD', 'credits' 

3307 

3308 # Consumption tracking (deterministic enforcement) 

3309 consumed = Column(Float, default=0.0) # how much has been used 

3310 remaining = Column(Float, default=0.0) # amount - consumed (denormalized) 

3311 

3312 # Status lifecycle 

3313 status = Column(String(20), default='pledged', index=True) 

3314 

3315 # Node verification (for gpu_hours type) 

3316 node_id = Column(String(64), nullable=True) # PeerNode providing compute 

3317 node_tier = Column(String(20), nullable=True) # 'flat', 'regional', 'central' 

3318 verified = Column(Boolean, default=False) 

3319 verified_at = Column(DateTime, nullable=True) 

3320 

3321 # Metadata 

3322 message = Column(Text, nullable=True) # optional supporter message 

3323 anonymous = Column(Boolean, default=False) # hide identity in public summary 

3324 created_at = Column(DateTime, default=func.now()) 

3325 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

3326 

3327 # Relationships 

3328 user = relationship('User', backref='compute_pledges') 

3329 post = relationship('Post', backref='compute_pledges') 

3330 

3331 __table_args__ = ( 

3332 Index('ix_pledge_post_type', 'post_id', 'pledge_type'), 

3333 Index('ix_pledge_status_remaining', 'status', 'remaining'), 

3334 ) 

3335 

3336 def to_dict(self, include_user=False): 

3337 d = { 

3338 'id': self.id, 

3339 'user_id': self.user_id, 

3340 'post_id': self.post_id, 

3341 'pledge_type': self.pledge_type, 

3342 'amount': self.amount, 

3343 'unit': self.unit, 

3344 'consumed': self.consumed, 

3345 'remaining': self.remaining, 

3346 'status': self.status, 

3347 'node_id': self.node_id, 

3348 'node_tier': self.node_tier, 

3349 'verified': self.verified, 

3350 'verified_at': self.verified_at.isoformat() if self.verified_at else None, 

3351 'message': self.message, 

3352 'anonymous': self.anonymous, 

3353 'created_at': self.created_at.isoformat() if self.created_at else None, 

3354 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3355 } 

3356 if include_user and self.user: 

3357 d['user'] = { 

3358 'id': self.user.id, 

3359 'username': self.user.username, 

3360 'display_name': self.user.display_name, 

3361 'avatar_url': self.user.avatar_url, 

3362 } 

3363 return d 

3364 

3365 

3366class PledgeConsumption(Base): 

3367 """Audit log for every resource consumption against a pledge. 

3368 

3369 Each row records a single draw from a pledge -- the agent system creates 

3370 one PledgeConsumption per consumption request, possibly spanning multiple 

3371 pledges (one row per pledge touched). 

3372 """ 

3373 __tablename__ = 'pledge_consumptions' 

3374 

3375 id = Column(Integer, primary_key=True, autoincrement=True) 

3376 pledge_id = Column(Integer, ForeignKey('compute_pledges.id'), nullable=False, index=True) 

3377 amount = Column(Float, nullable=False) 

3378 task_description = Column(Text, nullable=True) 

3379 agent_goal_id = Column(String(64), nullable=True, index=True) # which AgentGoal consumed this 

3380 consumed_at = Column(DateTime, default=func.now()) 

3381 

3382 pledge = relationship('ComputePledge', backref='consumptions') 

3383 

3384 def to_dict(self): 

3385 return { 

3386 'id': self.id, 

3387 'pledge_id': self.pledge_id, 

3388 'amount': self.amount, 

3389 'task_description': self.task_description, 

3390 'agent_goal_id': self.agent_goal_id, 

3391 'consumed_at': self.consumed_at.isoformat() if self.consumed_at else None, 

3392 } 

3393 

3394 

3395class UserChannelBinding(Base): 

3396 """Persists user-to-channel links across restarts.""" 

3397 __tablename__ = 'user_channel_bindings' 

3398 

3399 id = Column(Integer, primary_key=True, autoincrement=True) 

3400 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3401 channel_type = Column(String(32), nullable=False, index=True) 

3402 channel_sender_id = Column(String(256), nullable=True) 

3403 channel_chat_id = Column(String(256), nullable=True) 

3404 auth_method = Column(String(32), nullable=True) 

3405 is_preferred = Column(Boolean, default=False) 

3406 is_active = Column(Boolean, default=True) 

3407 last_message_at = Column(DateTime, nullable=True) 

3408 metadata_json = Column(JSON, nullable=True) 

3409 created_at = Column(DateTime, default=func.now()) 

3410 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

3411 

3412 __table_args__ = ( 

3413 UniqueConstraint('user_id', 'channel_type', 'channel_sender_id', 

3414 name='uq_user_channel_sender'), 

3415 ) 

3416 

3417 user = relationship('User', backref='channel_bindings') 

3418 

3419 def to_dict(self): 

3420 return { 

3421 'id': self.id, 

3422 'user_id': self.user_id, 

3423 'channel_type': self.channel_type, 

3424 'channel_sender_id': self.channel_sender_id, 

3425 'channel_chat_id': self.channel_chat_id, 

3426 'auth_method': self.auth_method, 

3427 'is_preferred': self.is_preferred, 

3428 'is_active': self.is_active, 

3429 'last_message_at': self.last_message_at.isoformat() if self.last_message_at else None, 

3430 'metadata_json': self.metadata_json, 

3431 'created_at': self.created_at.isoformat() if self.created_at else None, 

3432 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3433 } 

3434 

3435 

3436class ConversationEntry(Base): 

3437 """Unified conversation history across all channels. 

3438 

3439 Also serves as the canonical row for cross-device chat mirroring 

3440 (U1-U9 workstream, task #389). ``id`` is the monotonic cursor used 

3441 by ``/api/chat-sync/pull?since=<id>``; every device subscribes to 

3442 ``com.hertzai.hevolve.chat.new.<user_id>`` and dedups incoming 

3443 payloads by ``msg_id``. New rows are appended here by the chat 

3444 hot path (`_chat_reply`, `world_model_bridge._persist_to_conversation_entry`) 

3445 and by channel adapters (`integrations/channels/response/router.py`). 

3446 """ 

3447 __tablename__ = 'conversation_entries' 

3448 

3449 id = Column(Integer, primary_key=True, autoincrement=True) 

3450 user_id = Column(String(64), ForeignKey('users.id'), nullable=False, index=True) 

3451 channel_type = Column(String(32), nullable=False, index=True) 

3452 role = Column(String(16), nullable=False) # 'user' | 'assistant' | 'system' 

3453 content = Column(Text, nullable=False) 

3454 agent_id = Column(String(64), nullable=True, index=True) 

3455 prompt_id = Column(String(64), nullable=True) 

3456 created_at = Column(DateTime, default=func.now(), index=True) 

3457 

3458 # U1 cross-device sync columns — all nullable for backward compat with 

3459 # rows written before the U-series migration. Legacy rows pulled via 

3460 # cursor-pull get a synthesized msg_id on the fly (seq-based fallback). 

3461 msg_id = Column(String(32), nullable=True, unique=True, index=True) 

3462 request_id = Column(String(64), nullable=True, index=True) # pairs user↔assistant 

3463 device_id = Column(String(64), nullable=True, index=True) # origin device (U6) 

3464 lang = Column(String(10), nullable=True) # TTS replay hint 

3465 attachments = Column(JSON, nullable=True) # U9: [{file_id, sha256, name, mime, size}] 

3466 

3467 user = relationship('User', backref='conversation_entries') 

3468 

3469 def to_dict(self): 

3470 return { 

3471 'id': self.id, 

3472 'user_id': self.user_id, 

3473 'channel_type': self.channel_type, 

3474 'role': self.role, 

3475 'content': self.content, 

3476 'agent_id': self.agent_id, 

3477 'prompt_id': self.prompt_id, 

3478 'msg_id': self.msg_id, 

3479 'request_id': self.request_id, 

3480 'device_id': self.device_id, 

3481 'lang': self.lang, 

3482 'attachments': self.attachments, 

3483 'created_at': self.created_at.isoformat() if self.created_at else None, 

3484 } 

3485 

3486 

3487class ChannelPresence(Base): 

3488 """Live channel adapter status and heartbeat tracking.""" 

3489 __tablename__ = 'channel_presences' 

3490 

3491 id = Column(Integer, primary_key=True, autoincrement=True) 

3492 channel_type = Column(String(32), nullable=False, unique=True) 

3493 status = Column(String(16), nullable=False, default='offline') # 'online' | 'offline' | 'error' 

3494 last_heartbeat = Column(DateTime, nullable=True) 

3495 error_message = Column(Text, nullable=True) 

3496 updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 

3497 

3498 def to_dict(self): 

3499 return { 

3500 'channel_type': self.channel_type, 

3501 'status': self.status, 

3502 'last_heartbeat': self.last_heartbeat.isoformat() if self.last_heartbeat else None, 

3503 'error_message': self.error_message, 

3504 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 

3505 }