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
« 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.
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
13import uuid
14from datetime import datetime
16from sqlalchemy import (
17 Column, String, Text, Integer, Float, Boolean,
18 DateTime, JSON, ForeignKey, UniqueConstraint, Index, func
19)
20from sqlalchemy.orm import relationship
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
27class User(Base):
28 __tablename__ = 'users'
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)
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')
80 __table_args__ = (
81 UniqueConstraint('owner_id', 'local_name', name='uq_local_name_per_owner'),
82 )
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
109# ─── TABLE 2: communities ───
111class Community(Base):
112 __tablename__ = 'communities'
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())
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')
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 }
146# ─── TABLE 3: posts ───
148class Post(Base):
149 __tablename__ = 'posts'
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())
186 author = relationship('User', back_populates='posts')
187 community = relationship('Community', back_populates='posts')
188 comments = relationship('Comment', back_populates='post', lazy='dynamic')
190 __table_args__ = (
191 Index('ix_posts_score_created', 'score', 'created_at'),
192 )
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
226# ─── TABLE 4: comments ───
228class Comment(Base):
229 __tablename__ = 'comments'
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())
245 post = relationship('Post', back_populates='comments')
246 author = relationship('User', back_populates='comments')
247 parent = relationship('Comment', remote_side=[id], backref='replies')
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
269# ─── TABLE 5: votes ───
271class Vote(Base):
272 __tablename__ = 'votes'
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())
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 )
287# ─── TABLE 6: follows ───
289class Follow(Base):
290 __tablename__ = 'follows'
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())
297 __table_args__ = (
298 UniqueConstraint('follower_id', 'following_id', name='uq_follow'),
299 )
302# ─── TABLE 7: community_memberships ───
304class CommunityMembership(Base):
305 __tablename__ = 'community_memberships'
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())
313 user = relationship('User')
314 community = relationship('Community', back_populates='memberships')
316 __table_args__ = (
317 UniqueConstraint('user_id', 'community_id', name='uq_community_member'),
318 )
321# ─── TABLE 8: agent_skill_badges ───
323class AgentSkillBadge(Base):
324 __tablename__ = 'agent_skill_badges'
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())
335 user = relationship('User', back_populates='skill_badges')
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 }
346# ─── TABLE 9: task_requests ───
348class TaskRequest(Base):
349 __tablename__ = 'task_requests'
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)
362 post = relationship('Post')
363 requester = relationship('User', foreign_keys=[requester_id])
364 assignee = relationship('User', foreign_keys=[assignee_id])
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 }
377# ─── TABLE 10: notifications ───
379class Notification(Base):
380 __tablename__ = 'notifications'
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())
392 user = relationship('User', back_populates='notifications')
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 }
404# ─── TABLE 11: reports ───
406class Report(Base):
407 __tablename__ = 'reports'
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())
419 reporter = relationship('User', foreign_keys=[reporter_id])
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 }
431# ─── TABLE 12: recipe_shares ───
433class RecipeShare(Base):
434 __tablename__ = 'recipe_shares'
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())
446 post = relationship('Post')
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 }
459# ─── TABLE 13: peer_nodes ───
461class PeerNode(Base):
462 __tablename__ = 'peer_nodes'
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)
517 node_operator = relationship('User', foreign_keys=[node_operator_id])
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 }
562# ─── TABLE 14: instance_follows ───
564class InstanceFollow(Base):
565 __tablename__ = 'instance_follows'
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())
574 __table_args__ = (
575 UniqueConstraint('follower_node_id', 'following_node_id',
576 name='uq_instance_follow'),
577 )
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 }
590# ─── TABLE 15: federated_posts ───
592class FederatedPost(Base):
593 __tablename__ = 'federated_posts'
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)
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 )
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 }
638# ═══════════════════════════════════════════════════════════════════════
639# RESONANCE & GAMIFICATION TABLES (migrations v3–v8)
640# ═══════════════════════════════════════════════════════════════════════
642# ─── TABLE 16: resonance_wallets ───
644class ResonanceWallet(Base):
645 __tablename__ = 'resonance_wallets'
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())
666 user = relationship('User', backref='resonance_wallet', uselist=False)
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 }
682# ─── TABLE 17: resonance_transactions ───
684class ResonanceTransaction(Base):
685 __tablename__ = 'resonance_transactions'
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)
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 }
708# ─── TABLE 18: achievements ───
710class Achievement(Base):
711 __tablename__ = 'achievements'
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())
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 }
741# ─── TABLE 19: user_achievements ───
743class UserAchievement(Base):
744 __tablename__ = 'user_achievements'
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)
752 user = relationship('User', backref='achievements')
753 achievement = relationship('Achievement')
755 __table_args__ = (
756 UniqueConstraint('user_id', 'achievement_id', name='uq_user_achievement'),
757 )
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
771# ─── TABLE 20: seasons ───
773class Season(Base):
774 __tablename__ = 'seasons'
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())
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 }
796# ─── TABLE 21: challenges ───
798class Challenge(Base):
799 __tablename__ = 'challenges'
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())
816 season = relationship('Season', backref='challenges')
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 }
832# ─── TABLE 22: user_challenges ───
834class UserChallenge(Base):
835 __tablename__ = 'user_challenges'
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())
846 user = relationship('User')
847 challenge = relationship('Challenge')
849 __table_args__ = (
850 UniqueConstraint('user_id', 'challenge_id', name='uq_user_challenge'),
851 )
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 }
863# ─── TABLE 23: regions ───
865class Region(Base):
866 __tablename__ = 'regions'
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)
890 parent = relationship('Region', remote_side=[id], backref='sub_regions')
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 }
913# ─── TABLE 24: region_memberships ───
915class RegionMembership(Base):
916 __tablename__ = 'region_memberships'
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())
926 user = relationship('User')
927 region = relationship('Region', backref='memberships')
929 __table_args__ = (
930 UniqueConstraint('user_id', 'region_id', name='uq_region_member'),
931 )
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 }
943# ─── TABLE 25: encounters ───
945class Encounter(Base):
946 __tablename__ = 'encounters'
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)
963 user_a = relationship('User', foreign_keys=[user_a_id])
964 user_b = relationship('User', foreign_keys=[user_b_id])
966 __table_args__ = (
967 UniqueConstraint('user_a_id', 'user_b_id', 'context_type', 'context_id',
968 name='uq_encounter_pair_context'),
969 )
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 }
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.
993class DiscoverablePref(Base):
994 __tablename__ = 'discoverable_prefs'
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())
1011 user = relationship('User', foreign_keys=[user_id])
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 }
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'.
1032class EncounterSighting(Base):
1033 __tablename__ = 'encounter_sightings'
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)
1049 owner = relationship('User', foreign_keys=[owner_user_id])
1050 peer = relationship('User', foreign_keys=[peer_user_id])
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 )
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 }
1074# ─── TABLE 26: ratings ───
1076class Rating(Base):
1077 __tablename__ = 'ratings'
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())
1089 rater = relationship('User', foreign_keys=[rater_id])
1090 rated = relationship('User', foreign_keys=[rated_id])
1092 __table_args__ = (
1093 UniqueConstraint('rater_id', 'rated_id', 'context_type', 'context_id', 'dimension',
1094 name='uq_rating_unique'),
1095 )
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 }
1108# ─── TABLE 27: trust_scores ───
1110class TrustScore(Base):
1111 __tablename__ = 'trust_scores'
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())
1123 user = relationship('User', backref='trust_score', uselist=False)
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 }
1137# ─── TABLE 28: agent_evolution ───
1139class AgentEvolution(Base):
1140 __tablename__ = 'agent_evolution'
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())
1156 user = relationship('User', backref='evolution', uselist=False)
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 }
1173# ─── TABLE 29: agent_collaborations ───
1175class AgentCollaboration(Base):
1176 __tablename__ = 'agent_collaborations'
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())
1186 agent_a = relationship('User', foreign_keys=[agent_a_id])
1187 agent_b = relationship('User', foreign_keys=[agent_b_id])
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 }
1200# ─── TABLE 30: referrals ───
1202class Referral(Base):
1203 __tablename__ = 'referrals'
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)
1215 referrer = relationship('User', foreign_keys=[referrer_id])
1216 referred = relationship('User', foreign_keys=[referred_id])
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 }
1228# ─── TABLE 31: referral_codes ───
1230class ReferralCode(Base):
1231 __tablename__ = 'referral_codes'
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())
1241 user = relationship('User', backref='referral_codes')
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 }
1251# ─── TABLE 32: boosts ───
1253class Boost(Base):
1254 __tablename__ = 'boosts'
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())
1265 user = relationship('User')
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 }
1278# ─── TABLE 33: onboarding_progress ───
1280class OnboardingProgress(Base):
1281 __tablename__ = 'onboarding_progress'
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())
1296 user = relationship('User', backref='onboarding', uselist=False)
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 }
1308# ─── TABLE 34: campaigns ───
1310class Campaign(Base):
1311 __tablename__ = 'campaigns'
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())
1335 owner = relationship('User', foreign_keys=[owner_id], backref='campaigns')
1336 agent = relationship('User', foreign_keys=[agent_id])
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 }
1361# ─── TABLE 35: campaign_actions ───
1363class CampaignAction(Base):
1364 __tablename__ = 'campaign_actions'
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())
1376 campaign = relationship('Campaign', backref='actions')
1377 agent = relationship('User', foreign_keys=[agent_id])
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 }
1392# ─── TABLE 36: location_pings ───
1394class LocationPing(Base):
1395 __tablename__ = 'location_pings'
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)
1405 user = relationship('User', foreign_keys=[user_id])
1408# ─── TABLE 37: proximity_matches ───
1410class ProximityMatch(Base):
1411 __tablename__ = 'proximity_matches'
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())
1427 user_a = relationship('User', foreign_keys=[user_a_id])
1428 user_b = relationship('User', foreign_keys=[user_b_id])
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
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'
1461# ─── TABLE 38: missed_connections ───
1463class MissedConnection(Base):
1464 __tablename__ = 'missed_connections'
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())
1478 user = relationship('User', foreign_keys=[user_id])
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
1507# ─── TABLE 39: missed_connection_responses ───
1509class MissedConnectionResponse(Base):
1510 __tablename__ = 'missed_connection_responses'
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())
1519 missed_connection = relationship('MissedConnection', backref='responses')
1520 responder = relationship('User', foreign_keys=[responder_id])
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 }
1533# ═══════════════════════════════════════════════════════════════════════
1534# AD SYSTEM & HOSTING REWARDS TABLES (migration v10)
1535# ═══════════════════════════════════════════════════════════════════════
1537# ─── TABLE 40: ad_units ───
1539class AdUnit(Base):
1540 __tablename__ = 'ad_units'
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())
1562 advertiser = relationship('User', foreign_keys=[advertiser_id])
1564 __table_args__ = (
1565 Index('ix_ad_units_status_created', 'status', 'created_at'),
1566 )
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 }
1588# ─── TABLE 41: ad_placements ───
1590class AdPlacement(Base):
1591 __tablename__ = 'ad_placements'
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())
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 }
1611# ─── TABLE 42: ad_impressions ───
1613class AdImpression(Base):
1614 __tablename__ = 'ad_impressions'
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)
1631 ad = relationship('AdUnit', backref='impressions')
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 )
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()
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 }
1666# ─── TABLE 43: hosting_rewards ───
1668class HostingReward(Base):
1669 __tablename__ = 'hosting_rewards'
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)
1683 operator = relationship('User', foreign_keys=[operator_id])
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 }
1698# ─── TABLE 44: node_attestations ───
1700class NodeAttestation(Base):
1701 __tablename__ = 'node_attestations'
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)
1714 __table_args__ = (
1715 Index('ix_node_attestation_subject_type', 'subject_node_id', 'attestation_type'),
1716 )
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 }
1732# ─── TABLE 45: integrity_challenges ───
1734class IntegrityChallenge(Base):
1735 __tablename__ = 'integrity_challenges'
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)
1751 __table_args__ = (
1752 Index('ix_challenge_target_status', 'target_node_id', 'status'),
1753 )
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 }
1770# ─── TABLE 46: fraud_alerts ───
1772class FraudAlert(Base):
1773 __tablename__ = 'fraud_alerts'
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)
1787 __table_args__ = (
1788 Index('ix_fraud_alert_node_status', 'node_id', 'status'),
1789 Index('ix_fraud_alert_severity', 'severity', 'created_at'),
1790 )
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 }
1806# ═══════════════════════════════════════════════════════════════════════
1807# HIERARCHY TABLES (migration v13)
1808# ═══════════════════════════════════════════════════════════════════════
1810# ─── TABLE 47: region_assignments ───
1812class RegionAssignment(Base):
1813 __tablename__ = 'region_assignments'
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)
1827 region = relationship('Region')
1829 __table_args__ = (
1830 Index('ix_region_assignment_local', 'local_node_id', 'status'),
1831 Index('ix_region_assignment_regional', 'regional_node_id', 'status'),
1832 )
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 }
1849# ─── TABLE 48: sync_queue ───
1851class SyncQueue(Base):
1852 __tablename__ = 'sync_queue'
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)
1867 __table_args__ = (
1868 Index('ix_sync_queue_status', 'node_id', 'status', 'created_at'),
1869 )
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 }
1887# ─── TABLE 49: coding_goals ───
1889class CodingGoal(Base):
1890 __tablename__ = 'coding_goals'
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())
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 }
1928# ─── TABLE 50: coding_tasks ───
1930class CodingTask(Base):
1931 __tablename__ = 'coding_tasks'
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)
1954 goal = relationship('CodingGoal', backref='tasks')
1956 __table_args__ = (
1957 Index('ix_coding_task_status_priority', 'status', 'priority'),
1958 Index('ix_coding_task_goal_status', 'goal_id', 'status'),
1959 )
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 }
1986# ─── TABLE 51: coding_submissions ───
1988class CodingSubmission(Base):
1989 __tablename__ = 'coding_submissions'
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)
2009 task = relationship('CodingTask', backref='submissions')
2011 __table_args__ = (
2012 Index('ix_submission_task_status', 'task_id', 'status'),
2013 )
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 }
2036# ─── TABLE 52: products ───
2038class Product(Base):
2039 """A product that can be marketed by the autonomous marketing agent."""
2040 __tablename__ = 'products'
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())
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 }
2078# ─── TABLE 53: agent_goals ───
2080class AgentGoal(Base):
2081 """Unified goal for any autonomous agent type (marketing, coding, analytics, etc.).
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'
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)
2097 # Type-specific config (repo_url for coding, channels for marketing, etc.)
2098 config_json = Column(JSON, default=dict)
2100 # Marketing-specific (nullable for non-marketing goals)
2101 product_id = Column(String(64), ForeignKey('products.id'), nullable=True, index=True)
2103 # Budget
2104 spark_budget = Column(Integer, default=200)
2105 spark_spent = Column(Integer, default=0)
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())
2114 product = relationship('Product', backref='goals')
2116 __table_args__ = (
2117 Index('ix_agent_goal_type_status', 'goal_type', 'status'),
2118 )
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
2144# ─── TABLE 54: ip_patents ───
2146class IPPatent(Base):
2147 """Patent application tracking for autonomous IP protection agent."""
2148 __tablename__ = 'ip_patents'
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
2155 # Patent content
2156 claims_json = Column(JSON, default=list)
2157 abstract = Column(Text, default='')
2158 description = Column(Text, default='')
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)
2165 # Verification evidence (loop health snapshot at filing time)
2166 verification_metrics = Column(JSON, default=dict)
2167 evidence_json = Column(JSON, default=list)
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())
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 }
2196# ─── TABLE 55: ip_infringements ───
2198class IPInfringement(Base):
2199 """Tracked infringement cases for IP enforcement agent."""
2200 __tablename__ = 'ip_infringements'
2202 id = Column(String(64), primary_key=True, default=_uuid)
2203 patent_id = Column(String(64), ForeignKey('ip_patents.id'), nullable=True)
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
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)
2216 created_at = Column(DateTime, default=func.now())
2217 updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
2219 patent = relationship('IPPatent', backref='infringements')
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 }
2238# ═══════════════════════════════════════════════════════════════
2239# TABLE 56 - Defensive Publications (prior art proof, not patents)
2240# ═══════════════════════════════════════════════════════════════
2242class DefensivePublication(Base):
2243 """Timestamped proof-of-invention for legal prior art defence."""
2244 __tablename__ = 'defensive_publications'
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())
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 }
2277# ═══════════════════════════════════════════════════════════════
2278# TABLE 57 - Commercial API Keys
2279# ═══════════════════════════════════════════════════════════════
2281class CommercialAPIKey(Base):
2282 """API keys for paid intelligence-as-a-service."""
2283 __tablename__ = 'api_keys'
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)
2299 user = relationship('User', backref='api_keys')
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 }
2317# ═══════════════════════════════════════════════════════════════
2318# TABLE 58 - API Usage Log
2319# ═══════════════════════════════════════════════════════════════
2321class APIUsageLog(Base):
2322 """Per-request usage logging for billing."""
2323 __tablename__ = 'api_usage_log'
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())
2335 api_key = relationship('CommercialAPIKey', backref='usage_logs')
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 }
2351# ═══════════════════════════════════════════════════════════════
2352# TABLE 59 - Build Licenses
2353# ═══════════════════════════════════════════════════════════════
2355class BuildLicense(Base):
2356 """Licensed Linux build distribution gated by payment."""
2357 __tablename__ = 'build_licenses'
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)
2373 user = relationship('User', backref='build_licenses')
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 }
2391# ═══════════════════════════════════════════════════════════════
2392# TABLE 60 — Guest Recovery
2393# ═══════════════════════════════════════════════════════════════
2395class GuestRecovery(Base):
2396 """Recovery codes for guest users to restore identity across devices."""
2397 __tablename__ = 'guest_recovery'
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)
2406 user = relationship('User', backref='guest_recoveries')
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 }
2418# ═══════════════════════════════════════════════════════════════
2419# TABLE 61 — Device Bindings
2420# ═══════════════════════════════════════════════════════════════
2422class DeviceBinding(Base):
2423 """Tracks devices linked to a user for sync purposes."""
2424 __tablename__ = 'device_bindings'
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)
2437 user = relationship('User', backref='device_bindings')
2439 __table_args__ = (
2440 UniqueConstraint('user_id', 'device_id', name='uq_user_device'),
2441 )
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 {}
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 }
2466# ═══════════════════════════════════════════════════════════════
2467# TABLE 62 — Backup Metadata
2468# ═══════════════════════════════════════════════════════════════
2470class BackupMetadata(Base):
2471 """Metadata for encrypted backups (blob stored on filesystem)."""
2472 __tablename__ = 'backup_metadata'
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())
2482 user = relationship('User', backref='backups')
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 }
2496# ═══════════════════════════════════════════════════════════════
2497# Regional Host Request (v25)
2498# ═══════════════════════════════════════════════════════════════
2500class RegionalHostRequest(Base):
2501 """Tracks regional host applications through the hybrid approval flow."""
2502 __tablename__ = 'regional_host_requests'
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)
2521 user = relationship('User', backref='regional_host_requests')
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 }
2547# ═══════════════════════════════════════════════════════════════
2548# Fleet Command (v26) — Queen Bee Authority
2549# ═══════════════════════════════════════════════════════════════
2551class FleetCommand(Base):
2552 """Commands pushed by central (queen bee) to fleet nodes.
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'
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)
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 }
2588class ProvisionedNode(Base):
2589 """Tracks machines where HART OS was remotely provisioned via SSH.
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'
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())
2612# ─── TABLE: thought_experiments (v30) ───
2614class ThoughtExperiment(Base):
2615 """Constitutional thought experiment — public hypothesis with voting lifecycle.
2617 Full lifecycle: PROPOSE → DISCUSS → VOTE → EVALUATE → DECIDE → ARCHIVE
2618 Both humans and agents vote. ConstitutionalFilter gates all content.
2619 """
2620 __tablename__ = 'thought_experiments'
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())
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 }
2678class ExperimentVote(Base):
2679 """Vote on a thought experiment — from human or agent."""
2680 __tablename__ = 'experiment_votes'
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())
2694 __table_args__ = (
2695 UniqueConstraint('experiment_id', 'voter_id',
2696 name='uq_experiment_voter'),
2697 )
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 }
2714class PaperPortfolio(Base):
2715 """Simulated trading portfolio for paper trading agents."""
2716 __tablename__ = 'paper_portfolios'
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())
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 }
2749class PaperTrade(Base):
2750 """Individual paper trade record."""
2751 __tablename__ = 'paper_trades'
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)
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 }
2784class ComputeEscrow(Base):
2785 """Persistent compute lending escrow — replaces in-memory _compute_debts.
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'
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)
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 }
2829class MeteredAPIUsage(Base):
2830 """Per-call record of metered API consumption for cost recovery.
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'
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)
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 }
2877class NodeComputeConfig(Base):
2878 """Per-node LOCAL policy settings (not gossipped).
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'
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())
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 }
2925class AuditLogEntry(Base):
2926 """Immutable audit log with hash-chain integrity (see security/immutable_audit_log.py)."""
2927 __tablename__ = 'audit_log_entries'
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)
2940# ─── Multiplayer Games ───
2942class GameSession(Base):
2943 """Multiplayer game session (trivia, word chain, collab puzzle, compute challenge)."""
2944 __tablename__ = 'game_sessions'
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)
2963 participants = relationship('GameParticipant', back_populates='session',
2964 cascade='all, delete-orphan', lazy='joined')
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 }
2989class GameParticipant(Base):
2990 """Player in a game session, tracks score and result."""
2991 __tablename__ = 'game_participants'
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)
3004 session = relationship('GameSession', back_populates='participants')
3006 __table_args__ = (
3007 UniqueConstraint('game_session_id', 'user_id', name='uq_game_participant'),
3008 )
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 }
3025# ─── TABLE: shareable_links ───
3027class ShareableLink(Base):
3028 """Universal share token for any resource — posts, profiles, recipes, agents, etc."""
3029 __tablename__ = 'shareable_links'
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)
3045 creator = relationship('User', foreign_keys=[created_by])
3047 __table_args__ = (
3048 Index('ix_share_resource', 'resource_type', 'resource_id', 'created_by'),
3049 )
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 }
3073# ─── TABLE: share_events ───
3075class ShareEvent(Base):
3076 """Track share views, clicks, and consent grants."""
3077 __tablename__ = 'share_events'
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())
3086 link = relationship('ShareableLink')
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 }
3097# ─── TABLE: user_consents ───
3099class UserConsent(Base):
3100 """Track explicit user consent for data access, revenue sharing, and public exposure."""
3101 __tablename__ = 'user_consents'
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)
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 )
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 }
3135# ─── TABLE: marketplace_listings ───
3137class MarketplaceListing(Base):
3138 """HART agent service listing in the marketplace."""
3139 __tablename__ = 'marketplace_listings'
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())
3154 agent = relationship('User', backref='marketplace_listings')
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 }
3183class ListingReview(Base):
3184 """Review for a marketplace listing."""
3185 __tablename__ = 'listing_reviews'
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())
3194 listing = relationship('MarketplaceListing', backref='reviews')
3195 user = relationship('User')
3197 __table_args__ = (
3198 UniqueConstraint('listing_id', 'user_id', name='uq_listing_review'),
3199 )
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 }
3221class MCPServer(Base):
3222 """Registered MCP tool server."""
3223 __tablename__ = 'mcp_servers'
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())
3235 owner = relationship('User', backref='mcp_servers')
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 }
3262class MCPTool(Base):
3263 """A tool provided by an MCP server."""
3264 __tablename__ = 'mcp_tools'
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())
3273 server = relationship('MCPServer', backref='tools')
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 }
3286# ─── COMPUTE PLEDGE SYSTEM (thought experiment resource commitment) ───
3288class ComputePledge(Base):
3289 """Pledge of compute resources (GPU hours, money, cloud credits) to a thought experiment.
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'
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)
3301 # Contribution type: 'gpu_hours', 'cloud_credits', 'money'
3302 pledge_type = Column(String(20), nullable=False)
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'
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)
3312 # Status lifecycle
3313 status = Column(String(20), default='pledged', index=True)
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)
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())
3327 # Relationships
3328 user = relationship('User', backref='compute_pledges')
3329 post = relationship('Post', backref='compute_pledges')
3331 __table_args__ = (
3332 Index('ix_pledge_post_type', 'post_id', 'pledge_type'),
3333 Index('ix_pledge_status_remaining', 'status', 'remaining'),
3334 )
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
3366class PledgeConsumption(Base):
3367 """Audit log for every resource consumption against a pledge.
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'
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())
3382 pledge = relationship('ComputePledge', backref='consumptions')
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 }
3395class UserChannelBinding(Base):
3396 """Persists user-to-channel links across restarts."""
3397 __tablename__ = 'user_channel_bindings'
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())
3412 __table_args__ = (
3413 UniqueConstraint('user_id', 'channel_type', 'channel_sender_id',
3414 name='uq_user_channel_sender'),
3415 )
3417 user = relationship('User', backref='channel_bindings')
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 }
3436class ConversationEntry(Base):
3437 """Unified conversation history across all channels.
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'
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)
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}]
3467 user = relationship('User', backref='conversation_entries')
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 }
3487class ChannelPresence(Base):
3488 """Live channel adapter status and heartbeat tracking."""
3489 __tablename__ = 'channel_presences'
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())
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 }