v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/

- crates/ld-engine : formule inertielle (parité avec la maquette, garde C),
  coquille / changement de fond, assistant d'édition à règles, composition
  d'un document sous vote permanent (adoptions, aperçus, rivales).
- crates/trustin-access : qui voit, qui ouvre, qui gère les accès.
- crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de
  connect.monnaie-libre.fr), invitations, entrée en vigueur automatique,
  signalements de contradiction, traductions éditoriales, publication git,
  taille de la toile lue sur la chaîne.
- seeds/acte-engagement-certification.json : 33 articles, fr + en + es ;
  en vigueur : préambule, E1, E2 (seed a minima).
- maquette/ : frontend, backend et docker v2 déplacés tels quels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 02:04:42 +02:00
co-authored by Claude Opus 5.5
parent 4b1732916d
commit 67ddee28b2
275 changed files with 8979 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
from app.models.user import DuniterIdentity, Session
from app.models.organization import Organization, OrgMember
from app.models.document import Document, DocumentItem, ItemVersion
from app.models.decision import Decision, DecisionStep
from app.models.vote import VoteSession, Vote
from app.models.mandate import Mandate, MandateStep
from app.models.protocol import VotingProtocol, FormulaConfig
from app.models.qualification import QualificationProtocol
from app.models.group import Group, GroupMember
from app.models.sanctuary import SanctuaryEntry
from app.models.cache import BlockchainCache
__all__ = [
"DuniterIdentity", "Session",
"Organization", "OrgMember",
"Document", "DocumentItem", "ItemVersion",
"Decision", "DecisionStep",
"VoteSession", "Vote",
"Mandate", "MandateStep",
"VotingProtocol", "FormulaConfig",
"QualificationProtocol",
"Group", "GroupMember",
"SanctuaryEntry",
"BlockchainCache",
]
+17
View File
@@ -0,0 +1,17 @@
import uuid
from datetime import datetime
from sqlalchemy import String, DateTime, JSON, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class BlockchainCache(Base):
__tablename__ = "blockchain_cache"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
cache_key: Mapped[str] = mapped_column(String(256), unique=True, nullable=False, index=True)
cache_value: Mapped[dict] = mapped_column(JSON, nullable=False)
fetched_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
+42
View File
@@ -0,0 +1,42 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Decision(Base):
__tablename__ = "decisions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
title: Mapped[str] = mapped_column(String(256), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
context: Mapped[str | None] = mapped_column(Text)
decision_type: Mapped[str] = mapped_column(String(64), nullable=False) # runtime_upgrade, document_change, mandate_vote, custom
status: Mapped[str] = mapped_column(String(32), default="draft") # draft, qualification, review, voting, executed, closed
organization_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("organizations.id"), nullable=True, index=True)
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
created_by_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
steps: Mapped[list["DecisionStep"]] = relationship(back_populates="decision", cascade="all, delete-orphan", order_by="DecisionStep.step_order")
class DecisionStep(Base):
__tablename__ = "decision_steps"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
decision_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("decisions.id"), nullable=False)
step_order: Mapped[int] = mapped_column(Integer, nullable=False)
step_type: Mapped[str] = mapped_column(String(32), nullable=False) # qualification, review, vote, execution, reporting
title: Mapped[str | None] = mapped_column(String(256))
description: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), default="pending") # pending, active, completed, skipped
vote_session_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("vote_sessions.id"))
outcome: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
decision: Mapped["Decision"] = relationship(back_populates="steps")
+64
View File
@@ -0,0 +1,64 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Uuid, Boolean, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Document(Base):
__tablename__ = "documents"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
slug: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
title: Mapped[str] = mapped_column(String(256), nullable=False)
doc_type: Mapped[str] = mapped_column(String(64), nullable=False) # licence, engagement, reglement, constitution
version: Mapped[str] = mapped_column(String(32), default="0.1.0")
status: Mapped[str] = mapped_column(String(32), default="draft") # draft, active, archived
description: Mapped[str | None] = mapped_column(Text)
organization_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("organizations.id"), nullable=True, index=True)
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
chain_anchor: Mapped[str | None] = mapped_column(String(128))
genesis_json: Mapped[str | None] = mapped_column(Text) # JSON: source files, repos, forum URLs, formula trigger
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
items: Mapped[list["DocumentItem"]] = relationship(back_populates="document", cascade="all, delete-orphan", order_by="DocumentItem.sort_order")
class DocumentItem(Base):
__tablename__ = "document_items"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("documents.id"), nullable=False)
position: Mapped[str] = mapped_column(String(16), nullable=False) # "1", "1.1", "3.2"
item_type: Mapped[str] = mapped_column(String(32), default="clause") # clause, rule, verification, preamble, section, genesis
title: Mapped[str | None] = mapped_column(String(256))
current_text: Mapped[str] = mapped_column(Text, nullable=False)
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
sort_order: Mapped[int] = mapped_column(Integer, default=0)
section_tag: Mapped[str | None] = mapped_column(String(64)) # genesis, fondamental, technique, annexe, formule, inertie, ordonnancement
inertia_preset: Mapped[str] = mapped_column(String(16), default="standard") # low, standard, high, very_high
is_permanent_vote: Mapped[bool] = mapped_column(default=True) # permanent vote vs time-bounded
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
document: Mapped["Document"] = relationship(back_populates="items")
versions: Mapped[list["ItemVersion"]] = relationship(back_populates="item", cascade="all, delete-orphan")
class ItemVersion(Base):
__tablename__ = "item_versions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
item_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("document_items.id"), nullable=False)
proposed_text: Mapped[str] = mapped_column(Text, nullable=False)
diff_text: Mapped[str | None] = mapped_column(Text)
rationale: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), default="proposed") # proposed, voting, accepted, rejected
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
proposed_by_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
item: Mapped["DocumentItem"] = relationship(back_populates="versions")
+41
View File
@@ -0,0 +1,41 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Text, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Group(Base):
__tablename__ = "groups"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
organization_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("organizations.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
members: Mapped[list["GroupMember"]] = relationship(
back_populates="group", cascade="all, delete-orphan"
)
class GroupMember(Base):
__tablename__ = "group_members"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
group_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("groups.id"), nullable=False, index=True)
# FK to duniter_identities when the member is a known WoT member; nullable for free-text entries
identity_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("duniter_identities.id"), nullable=True
)
display_name: Mapped[str] = mapped_column(String(128), nullable=False)
added_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
group: Mapped["Group"] = relationship(back_populates="members")
+68
View File
@@ -0,0 +1,68 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Mandate(Base):
__tablename__ = "mandates"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
title: Mapped[str] = mapped_column(String(256), nullable=False)
origin_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"), nullable=True, index=True)
description: Mapped[str | None] = mapped_column(Text)
mandate_type: Mapped[str] = mapped_column(String(64), nullable=False)
status: Mapped[str] = mapped_column(String(32), default="draft")
organization_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("organizations.id"), nullable=True, index=True)
mandatee_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
starts_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
steps: Mapped[list["MandateStep"]] = relationship(
back_populates="mandate", cascade="all, delete-orphan", order_by="MandateStep.step_order"
)
origin_identity: Mapped["DuniterIdentity | None"] = relationship( # type: ignore[name-defined]
"DuniterIdentity", foreign_keys=[origin_id]
)
mandatee: Mapped["DuniterIdentity | None"] = relationship( # type: ignore[name-defined]
"DuniterIdentity", foreign_keys=[mandatee_id]
)
@property
def origin_display_name(self) -> str | None:
if self.origin_identity is not None:
return self.origin_identity.display_name or self.origin_identity.address
return None
@property
def mandatee_display_name(self) -> str | None:
if self.mandatee is not None:
return self.mandatee.display_name or self.mandatee.address
return None
class MandateStep(Base):
__tablename__ = "mandate_steps"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
mandate_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("mandates.id"), nullable=False)
step_order: Mapped[int] = mapped_column(Integer, nullable=False)
step_type: Mapped[str] = mapped_column(String(32), nullable=False)
title: Mapped[str | None] = mapped_column(String(256))
description: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(32), default="pending")
vote_session_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("vote_sessions.id"))
outcome: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
mandate: Mapped["Mandate"] = relationship(back_populates="steps")
# Avoid circular import — DuniterIdentity imported at runtime by SQLAlchemy relationship resolution
from app.models.user import DuniterIdentity # noqa: E402, F401
@@ -0,0 +1,42 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Text, Boolean, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class Organization(Base):
__tablename__ = "organizations"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(128), nullable=False)
slug: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
# commune, enterprise, association, collective, basin, intercommunality, community
org_type: Mapped[str] = mapped_column(String(64), default="community")
# True = all authenticated users see & interact with content (Duniter G1, Axiom Team)
# False = membership required
is_transparent: Mapped[bool] = mapped_column(Boolean, default=False)
color: Mapped[str | None] = mapped_column(String(32)) # CSS color or mood token
icon: Mapped[str | None] = mapped_column(String(64)) # lucide icon name
description: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
members: Mapped[list["OrgMember"]] = relationship(
back_populates="organization", cascade="all, delete-orphan"
)
class OrgMember(Base):
__tablename__ = "org_members"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
org_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("organizations.id"), nullable=False)
identity_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("duniter_identities.id"), nullable=False
)
role: Mapped[str] = mapped_column(String(32), default="member") # admin, member, observer
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
organization: Mapped["Organization"] = relationship(back_populates="members")
+52
View File
@@ -0,0 +1,52 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Float, Boolean, DateTime, ForeignKey, Text, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class FormulaConfig(Base):
__tablename__ = "formula_configs"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
# WoT threshold params
duration_days: Mapped[int] = mapped_column(Integer, default=30)
majority_pct: Mapped[int] = mapped_column(Integer, default=50)
base_exponent: Mapped[float] = mapped_column(Float, default=0.1)
gradient_exponent: Mapped[float] = mapped_column(Float, default=0.2)
constant_base: Mapped[float] = mapped_column(Float, default=0.0)
# Smith criterion
smith_exponent: Mapped[float | None] = mapped_column(Float)
# TechComm criterion
techcomm_exponent: Mapped[float | None] = mapped_column(Float)
# Nuanced vote
nuanced_min_participants: Mapped[int | None] = mapped_column(Integer)
nuanced_threshold_pct: Mapped[int | None] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
protocols: Mapped[list["VotingProtocol"]] = relationship(back_populates="formula_config")
class VotingProtocol(Base):
__tablename__ = "voting_protocols"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
vote_type: Mapped[str] = mapped_column(String(32), nullable=False) # binary, nuanced
formula_config_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("formula_configs.id"), nullable=False)
organization_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("organizations.id"), nullable=True, index=True)
mode_params: Mapped[str | None] = mapped_column(String(64)) # e.g. "D30M50B.1G.2T.1"
is_meta_governed: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
formula_config: Mapped["FormulaConfig"] = relationship(back_populates="protocols")
@@ -0,0 +1,38 @@
import json
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, Text, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class QualificationProtocol(Base):
"""Active configuration for the decision qualification engine.
Thresholds stored here override the engine defaults and can be updated
through the admin interface (meta-governance).
Only one record should be active at a time (is_active=True).
"""
__tablename__ = "qualification_protocols"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
small_group_max: Mapped[int] = mapped_column(Integer, default=5)
collective_wot_min: Mapped[int] = mapped_column(Integer, default=50)
# JSON array of modality slugs, e.g. '["vote_wot","vote_smith","election"]'
default_modalities_json: Mapped[str] = mapped_column(
Text,
default='["vote_wot","vote_smith","consultation_avis","election"]',
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@property
def default_modalities(self) -> list[str]:
return json.loads(self.default_modalities_json)
+22
View File
@@ -0,0 +1,22 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Text, DateTime, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class SanctuaryEntry(Base):
__tablename__ = "sanctuary_entries"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
entry_type: Mapped[str] = mapped_column(String(64), nullable=False) # document, decision, vote_result
reference_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False)
title: Mapped[str | None] = mapped_column(String(256))
content_hash: Mapped[str] = mapped_column(String(128), nullable=False) # SHA-256
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
chain_tx_hash: Mapped[str | None] = mapped_column(String(128))
chain_block: Mapped[int | None] = mapped_column(Integer)
metadata_json: Mapped[str | None] = mapped_column(Text) # JSON string for extra data
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
+34
View File
@@ -0,0 +1,34 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class DuniterIdentity(Base):
__tablename__ = "duniter_identities"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
address: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
display_name: Mapped[str | None] = mapped_column(String(128))
wot_status: Mapped[str] = mapped_column(String(32), default="unknown") # member, pending, revoked, unknown
is_smith: Mapped[bool] = mapped_column(Boolean, default=False)
is_techcomm: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
sessions: Mapped[list["Session"]] = relationship(back_populates="identity", cascade="all, delete-orphan")
class Session(Base):
__tablename__ = "sessions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
identity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("duniter_identities.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
identity: Mapped["DuniterIdentity"] = relationship(back_populates="sessions")
+70
View File
@@ -0,0 +1,70 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Integer, Float, Boolean, Text, DateTime, ForeignKey, Uuid, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class VoteSession(Base):
__tablename__ = "vote_sessions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
item_version_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("item_versions.id"))
voting_protocol_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("voting_protocols.id"), nullable=False)
# Snapshot at session start
wot_size: Mapped[int] = mapped_column(Integer, default=0)
smith_size: Mapped[int] = mapped_column(Integer, default=0)
techcomm_size: Mapped[int] = mapped_column(Integer, default=0)
# Dates
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
ends_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
# Status
status: Mapped[str] = mapped_column(String(32), default="open") # open, closed, tallied
# Tallies
votes_for: Mapped[int] = mapped_column(Integer, default=0)
votes_against: Mapped[int] = mapped_column(Integer, default=0)
votes_total: Mapped[int] = mapped_column(Integer, default=0)
smith_votes_for: Mapped[int] = mapped_column(Integer, default=0)
techcomm_votes_for: Mapped[int] = mapped_column(Integer, default=0)
threshold_required: Mapped[float] = mapped_column(Float, default=0.0)
result: Mapped[str | None] = mapped_column(String(32)) # adopted, rejected, null
# Chain recording
chain_recorded: Mapped[bool] = mapped_column(Boolean, default=False)
chain_tx_hash: Mapped[str | None] = mapped_column(String(128))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
votes: Mapped[list["Vote"]] = relationship(back_populates="session", cascade="all, delete-orphan")
class Vote(Base):
__tablename__ = "votes"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
session_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("vote_sessions.id"), nullable=False)
voter_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("duniter_identities.id"), nullable=False)
vote_value: Mapped[str] = mapped_column(String(32), nullable=False) # for, against, or nuanced levels
nuanced_level: Mapped[int | None] = mapped_column(Integer) # 0-5 for nuanced votes
comment: Mapped[str | None] = mapped_column(Text)
# Cryptographic proof
signature: Mapped[str] = mapped_column(Text, nullable=False)
signed_payload: Mapped[str] = mapped_column(Text, nullable=False)
# Voter status snapshot
voter_wot_status: Mapped[str] = mapped_column(String(32), default="member")
voter_is_smith: Mapped[bool] = mapped_column(Boolean, default=False)
voter_is_techcomm: Mapped[bool] = mapped_column(Boolean, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
session: Mapped["VoteSession"] = relationship(back_populates="votes")