From 25b0d34ee569e3f43036a26ec2a9d36257dc20dd Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 14:57:00 -0500 Subject: [PATCH 1/6] Add Noosphere Engine: Phase 3 features and orchestrator Implement the Noosphere Engine subsystem with the following modules: - Crystal Room: 3-state machine (open/sealed/breathing) with quorum-based sealing, Discord permission-based access control, and slash commands - Ghost Channel: Ephemeral Discord threads with optional LLM oracle (Anthropic API), Fibonacci-spaced posting schedule, template fallback - Morphogenetic Pulse: Socratic prompt generator on phi-timed intervals (B, B*phi, B*phi^2, B*phi^3, reset), weighted by mode oscillator - Serendipity Injector: Cross-topic bridge finder with noise-augmented similarity scoring using Jaccard word overlap - Mode Manager: 10-mode computation taxonomy with pathology detection, admin commands for manual mode switching - Phi Parameter: Golden-ratio oscillator computing mode weights from phase proximity to Fibonacci fraction approximations - NoosphereEngine orchestrator: Per-guild coordinator with phi oscillator, cryptobiosis dormancy detection, event dispatching, cog loading - NoosphereSettings: Pydantic-settings config with env var support - Bot integration: Wire NoosphereCog into setup_hook with graceful fallback All modules have comprehensive unit tests (674 total tests pass). --- src/intelstream/bot.py | 11 + src/intelstream/noosphere/__init__.py | 0 src/intelstream/noosphere/config.py | 75 +++++++ src/intelstream/noosphere/constants.py | 44 ++++ .../noosphere/crystal_room/__init__.py | 0 .../noosphere/crystal_room/access_control.py | 98 +++++++++ src/intelstream/noosphere/crystal_room/cog.py | 204 +++++++++++++++++ .../noosphere/crystal_room/manager.py | 172 +++++++++++++++ src/intelstream/noosphere/engine.py | 205 ++++++++++++++++++ .../noosphere/ghost_channel/__init__.py | 0 .../noosphere/ghost_channel/cog.py | 113 ++++++++++ .../noosphere/ghost_channel/oracle.py | 106 +++++++++ .../noosphere/morphogenetic_field/__init__.py | 0 .../noosphere/morphogenetic_field/cog.py | 93 ++++++++ .../noosphere/morphogenetic_field/pulse.py | 137 ++++++++++++ .../morphogenetic_field/serendipity.py | 106 +++++++++ src/intelstream/noosphere/shared/__init__.py | 0 .../noosphere/shared/mode_manager.py | 181 ++++++++++++++++ .../noosphere/shared/phi_parameter.py | 74 +++++++ tests/test_noosphere/__init__.py | 0 tests/test_noosphere/test_config.py | 42 ++++ tests/test_noosphere/test_constants.py | 41 ++++ tests/test_noosphere/test_crystal_room.py | 187 ++++++++++++++++ tests/test_noosphere/test_engine.py | 122 +++++++++++ tests/test_noosphere/test_ghost_channel.py | 45 ++++ tests/test_noosphere/test_mode_manager.py | 68 ++++++ tests/test_noosphere/test_phi_parameter.py | 64 ++++++ tests/test_noosphere/test_pulse.py | 60 +++++ tests/test_noosphere/test_serendipity.py | 76 +++++++ 29 files changed, 2324 insertions(+) create mode 100644 src/intelstream/noosphere/__init__.py create mode 100644 src/intelstream/noosphere/config.py create mode 100644 src/intelstream/noosphere/constants.py create mode 100644 src/intelstream/noosphere/crystal_room/__init__.py create mode 100644 src/intelstream/noosphere/crystal_room/access_control.py create mode 100644 src/intelstream/noosphere/crystal_room/cog.py create mode 100644 src/intelstream/noosphere/crystal_room/manager.py create mode 100644 src/intelstream/noosphere/engine.py create mode 100644 src/intelstream/noosphere/ghost_channel/__init__.py create mode 100644 src/intelstream/noosphere/ghost_channel/cog.py create mode 100644 src/intelstream/noosphere/ghost_channel/oracle.py create mode 100644 src/intelstream/noosphere/morphogenetic_field/__init__.py create mode 100644 src/intelstream/noosphere/morphogenetic_field/cog.py create mode 100644 src/intelstream/noosphere/morphogenetic_field/pulse.py create mode 100644 src/intelstream/noosphere/morphogenetic_field/serendipity.py create mode 100644 src/intelstream/noosphere/shared/__init__.py create mode 100644 src/intelstream/noosphere/shared/mode_manager.py create mode 100644 src/intelstream/noosphere/shared/phi_parameter.py create mode 100644 tests/test_noosphere/__init__.py create mode 100644 tests/test_noosphere/test_config.py create mode 100644 tests/test_noosphere/test_constants.py create mode 100644 tests/test_noosphere/test_crystal_room.py create mode 100644 tests/test_noosphere/test_engine.py create mode 100644 tests/test_noosphere/test_ghost_channel.py create mode 100644 tests/test_noosphere/test_mode_manager.py create mode 100644 tests/test_noosphere/test_phi_parameter.py create mode 100644 tests/test_noosphere/test_pulse.py create mode 100644 tests/test_noosphere/test_serendipity.py diff --git a/src/intelstream/bot.py b/src/intelstream/bot.py index fc23761..2597738 100644 --- a/src/intelstream/bot.py +++ b/src/intelstream/bot.py @@ -153,6 +153,17 @@ async def setup_hook(self) -> None: await self.add_cog(GitHubCommands(self)) await self.add_cog(GitHubPolling(self)) + try: + from intelstream.noosphere.config import NoosphereSettings + from intelstream.noosphere.engine import NoosphereCog + + noosphere_settings = NoosphereSettings() + if noosphere_settings.enabled: + await self.add_cog(NoosphereCog(self, noosphere_settings)) + logger.info("Noosphere Engine cog loaded") + except Exception: + logger.debug("Noosphere Engine not loaded (disabled or missing dependencies)") + guild = discord.Object(id=self.settings.discord_guild_id) self.tree.copy_global_to(guild=guild) await self.tree.sync(guild=guild) diff --git a/src/intelstream/noosphere/__init__.py b/src/intelstream/noosphere/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/intelstream/noosphere/config.py b/src/intelstream/noosphere/config.py new file mode 100644 index 0000000..89af211 --- /dev/null +++ b/src/intelstream/noosphere/config.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class NoosphereSettings(BaseSettings): + model_config = SettingsConfigDict( + env_prefix="NOOSPHERE_", + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + enabled: bool = Field(default=False, description="Enable the Noosphere Engine") + + # Crystal Room + crystal_room_enabled: bool = Field(default=True, description="Enable Crystal Room feature") + crystal_room_seal_quorum: int = Field( + default=3, ge=2, le=20, description="Minimum members to seal a crystal room" + ) + crystal_room_max_per_guild: int = Field( + default=5, ge=1, le=50, description="Maximum crystal rooms per guild" + ) + + # Ghost Channel + ghost_channel_enabled: bool = Field(default=True, description="Enable Ghost Channel feature") + ghost_oracle_temperature: float = Field( + default=0.9, ge=0.0, le=2.0, description="LLM temperature for ghost oracle" + ) + ghost_oracle_top_p: float = Field( + default=0.95, ge=0.0, le=1.0, description="LLM top_p for ghost oracle" + ) + ghost_base_interval_hours: float = Field( + default=4.0, ge=0.5, le=48.0, description="Base interval for ghost channel posting" + ) + ghost_thread_auto_archive_minutes: int = Field( + default=60, description="Auto-archive ghost threads after N minutes" + ) + + # Morphogenetic Pulse + pulse_enabled: bool = Field(default=True, description="Enable morphogenetic pulse") + pulse_base_interval_minutes: float = Field( + default=60.0, + ge=5.0, + le=1440.0, + description="Base interval for morphogenetic pulses in minutes", + ) + + # Serendipity Injector + serendipity_enabled: bool = Field(default=True, description="Enable serendipity injection") + serendipity_noise_sigma: float = Field( + default=0.2, + ge=0.0, + le=1.0, + description="Noise sigma for serendipity scoring", + ) + serendipity_similarity_min: float = Field( + default=0.3, ge=0.0, le=1.0, description="Minimum similarity for serendipity bridges" + ) + serendipity_similarity_max: float = Field( + default=0.6, ge=0.0, le=1.0, description="Maximum similarity for serendipity bridges" + ) + + # Mode Manager + default_mode: str = Field(default="integrative", description="Default computation mode") + + # Engine + dormancy_threshold_hours: float = Field( + default=48.0, + ge=1.0, + le=720.0, + description="Hours of inactivity before cryptobiosis", + ) diff --git a/src/intelstream/noosphere/constants.py b/src/intelstream/noosphere/constants.py new file mode 100644 index 0000000..5053647 --- /dev/null +++ b/src/intelstream/noosphere/constants.py @@ -0,0 +1,44 @@ +import math +from enum import Enum + +PHI: float = (1 + math.sqrt(5)) / 2 +GOLDEN_ANGLE: float = 2 * math.pi / (PHI**2) +FIBONACCI_SEQ: list[int] = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + + +class CrystalRoomMode(str, Enum): + NUMBER_STATION = "number_station" + WHALE = "whale" + GHOST = "ghost" + + +class CrystalRoomState(str, Enum): + OPEN = "open" + SEALED = "sealed" + BREATHING = "breathing" + + +class ComputationMode(str, Enum): + SUBTRACTIVE = "subtractive" + BROADCAST = "broadcast" + RESONANT = "resonant" + STIGMERGIC = "stigmergic" + PARASITIC = "parasitic" + PARLIAMENTARY = "parliamentary" + INTEGRATIVE = "integrative" + CRYPTOBIOTIC = "cryptobiotic" + PROJECTIVE = "projective" + TOPOLOGICAL = "topological" + + +class PathologyType(str, Enum): + CANCER = "non_terminating_pruning" + CYTOKINE_STORM = "receiver_saturation" + SEIZURE = "destructive_sync" + ANT_MILL = "positive_feedback_loop" + ADDICTION = "host_destructive_opt" + AUTOIMMUNE = "perpetual_non_consensus" + GROUPTHINK = "integration_no_diff" + COMA = "irreversible_suspension" + MISUNDERSTANDING = "irrecoverable_dim_loss" + SCHISM = "topological_damage" diff --git a/src/intelstream/noosphere/crystal_room/__init__.py b/src/intelstream/noosphere/crystal_room/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/intelstream/noosphere/crystal_room/access_control.py b/src/intelstream/noosphere/crystal_room/access_control.py new file mode 100644 index 0000000..8cb86cc --- /dev/null +++ b/src/intelstream/noosphere/crystal_room/access_control.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import discord +import structlog + +logger = structlog.get_logger(__name__) + + +async def create_private_channel( + guild: discord.Guild, + name: str, + creator: discord.Member, + category: discord.CategoryChannel | None = None, +) -> discord.TextChannel: + """Create a private channel visible only to the creator and the bot.""" + overwrites: dict[ + discord.Role | discord.Member | discord.Object, discord.PermissionOverwrite + ] = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + creator: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + read_message_history=True, + ), + } + + if guild.me: + overwrites[guild.me] = discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + manage_channels=True, + manage_messages=True, + read_message_history=True, + ) + + channel = await guild.create_text_channel( + name=f"crystal-{name}", + overwrites=overwrites, + category=category, + topic="Crystal Room -- sealed discussion space", + ) + + logger.info( + "Private crystal channel created", + guild_id=str(guild.id), + channel_id=str(channel.id), + creator_id=str(creator.id), + ) + return channel + + +async def grant_access( + channel: discord.TextChannel, + member: discord.Member, +) -> None: + """Grant a member access to a crystal room channel.""" + await channel.set_permissions( + member, + read_messages=True, + send_messages=True, + read_message_history=True, + ) + logger.info( + "Access granted to crystal room", + channel_id=str(channel.id), + member_id=str(member.id), + ) + + +async def revoke_access( + channel: discord.TextChannel, + member: discord.Member, +) -> None: + """Revoke a member's access to a crystal room channel.""" + await channel.set_permissions(member, overwrite=None) + logger.info( + "Access revoked from crystal room", + channel_id=str(channel.id), + member_id=str(member.id), + ) + + +async def set_sealed_permissions( + channel: discord.TextChannel, +) -> None: + """In sealed state, prevent new members from being added externally.""" + await channel.edit( + topic="Crystal Room [SEALED] -- no new members", + ) + + +async def set_open_permissions( + channel: discord.TextChannel, +) -> None: + """Restore open state permissions.""" + await channel.edit( + topic="Crystal Room -- sealed discussion space", + ) diff --git a/src/intelstream/noosphere/crystal_room/cog.py b/src/intelstream/noosphere/crystal_room/cog.py new file mode 100644 index 0000000..5c7f6a4 --- /dev/null +++ b/src/intelstream/noosphere/crystal_room/cog.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import discord +import structlog +from discord import app_commands +from discord.ext import commands + +from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.constants import CrystalRoomMode, CrystalRoomState +from intelstream.noosphere.crystal_room.access_control import ( + create_private_channel, + grant_access, + set_open_permissions, + set_sealed_permissions, +) +from intelstream.noosphere.crystal_room.manager import CrystalRoomManager + +logger = structlog.get_logger(__name__) + + +class CrystalRoomCog(commands.Cog, name="CrystalRoom"): + """Discord cog for Crystal Room management.""" + + def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: + self.bot = bot + ns = settings or NoosphereSettings() + self.manager = CrystalRoomManager( + seal_quorum=ns.crystal_room_seal_quorum, + max_rooms_per_guild=ns.crystal_room_max_per_guild, + ) + + crystal = app_commands.Group(name="crystal", description="Crystal Room commands") + + @crystal.command(name="create", description="Create a new Crystal Room") + @app_commands.describe( + name="Room name", + mode="Room mode: number_station, whale, or ghost", + ) + async def crystal_create( + self, + interaction: discord.Interaction, + name: str, + mode: str = "number_station", + ) -> None: + if not interaction.guild or not isinstance(interaction.user, discord.Member): + await interaction.response.send_message( + "This command must be used in a server.", ephemeral=True + ) + return + + try: + room_mode = CrystalRoomMode(mode) + except ValueError: + valid = ", ".join(m.value for m in CrystalRoomMode) + await interaction.response.send_message( + f"Invalid mode. Valid modes: {valid}", ephemeral=True + ) + return + + await interaction.response.defer(ephemeral=True) + + try: + channel = await create_private_channel( + guild=interaction.guild, + name=name, + creator=interaction.user, + ) + + self.manager.create_room( + guild_id=str(interaction.guild.id), + channel_id=str(channel.id), + mode=room_mode, + creator_id=str(interaction.user.id), + ) + + await interaction.followup.send( + f"Crystal Room created: {channel.mention} (mode: {room_mode.value})", + ephemeral=True, + ) + + except ValueError as e: + await interaction.followup.send(str(e), ephemeral=True) + except discord.Forbidden: + await interaction.followup.send( + "Missing permissions to create channels.", ephemeral=True + ) + + @crystal.command(name="join", description="Join an existing Crystal Room") + async def crystal_join(self, interaction: discord.Interaction) -> None: + if not interaction.guild or not isinstance(interaction.user, discord.Member): + await interaction.response.send_message( + "This command must be used in a server.", ephemeral=True + ) + return + + channel_id = str(interaction.channel_id) + room = self.manager.get_room(channel_id) + + if room is None: + await interaction.response.send_message( + "This channel is not a Crystal Room.", ephemeral=True + ) + return + + try: + self.manager.add_member(channel_id, str(interaction.user.id)) + except ValueError as e: + await interaction.response.send_message(str(e), ephemeral=True) + return + + channel = interaction.guild.get_channel(int(channel_id)) + if isinstance(channel, discord.TextChannel): + await grant_access(channel, interaction.user) + + await interaction.response.send_message( + f"{interaction.user.display_name} joined the Crystal Room." + ) + + @crystal.command(name="seal", description="Vote to seal the Crystal Room") + async def crystal_seal(self, interaction: discord.Interaction) -> None: + if not interaction.guild: + await interaction.response.send_message( + "This command must be used in a server.", ephemeral=True + ) + return + + channel_id = str(interaction.channel_id) + + try: + sealed, current, needed = self.manager.vote_seal(channel_id, str(interaction.user.id)) + except ValueError as e: + await interaction.response.send_message(str(e), ephemeral=True) + return + + if sealed: + channel = interaction.guild.get_channel(int(channel_id)) + if isinstance(channel, discord.TextChannel): + await set_sealed_permissions(channel) + + self.bot.dispatch( + "crystal_state_change", + guild_id=str(interaction.guild.id), + channel_id=channel_id, + new_state=CrystalRoomState.SEALED.value, + ) + + await interaction.response.send_message( + "The Crystal Room is now **SEALED**. Bot behavior has shifted." + ) + else: + await interaction.response.send_message( + f"Seal vote recorded. {current}/{needed} votes needed." + ) + + @crystal.command(name="unseal", description="Unseal the Crystal Room") + async def crystal_unseal(self, interaction: discord.Interaction) -> None: + if not interaction.guild: + await interaction.response.send_message( + "This command must be used in a server.", ephemeral=True + ) + return + + channel_id = str(interaction.channel_id) + + try: + self.manager.unseal(channel_id, str(interaction.user.id)) + except ValueError as e: + await interaction.response.send_message(str(e), ephemeral=True) + return + + channel = interaction.guild.get_channel(int(channel_id)) + if isinstance(channel, discord.TextChannel): + await set_open_permissions(channel) + + self.bot.dispatch( + "crystal_state_change", + guild_id=str(interaction.guild.id), + channel_id=channel_id, + new_state=CrystalRoomState.OPEN.value, + ) + + await interaction.response.send_message("The Crystal Room is now **OPEN**.") + + @crystal.command(name="status", description="Show Crystal Room status") + async def crystal_status(self, interaction: discord.Interaction) -> None: + channel_id = str(interaction.channel_id) + room = self.manager.get_room(channel_id) + + if room is None: + await interaction.response.send_message( + "This channel is not a Crystal Room.", ephemeral=True + ) + return + + lines = [ + f"**Mode:** {room.mode.value}", + f"**State:** {room.state.value}", + f"**Members:** {len(room.member_ids)}", + f"**Created:** {room.created_at.strftime('%Y-%m-%d %H:%M UTC')}", + ] + if room.sealed_at: + lines.append(f"**Sealed at:** {room.sealed_at.strftime('%Y-%m-%d %H:%M UTC')}") + + await interaction.response.send_message("\n".join(lines)) diff --git a/src/intelstream/noosphere/crystal_room/manager.py b/src/intelstream/noosphere/crystal_room/manager.py new file mode 100644 index 0000000..061794a --- /dev/null +++ b/src/intelstream/noosphere/crystal_room/manager.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import UTC, datetime + +import structlog + +from intelstream.noosphere.constants import CrystalRoomMode, CrystalRoomState + +logger = structlog.get_logger(__name__) + + +@dataclass +class CrystalRoomInfo: + guild_id: str + channel_id: str + mode: CrystalRoomMode + state: CrystalRoomState + member_ids: list[str] + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + sealed_at: datetime | None = None + sealed_by: list[str] = field(default_factory=list) + + +class CrystalRoomManager: + """Manages Crystal Room lifecycle and state transitions. + + State machine: open -> sealed -> breathing (per Arch-3). + Quorum-based transitions require minimum member count to seal. + """ + + def __init__(self, seal_quorum: int = 3, max_rooms_per_guild: int = 5): + self._rooms: dict[str, CrystalRoomInfo] = {} + self._seal_quorum = seal_quorum + self._max_rooms_per_guild = max_rooms_per_guild + self._seal_votes: dict[str, set[str]] = {} + + @property + def rooms(self) -> dict[str, CrystalRoomInfo]: + return dict(self._rooms) + + def guild_room_count(self, guild_id: str) -> int: + return sum(1 for r in self._rooms.values() if r.guild_id == guild_id) + + def create_room( + self, + guild_id: str, + channel_id: str, + mode: CrystalRoomMode, + creator_id: str, + ) -> CrystalRoomInfo: + if self.guild_room_count(guild_id) >= self._max_rooms_per_guild: + raise ValueError( + f"Maximum rooms ({self._max_rooms_per_guild}) reached for guild {guild_id}" + ) + + if channel_id in self._rooms: + raise ValueError(f"Room already exists for channel {channel_id}") + + room = CrystalRoomInfo( + guild_id=guild_id, + channel_id=channel_id, + mode=mode, + state=CrystalRoomState.OPEN, + member_ids=[creator_id], + ) + self._rooms[channel_id] = room + self._seal_votes[channel_id] = set() + + logger.info( + "Crystal room created", + guild_id=guild_id, + channel_id=channel_id, + mode=mode.value, + creator_id=creator_id, + ) + return room + + def get_room(self, channel_id: str) -> CrystalRoomInfo | None: + return self._rooms.get(channel_id) + + def add_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + room = self._rooms.get(channel_id) + if room is None: + raise ValueError(f"No room for channel {channel_id}") + + if room.state == CrystalRoomState.SEALED: + raise ValueError("Cannot join a sealed room") + + if user_id not in room.member_ids: + room.member_ids.append(user_id) + if room.state == CrystalRoomState.BREATHING: + room.state = CrystalRoomState.OPEN + logger.info( + "Room unsealed due to new member", + channel_id=channel_id, + new_member=user_id, + ) + + return room + + def remove_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + room = self._rooms.get(channel_id) + if room is None: + raise ValueError(f"No room for channel {channel_id}") + + if user_id in room.member_ids: + room.member_ids.remove(user_id) + + if not room.member_ids and room.state == CrystalRoomState.SEALED: + room.state = CrystalRoomState.BREATHING + logger.info( + "Room entered breathing state (all members left sealed room)", + channel_id=channel_id, + ) + + return room + + def vote_seal(self, channel_id: str, user_id: str) -> tuple[bool, int, int]: + """Vote to seal a room. Returns (sealed, current_votes, needed).""" + room = self._rooms.get(channel_id) + if room is None: + raise ValueError(f"No room for channel {channel_id}") + + if room.state != CrystalRoomState.OPEN: + raise ValueError(f"Room is already {room.state.value}") + + if user_id not in room.member_ids: + raise ValueError("Only room members can vote to seal") + + votes = self._seal_votes.setdefault(channel_id, set()) + votes.add(user_id) + + needed = min(self._seal_quorum, len(room.member_ids)) + current = len(votes) + + if current >= needed: + room.state = CrystalRoomState.SEALED + room.sealed_at = datetime.now(UTC) + room.sealed_by = list(votes) + self._seal_votes[channel_id] = set() + logger.info( + "Room sealed", + channel_id=channel_id, + sealed_by=room.sealed_by, + ) + return True, current, needed + + return False, current, needed + + def unseal(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + room = self._rooms.get(channel_id) + if room is None: + raise ValueError(f"No room for channel {channel_id}") + + if room.state not in (CrystalRoomState.SEALED, CrystalRoomState.BREATHING): + raise ValueError("Room is not sealed or breathing") + + if user_id not in room.member_ids and room.state == CrystalRoomState.SEALED: + raise ValueError("Only room members can unseal") + + room.state = CrystalRoomState.OPEN + room.sealed_at = None + room.sealed_by = [] + self._seal_votes[channel_id] = set() + + logger.info("Room unsealed", channel_id=channel_id, unsealed_by=user_id) + return room + + def delete_room(self, channel_id: str) -> None: + self._rooms.pop(channel_id, None) + self._seal_votes.pop(channel_id, None) diff --git a/src/intelstream/noosphere/engine.py b/src/intelstream/noosphere/engine.py new file mode 100644 index 0000000..dfa1114 --- /dev/null +++ b/src/intelstream/noosphere/engine.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import TYPE_CHECKING + +import structlog +from discord.ext import commands, tasks + +from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.shared.mode_manager import ModeManager +from intelstream.noosphere.shared.phi_parameter import PhiParameter + +if TYPE_CHECKING: + import discord + +logger = structlog.get_logger(__name__) + + +class NoosphereEngine: + """Central orchestrator for the Noosphere Engine. + + Manages the phi oscillator, coordinates between components, + and handles mode transitions. One instance per guild. + """ + + def __init__(self, bot: commands.Bot, guild_id: str, settings: NoosphereSettings): + self.bot = bot + self.guild_id = guild_id + self.settings = settings + + self.phi = PhiParameter() + self.mode_manager = ModeManager(guild_id) + + self._is_active = True + self._is_cryptobiotic = False + self._last_human_message: datetime = datetime.now(UTC) + self._tick_count = 0 + + @property + def is_active(self) -> bool: + return self._is_active + + @property + def is_cryptobiotic(self) -> bool: + return self._is_cryptobiotic + + async def initialize(self) -> None: + logger.info("NoosphereEngine initialized", guild_id=self.guild_id) + + async def tick(self) -> None: + """Main loop tick. Advances phi phase and computes mode weights.""" + if not self._is_active or self._is_cryptobiotic: + return + + self.phi.advance() + self._tick_count += 1 + + mode_weights = self.phi.mode_weights() + + self.bot.dispatch( + "state_vector_updated", + guild_id=self.guild_id, + mode_weights=mode_weights, + tick_count=self._tick_count, + ) + + await self._check_dormancy() + + async def process_message(self, message: discord.Message) -> None: + """Process an incoming message through the engine.""" + if message.author.bot: + return + + self._last_human_message = datetime.now(UTC) + + if self._is_cryptobiotic: + await self._exit_cryptobiosis() + + self.bot.dispatch( + "message_processed", + guild_id=self.guild_id, + channel_id=str(message.channel.id), + author_id=str(message.author.id), + content=message.content, + ) + + async def _check_dormancy(self) -> None: + """Check if the guild should enter cryptobiosis.""" + now = datetime.now(UTC) + hours_inactive = (now - self._last_human_message).total_seconds() / 3600 + + if hours_inactive >= self.settings.dormancy_threshold_hours and not self._is_cryptobiotic: + await self._enter_cryptobiosis() + + async def _enter_cryptobiosis(self) -> None: + self._is_cryptobiotic = True + logger.info( + "Entering cryptobiosis", + guild_id=self.guild_id, + hours_inactive=(datetime.now(UTC) - self._last_human_message).total_seconds() / 3600, + ) + self.bot.dispatch( + "cryptobiosis_trigger", + guild_id=self.guild_id, + entering_or_exiting="entering", + ) + + async def _exit_cryptobiosis(self) -> None: + self._is_cryptobiotic = False + logger.info("Exiting cryptobiosis", guild_id=self.guild_id) + self.bot.dispatch( + "cryptobiosis_trigger", + guild_id=self.guild_id, + entering_or_exiting="exiting", + ) + + async def shutdown(self) -> None: + self._is_active = False + logger.info("NoosphereEngine shutdown", guild_id=self.guild_id) + + +class NoosphereCog(commands.Cog, name="Noosphere"): + """Top-level cog that manages NoosphereEngine instances per guild.""" + + def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: + self.bot = bot + self.settings = settings or NoosphereSettings() + self.engines: dict[str, NoosphereEngine] = {} + self._tick_task_running = False + + async def cog_load(self) -> None: + if self.settings.enabled: + self._start_tick_loop() + await self._load_sub_cogs() + logger.info("NoosphereCog loaded") + + async def cog_unload(self) -> None: + self._tick_loop.cancel() + for engine in self.engines.values(): + await engine.shutdown() + logger.info("NoosphereCog unloaded") + + async def _load_sub_cogs(self) -> None: + """Load all Phase 3 sub-cogs.""" + from intelstream.noosphere.crystal_room.cog import CrystalRoomCog + from intelstream.noosphere.ghost_channel.cog import GhostChannelCog + from intelstream.noosphere.morphogenetic_field.cog import MorphogeneticPulseCog + from intelstream.noosphere.shared.mode_manager import ModeManagerCog + + cogs: list[commands.Cog] = [ + CrystalRoomCog(self.bot, self.settings), + GhostChannelCog(self.bot, self.settings), + MorphogeneticPulseCog(self.bot, self.settings), + ModeManagerCog(self.bot), + ] + + for cog in cogs: + try: + await self.bot.add_cog(cog) + logger.info("Loaded noosphere sub-cog", cog=cog.qualified_name) + except Exception: + logger.exception("Failed to load noosphere sub-cog", cog=type(cog).__name__) + + def _get_or_create_engine(self, guild_id: str) -> NoosphereEngine: + if guild_id not in self.engines: + engine = NoosphereEngine(self.bot, guild_id, self.settings) + self.engines[guild_id] = engine + return self.engines[guild_id] + + def _start_tick_loop(self) -> None: + if not self._tick_task_running: + self._tick_loop.start() + self._tick_task_running = True + + @tasks.loop(minutes=5) + async def _tick_loop(self) -> None: + for engine in list(self.engines.values()): + try: + await engine.tick() + except Exception: + logger.exception("Engine tick failed", guild_id=engine.guild_id) + + @_tick_loop.before_loop + async def _before_tick_loop(self) -> None: + await self.bot.wait_until_ready() + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + if message.author.bot or not message.guild: + return + if not self.settings.enabled: + return + guild_id = str(message.guild.id) + engine = self._get_or_create_engine(guild_id) + await engine.process_message(message) + + @commands.Cog.listener() + async def on_ready(self) -> None: + if not self.settings.enabled: + return + for guild in self.bot.guilds: + guild_id = str(guild.id) + engine = self._get_or_create_engine(guild_id) + await engine.initialize() + logger.info("NoosphereEngine initialized for all guilds", count=len(self.engines)) diff --git a/src/intelstream/noosphere/ghost_channel/__init__.py b/src/intelstream/noosphere/ghost_channel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/intelstream/noosphere/ghost_channel/cog.py b/src/intelstream/noosphere/ghost_channel/cog.py new file mode 100644 index 0000000..e726d64 --- /dev/null +++ b/src/intelstream/noosphere/ghost_channel/cog.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import math + +import discord +import structlog +from discord import app_commands +from discord.ext import commands + +from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.constants import FIBONACCI_SEQ +from intelstream.noosphere.ghost_channel.oracle import GhostOracle + +logger = structlog.get_logger(__name__) + + +class GhostChannelCog(commands.Cog, name="GhostChannel"): + """Ephemeral Discord threads with optional LLM oracle. + + Posts are Fibonacci-spaced for quasiperiodic timing. + Threads auto-archive after a configurable period. + """ + + def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: + self.bot = bot + ns = settings or NoosphereSettings() + self.oracle = GhostOracle( + temperature=ns.ghost_oracle_temperature, + top_p=ns.ghost_oracle_top_p, + ) + self._auto_archive_minutes = ns.ghost_thread_auto_archive_minutes + self._base_interval_hours = ns.ghost_base_interval_hours + self._fib_index = 0 + self._enabled = ns.ghost_channel_enabled + + def _next_fib_interval_minutes(self) -> float: + """Get next Fibonacci-spaced interval in minutes.""" + fib = FIBONACCI_SEQ[self._fib_index % len(FIBONACCI_SEQ)] + self._fib_index += 1 + if self._fib_index >= len(FIBONACCI_SEQ): + self._fib_index = 0 + return fib + + def _next_posting_delay_seconds(self) -> float: + """Compute the next posting delay using base interval + Fibonacci offset.""" + base_seconds = self._base_interval_hours * 3600 + fib_offset_minutes = self._next_fib_interval_minutes() + fib_offset_seconds = fib_offset_minutes * 60 + jitter = math.sin(self._fib_index * 0.618) * 60 + return max(60.0, base_seconds + fib_offset_seconds + jitter) + + @app_commands.command(name="ghost", description="Ask the Ghost Oracle a question") + @app_commands.describe(question="Your question for the oracle") + async def ghost_ask( + self, + interaction: discord.Interaction, + question: str, + ) -> None: + if not interaction.guild or not interaction.channel: + await interaction.response.send_message( + "This command must be used in a server channel.", ephemeral=True + ) + return + + if not self._enabled: + await interaction.response.send_message( + "Ghost Channel is currently disabled.", ephemeral=True + ) + return + + await interaction.response.defer() + + anthropic_client = getattr(self.bot, "_anthropic_client", None) + + result = await self.oracle.generate_response( + question=question, + fragments=None, + anthropic_client=anthropic_client, + ) + + archive_duration = 60 + if self._auto_archive_minutes in (60, 1440, 4320, 10080): + archive_duration = self._auto_archive_minutes + + if isinstance(interaction.channel, discord.TextChannel): + thread = await interaction.channel.create_thread( + name=f"Ghost: {question[:50]}", + auto_archive_duration=archive_duration, # type: ignore[arg-type] + reason="Ghost Channel oracle response", + ) + + await thread.send( + f"**Question:** {question}\n\n" + f"**The oracle speaks:**\n> {result.response}\n\n" + f"*This thread will auto-archive in " + f"{archive_duration} minutes.*" + ) + + await interaction.followup.send( + f"The oracle has spoken in {thread.mention}", ephemeral=True + ) + else: + await interaction.followup.send(f"**The oracle speaks:**\n> {result.response}") + + @app_commands.command(name="ghost-status", description="Show Ghost Channel configuration") + async def ghost_status(self, interaction: discord.Interaction) -> None: + status = "enabled" if self._enabled else "disabled" + await interaction.response.send_message( + f"**Ghost Channel:** {status}\n" + f"**Oracle temperature:** {self.oracle.temperature}\n" + f"**Auto-archive:** {self._auto_archive_minutes} minutes\n" + f"**Base interval:** {self._base_interval_hours} hours" + ) diff --git a/src/intelstream/noosphere/ghost_channel/oracle.py b/src/intelstream/noosphere/ghost_channel/oracle.py new file mode 100644 index 0000000..e1b2f04 --- /dev/null +++ b/src/intelstream/noosphere/ghost_channel/oracle.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import structlog + +logger = structlog.get_logger(__name__) + +GHOST_ORACLE_SYSTEM_PROMPT = ( + "You are a pareidolic oracle. You receive questions and generate responses " + "as if you were a pattern-recognition engine interpreting structured noise. " + "Your outputs should feel uncannily relevant to the question while being " + "explicitly non-factual. You are a Rorschach test, not an authority. " + "Keep responses to 1-2 sentences. Be cryptic but evocative." +) + + +@dataclass +class OracleResponse: + question: str + response: str + fragments_used: list[str] + + +class GhostOracle: + """Generates pareidolic oracle responses using LLM with high temperature. + + The oracle produces pattern-like responses that feel relevant to questions + while being explicitly non-factual. Uses community data fragments as + source material for pattern-matching. + """ + + def __init__( + self, + temperature: float = 0.9, + top_p: float = 0.95, + ): + self.temperature = temperature + self.top_p = top_p + + async def generate_response( + self, + question: str, + fragments: list[str] | None = None, + anthropic_client: object | None = None, + ) -> OracleResponse: + """Generate an oracle response. Falls back to template if no LLM client.""" + used_fragments = fragments[:3] if fragments else [] + + if anthropic_client is not None: + response_text = await self._llm_response(question, used_fragments, anthropic_client) + else: + response_text = self._template_response(question, used_fragments) + + return OracleResponse( + question=question, + response=response_text, + fragments_used=used_fragments, + ) + + async def _llm_response( + self, + question: str, + fragments: list[str], + client: object, + ) -> str: + fragment_context = "" + if fragments: + fragment_context = "\n\nPatterns detected in the noise: " + " | ".join(fragments) + + user_message = f"The questioner asks: {question}{fragment_context}" + + try: + from anthropic import AsyncAnthropic + + if not isinstance(client, AsyncAnthropic): + return self._template_response(question, fragments) + + response = await client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=150, + temperature=self.temperature, + top_p=self.top_p, + system=GHOST_ORACLE_SYSTEM_PROMPT, + messages=[{"role": "user", "content": user_message}], + ) + + if response.content and len(response.content) > 0: + return response.content[0].text # type: ignore[union-attr] + except Exception: + logger.exception("Ghost oracle LLM call failed, using template fallback") + + return self._template_response(question, fragments) + + def _template_response(self, question: str, fragments: list[str]) -> str: + words = question.lower().split() + key_word = max(words, key=len) if words else "silence" + + if fragments: + return ( + f"The noise arranges itself around '{key_word}'. A pattern emerges: {fragments[0]}" + ) + return ( + f"In the static, the shape of '{key_word}' repeats. " + "Whether this means anything is your decision, not the oracle's." + ) diff --git a/src/intelstream/noosphere/morphogenetic_field/__init__.py b/src/intelstream/noosphere/morphogenetic_field/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/intelstream/noosphere/morphogenetic_field/cog.py b/src/intelstream/noosphere/morphogenetic_field/cog.py new file mode 100644 index 0000000..8a559e7 --- /dev/null +++ b/src/intelstream/noosphere/morphogenetic_field/cog.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import discord +import structlog +from discord import app_commands +from discord.ext import commands + +from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.morphogenetic_field.pulse import MorphogeneticPulseGenerator +from intelstream.noosphere.morphogenetic_field.serendipity import SerendipityInjector + +logger = structlog.get_logger(__name__) + + +class MorphogeneticPulseCog(commands.Cog, name="MorphogeneticPulse"): + """Cog for morphogenetic pulse and serendipity injection. + + Manages Socratic prompts on phi-timed schedule and + cross-topic bridge discovery. + """ + + def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: + self.bot = bot + ns = settings or NoosphereSettings() + self.pulse_generator = MorphogeneticPulseGenerator( + base_interval_minutes=ns.pulse_base_interval_minutes, + ) + self.serendipity = SerendipityInjector( + noise_sigma=ns.serendipity_noise_sigma, + similarity_min=ns.serendipity_similarity_min, + similarity_max=ns.serendipity_similarity_max, + ) + self._pulse_enabled = ns.pulse_enabled + self._serendipity_enabled = ns.serendipity_enabled + + @app_commands.command(name="pulse", description="Trigger a morphogenetic pulse") + @app_commands.checks.has_permissions(administrator=True) + async def manual_pulse(self, interaction: discord.Interaction) -> None: + if not interaction.guild or not interaction.channel_id: + await interaction.response.send_message( + "This command must be used in a server channel.", ephemeral=True + ) + return + + if not self._pulse_enabled: + await interaction.response.send_message( + "Morphogenetic pulse is currently disabled.", ephemeral=True + ) + return + + pulse = self.pulse_generator.generate_pulse( + channel_id=str(interaction.channel_id), + ) + + self.bot.dispatch( + "pulse_fired", + guild_id=str(interaction.guild.id), + channel_id=str(interaction.channel_id), + content=pulse.content, + ) + + await interaction.response.send_message(pulse.content) + + @app_commands.command(name="pulse-status", description="Show pulse generator status") + async def pulse_status(self, interaction: discord.Interaction) -> None: + next_interval = self.pulse_generator.next_interval_minutes() + step = self.pulse_generator.step + + status = "enabled" if self._pulse_enabled else "disabled" + seren_status = "enabled" if self._serendipity_enabled else "disabled" + + await interaction.response.send_message( + f"**Morphogenetic Pulse:** {status}\n" + f"**Serendipity Injector:** {seren_status}\n" + f"**Current step:** {step}\n" + f"**Next interval:** {next_interval:.1f} minutes" + ) + + @app_commands.command( + name="serendipity", + description="Find serendipitous connections in current topics", + ) + async def find_serendipity(self, interaction: discord.Interaction) -> None: + if not self._serendipity_enabled: + await interaction.response.send_message( + "Serendipity injection is currently disabled.", ephemeral=True + ) + return + + await interaction.response.send_message( + "Serendipity injection requires active topic data from the analytics pipeline. " + "Use `/pulse` to trigger a manual catalytic prompt instead." + ) diff --git a/src/intelstream/noosphere/morphogenetic_field/pulse.py b/src/intelstream/noosphere/morphogenetic_field/pulse.py new file mode 100644 index 0000000..aa72510 --- /dev/null +++ b/src/intelstream/noosphere/morphogenetic_field/pulse.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import random +from dataclasses import dataclass, field +from datetime import UTC, datetime +from enum import Enum + +import structlog + +from intelstream.noosphere.constants import PHI + +logger = structlog.get_logger(__name__) + + +class PulseType(str, Enum): + QUESTION = "question" + CROSS_REFERENCE = "cross_reference" + RESURFACED_THREAD = "resurfaced_thread" + THEMATIC_PROMPT = "thematic_prompt" + + +@dataclass +class Pulse: + pulse_type: PulseType + content: str + target_channel_id: str + timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) + + +class MorphogeneticPulseGenerator: + """Generates Socratic prompts on phi-timed schedule. + + Pulse intervals follow: B, B*phi, B*phi^2, B*phi^3, then reset. + Content type is selected by mode weights from the phi oscillator. + """ + + def __init__(self, base_interval_minutes: float = 60.0): + self._base_interval = base_interval_minutes + self._step = 0 + self._last_pulse_time: datetime | None = None + + @property + def step(self) -> int: + return self._step + + def next_interval_minutes(self) -> float: + """Get next pulse interval using phi-scaling. Resets after 4 steps.""" + phase = self._step % 4 + interval = self._base_interval * (PHI**phase) + self._step += 1 + return interval + + def generate_pulse( + self, + channel_id: str, + mode_weights: dict[str, float] | None = None, + available_topics: list[str] | None = None, + recent_questions: list[str] | None = None, + ) -> Pulse: + """Generate a pulse based on mode weights and available content.""" + pulse_type = self._select_pulse_type(mode_weights) + content = self._generate_content(pulse_type, available_topics, recent_questions) + + self._last_pulse_time = datetime.now(UTC) + + logger.info( + "Pulse generated", + pulse_type=pulse_type.value, + channel_id=channel_id, + step=self._step, + ) + + return Pulse( + pulse_type=pulse_type, + content=content, + target_channel_id=channel_id, + ) + + def _select_pulse_type(self, mode_weights: dict[str, float] | None) -> PulseType: + if mode_weights is None: + return random.choice(list(PulseType)) + + crystal_w = mode_weights.get("crystal", 0.25) + attractor_w = mode_weights.get("attractor", 0.25) + quasicrystal_w = mode_weights.get("quasicrystal", 0.25) + ghost_w = mode_weights.get("ghost", 0.25) + + weights = [ + (PulseType.QUESTION, attractor_w + quasicrystal_w), + (PulseType.CROSS_REFERENCE, crystal_w), + (PulseType.RESURFACED_THREAD, quasicrystal_w), + (PulseType.THEMATIC_PROMPT, ghost_w + attractor_w), + ] + + pulse_types = [w[0] for w in weights] + probs = [w[1] for w in weights] + total = sum(probs) + if total <= 0: + return random.choice(list(PulseType)) + normalized = [p / total for p in probs] + + r = random.random() + cumulative = 0.0 + for t, p in zip(pulse_types, normalized, strict=True): + cumulative += p + if r <= cumulative: + return t + return pulse_types[-1] + + def _generate_content( + self, + pulse_type: PulseType, + topics: list[str] | None, + questions: list[str] | None, + ) -> str: + if pulse_type == PulseType.QUESTION: + if topics: + topic = random.choice(topics) + return f"Has anyone explored {topic} from a different angle recently?" + return "What assumptions are we making that we haven't examined?" + + if pulse_type == PulseType.CROSS_REFERENCE: + if topics and len(topics) >= 2: + t1, t2 = random.sample(topics, 2) + return f"There might be an interesting connection between {t1} and {t2}." + return "Some of the recent threads seem to be converging on a shared theme." + + if pulse_type == PulseType.RESURFACED_THREAD: + if questions: + q = random.choice(questions) + return f"An earlier question went unanswered: {q}" + return "There are threads from before that might be worth revisiting." + + if topics: + topic = random.choice(topics) + return f"Consider this from the perspective of {topic}." + return "What would change if we looked at this from the opposite direction?" diff --git a/src/intelstream/noosphere/morphogenetic_field/serendipity.py b/src/intelstream/noosphere/morphogenetic_field/serendipity.py new file mode 100644 index 0000000..76a825e --- /dev/null +++ b/src/intelstream/noosphere/morphogenetic_field/serendipity.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import random +from dataclasses import dataclass, field +from datetime import UTC, datetime + +import structlog + +logger = structlog.get_logger(__name__) + + +@dataclass +class SerendipityBridge: + source_topic: str + target_topic: str + similarity: float + message: str + timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) + + +class SerendipityInjector: + """Generates cross-topic bridges from archive and topic keywords. + + Adds controlled noise to similarity scores to surface unexpected + connections. Bridges topics that are related enough to be relevant + but different enough to surprise. + """ + + def __init__( + self, + noise_sigma: float = 0.2, + similarity_min: float = 0.3, + similarity_max: float = 0.6, + ): + self._noise_sigma = noise_sigma + self._similarity_min = similarity_min + self._similarity_max = similarity_max + + def find_bridges( + self, + current_topics: list[str], + archive_topics: list[str], + similarities: dict[tuple[str, str], float] | None = None, + ) -> list[SerendipityBridge]: + """Find serendipitous connections between current and archived topics.""" + bridges: list[SerendipityBridge] = [] + + if not current_topics or not archive_topics: + return bridges + + for current in current_topics: + for archived in archive_topics: + if current == archived: + continue + + if similarities: + base_sim = similarities.get((current, archived), 0.0) + else: + base_sim = self._estimate_similarity(current, archived) + + noisy_sim = base_sim + random.gauss(0, self._noise_sigma) + noisy_sim = max(0.0, min(1.0, noisy_sim)) + + if self._similarity_min <= noisy_sim <= self._similarity_max: + message = self._generate_bridge_message(current, archived) + bridges.append( + SerendipityBridge( + source_topic=current, + target_topic=archived, + similarity=noisy_sim, + message=message, + ) + ) + + bridges.sort(key=lambda b: b.similarity, reverse=True) + return bridges[:3] + + def select_injection( + self, + current_topics: list[str], + archive_topics: list[str], + similarities: dict[tuple[str, str], float] | None = None, + ) -> SerendipityBridge | None: + """Select the best serendipity injection, if any.""" + bridges = self.find_bridges(current_topics, archive_topics, similarities) + if not bridges: + return None + return bridges[0] + + def _estimate_similarity(self, topic_a: str, topic_b: str) -> float: + """Rough word-overlap similarity when embeddings are unavailable.""" + words_a = set(topic_a.lower().split()) + words_b = set(topic_b.lower().split()) + if not words_a or not words_b: + return 0.0 + intersection = words_a & words_b + union = words_a | words_b + return len(intersection) / len(union) if union else 0.0 + + def _generate_bridge_message(self, source: str, target: str) -> str: + templates = [ + f"This discussion about {source} has an interesting parallel with {target} from the archive.", + f"Has anyone noticed the connection between {source} and the earlier thread on {target}?", + f"The pattern in {source} echoes something from {target} -- worth exploring?", + ] + return random.choice(templates) diff --git a/src/intelstream/noosphere/shared/__init__.py b/src/intelstream/noosphere/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/intelstream/noosphere/shared/mode_manager.py b/src/intelstream/noosphere/shared/mode_manager.py new file mode 100644 index 0000000..5074c1d --- /dev/null +++ b/src/intelstream/noosphere/shared/mode_manager.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import UTC, datetime + +import discord +import structlog +from discord.ext import commands + +from intelstream.noosphere.constants import ComputationMode, PathologyType + +logger = structlog.get_logger(__name__) + + +@dataclass +class ModeTransition: + old_mode: ComputationMode + new_mode: ComputationMode + reason: str + timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) + + +class ModeManager: + """Manages the 10-mode taxonomy for a guild. + + Phase 3: manual mode switching via admin commands. + Phase 4: automatic transitions driven by pathology detection. + """ + + def __init__(self, guild_id: str, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): + self.guild_id = guild_id + self._current_mode = default_mode + self._history: list[ModeTransition] = [] + self._active_pathologies: dict[PathologyType, float] = {} + + @property + def current_mode(self) -> ComputationMode: + return self._current_mode + + @property + def active_pathologies(self) -> dict[PathologyType, float]: + return dict(self._active_pathologies) + + @property + def history(self) -> list[ModeTransition]: + return list(self._history) + + def set_mode(self, new_mode: ComputationMode, reason: str = "manual") -> ModeTransition: + old_mode = self._current_mode + transition = ModeTransition( + old_mode=old_mode, + new_mode=new_mode, + reason=reason, + ) + self._current_mode = new_mode + self._history.append(transition) + logger.info( + "Mode transition", + guild_id=self.guild_id, + old_mode=old_mode.value, + new_mode=new_mode.value, + reason=reason, + ) + return transition + + def report_pathology(self, pathology: PathologyType, severity: float) -> None: + self._active_pathologies[pathology] = max(0.0, min(1.0, severity)) + logger.warning( + "Pathology reported", + guild_id=self.guild_id, + pathology=pathology.value, + severity=severity, + current_mode=self._current_mode.value, + ) + + def clear_pathology(self, pathology: PathologyType) -> None: + self._active_pathologies.pop(pathology, None) + + def get_mode_description(self) -> str: + descriptions: dict[ComputationMode, str] = { + ComputationMode.SUBTRACTIVE: "Pruning low-value paths (Physarum optimization)", + ComputationMode.BROADCAST: "VOC saturation broadcasting", + ComputationMode.RESONANT: "Frequency synchronization across participants", + ComputationMode.STIGMERGIC: "Environmental trace-based coordination", + ComputationMode.PARASITIC: "Behavioral redirection (Cordyceps-style)", + ComputationMode.PARLIAMENTARY: "Distributed authority and consensus", + ComputationMode.INTEGRATIVE: "Gap junction coupling between participants", + ComputationMode.CRYPTOBIOTIC: "Suspended computation (dormancy)", + ComputationMode.PROJECTIVE: "Higher-dimensional shadow projection", + ComputationMode.TOPOLOGICAL: "Shape-invariant information processing", + } + return descriptions.get(self._current_mode, "Unknown mode") + + +class ModeManagerCog(commands.Cog): + """Discord cog for manual mode management commands.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self._managers: dict[str, ModeManager] = {} + + def get_manager(self, guild_id: str) -> ModeManager: + if guild_id not in self._managers: + self._managers[guild_id] = ModeManager(guild_id) + return self._managers[guild_id] + + @discord.app_commands.command(name="mode", description="Show current computation mode") + async def mode_status(self, interaction: discord.Interaction) -> None: + if not interaction.guild: + await interaction.response.send_message("This command must be used in a server.") + return + + manager = self.get_manager(str(interaction.guild.id)) + mode = manager.current_mode + description = manager.get_mode_description() + + pathologies = manager.active_pathologies + pathology_text = "" + if pathologies: + lines = [f" {p.value}: severity {s:.2f}" for p, s in pathologies.items()] + pathology_text = "\nActive pathologies:\n" + "\n".join(lines) + + await interaction.response.send_message( + f"**Current Mode:** {mode.value}\n**Description:** {description}{pathology_text}" + ) + + @discord.app_commands.command(name="mode-set", description="Set computation mode (admin)") + @discord.app_commands.describe(mode="The computation mode to switch to") + @discord.app_commands.checks.has_permissions(administrator=True) + async def mode_set(self, interaction: discord.Interaction, mode: str) -> None: + if not interaction.guild: + await interaction.response.send_message("This command must be used in a server.") + return + + try: + new_mode = ComputationMode(mode) + except ValueError: + valid = ", ".join(m.value for m in ComputationMode) + await interaction.response.send_message( + f"Invalid mode. Valid modes: {valid}", ephemeral=True + ) + return + + manager = self.get_manager(str(interaction.guild.id)) + transition = manager.set_mode(new_mode, reason=f"manual by {interaction.user}") + + self.bot.dispatch( + "mode_transition", + guild_id=str(interaction.guild.id), + old_mode=transition.old_mode.value, + new_mode=transition.new_mode.value, + reason=transition.reason, + ) + + await interaction.response.send_message( + f"Mode changed: {transition.old_mode.value} -> {transition.new_mode.value}" + ) + + @discord.app_commands.command(name="mode-history", description="Show mode transition history") + async def mode_history(self, interaction: discord.Interaction) -> None: + if not interaction.guild: + await interaction.response.send_message("This command must be used in a server.") + return + + manager = self.get_manager(str(interaction.guild.id)) + history = manager.history + + if not history: + await interaction.response.send_message("No mode transitions recorded.") + return + + lines = [] + for t in history[-10:]: + lines.append( + f"{t.timestamp.strftime('%Y-%m-%d %H:%M')} " + f"{t.old_mode.value} -> {t.new_mode.value} ({t.reason})" + ) + + await interaction.response.send_message( + "**Recent Mode Transitions:**\n```\n" + "\n".join(lines) + "\n```" + ) diff --git a/src/intelstream/noosphere/shared/phi_parameter.py b/src/intelstream/noosphere/shared/phi_parameter.py new file mode 100644 index 0000000..ea91360 --- /dev/null +++ b/src/intelstream/noosphere/shared/phi_parameter.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import math +from typing import ClassVar + +from intelstream.noosphere.constants import GOLDEN_ANGLE, PHI + + +class PhiParameter: + """Golden-ratio oscillator for mode balancing. + + The phase advances by the golden angle each tick. + Mode weights are derived from the phase's proximity + to Fibonacci fraction approximations of phi. + """ + + FIBONACCI_FRACTIONS: ClassVar[list[tuple[int, int]]] = [ + (1, 1), + (2, 1), + (3, 2), + (5, 3), + (8, 5), + (13, 8), + (21, 13), + (34, 21), + (55, 34), + ] + + def __init__(self) -> None: + self._phase = 0.0 + self._tick_count = 0 + + @property + def phase(self) -> float: + return self._phase + + @property + def tick_count(self) -> int: + return self._tick_count + + def advance(self) -> None: + self._phase = (self._phase + GOLDEN_ANGLE) % (2 * math.pi) + self._tick_count += 1 + + def mode_weights(self) -> dict[str, float]: + proximity = self._fibonacci_proximity() + crystal_w = proximity**2 + quasicrystal_w = (1.0 - proximity) ** 2 + attractor_w = math.sin(self._phase) * 0.3 + 0.3 + ghost_w = math.cos(self._phase * PHI) * 0.2 + 0.2 + total = crystal_w + quasicrystal_w + attractor_w + ghost_w + if total <= 0: + return {"crystal": 0.25, "attractor": 0.25, "quasicrystal": 0.25, "ghost": 0.25} + return { + "crystal": crystal_w / total, + "attractor": attractor_w / total, + "quasicrystal": quasicrystal_w / total, + "ghost": ghost_w / total, + } + + def _fibonacci_proximity(self) -> float: + """How close the current phase is to any Fibonacci fraction of 2*pi.""" + min_dist = float("inf") + for p, q in self.FIBONACCI_FRACTIONS: + frac_phase = (2 * math.pi * p / q) % (2 * math.pi) + dist = min( + abs(self._phase - frac_phase), + 2 * math.pi - abs(self._phase - frac_phase), + ) + min_dist = min(min_dist, dist) + return 1.0 - (min_dist / math.pi) + + def set_phase(self, phase: float) -> None: + self._phase = phase % (2 * math.pi) diff --git a/tests/test_noosphere/__init__.py b/tests/test_noosphere/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_noosphere/test_config.py b/tests/test_noosphere/test_config.py new file mode 100644 index 0000000..b5a0c1b --- /dev/null +++ b/tests/test_noosphere/test_config.py @@ -0,0 +1,42 @@ +from unittest.mock import patch + +from intelstream.noosphere.config import NoosphereSettings + + +class TestNoosphereSettings: + def test_default_values(self) -> None: + with patch.dict("os.environ", {}, clear=False): + settings = NoosphereSettings() + + assert settings.enabled is False + assert settings.crystal_room_enabled is True + assert settings.crystal_room_seal_quorum == 3 + assert settings.crystal_room_max_per_guild == 5 + assert settings.ghost_channel_enabled is True + assert settings.ghost_oracle_temperature == 0.9 + assert settings.ghost_oracle_top_p == 0.95 + assert settings.ghost_base_interval_hours == 4.0 + assert settings.ghost_thread_auto_archive_minutes == 60 + assert settings.pulse_enabled is True + assert settings.pulse_base_interval_minutes == 60.0 + assert settings.serendipity_enabled is True + assert settings.serendipity_noise_sigma == 0.2 + assert settings.default_mode == "integrative" + assert settings.dormancy_threshold_hours == 48.0 + + def test_env_override(self) -> None: + with patch.dict( + "os.environ", + { + "NOOSPHERE_ENABLED": "true", + "NOOSPHERE_CRYSTAL_ROOM_SEAL_QUORUM": "5", + "NOOSPHERE_GHOST_ORACLE_TEMPERATURE": "0.7", + "NOOSPHERE_PULSE_BASE_INTERVAL_MINUTES": "30", + }, + ): + settings = NoosphereSettings() + + assert settings.enabled is True + assert settings.crystal_room_seal_quorum == 5 + assert settings.ghost_oracle_temperature == 0.7 + assert settings.pulse_base_interval_minutes == 30.0 diff --git a/tests/test_noosphere/test_constants.py b/tests/test_noosphere/test_constants.py new file mode 100644 index 0000000..e29650e --- /dev/null +++ b/tests/test_noosphere/test_constants.py @@ -0,0 +1,41 @@ +import math + +from intelstream.noosphere.constants import ( + FIBONACCI_SEQ, + GOLDEN_ANGLE, + PHI, + ComputationMode, + CrystalRoomMode, + CrystalRoomState, + PathologyType, +) + + +class TestConstants: + def test_phi_value(self) -> None: + assert abs(PHI - 1.618033988749895) < 1e-10 + + def test_golden_angle_value(self) -> None: + expected = 2 * math.pi / (PHI**2) + assert abs(GOLDEN_ANGLE - expected) < 1e-10 + + def test_fibonacci_sequence(self) -> None: + assert FIBONACCI_SEQ == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] + for i in range(2, len(FIBONACCI_SEQ)): + assert FIBONACCI_SEQ[i] == FIBONACCI_SEQ[i - 1] + FIBONACCI_SEQ[i - 2] + + def test_crystal_room_modes(self) -> None: + assert CrystalRoomMode.NUMBER_STATION.value == "number_station" + assert CrystalRoomMode.WHALE.value == "whale" + assert CrystalRoomMode.GHOST.value == "ghost" + + def test_crystal_room_states(self) -> None: + assert CrystalRoomState.OPEN.value == "open" + assert CrystalRoomState.SEALED.value == "sealed" + assert CrystalRoomState.BREATHING.value == "breathing" + + def test_computation_modes_count(self) -> None: + assert len(ComputationMode) == 10 + + def test_pathology_types_count(self) -> None: + assert len(PathologyType) == 10 diff --git a/tests/test_noosphere/test_crystal_room.py b/tests/test_noosphere/test_crystal_room.py new file mode 100644 index 0000000..309e0f5 --- /dev/null +++ b/tests/test_noosphere/test_crystal_room.py @@ -0,0 +1,187 @@ +import pytest + +from intelstream.noosphere.constants import CrystalRoomMode, CrystalRoomState +from intelstream.noosphere.crystal_room.manager import CrystalRoomManager + + +class TestCrystalRoomManager: + @pytest.fixture + def manager(self) -> CrystalRoomManager: + return CrystalRoomManager(seal_quorum=3, max_rooms_per_guild=5) + + def test_create_room(self, manager: CrystalRoomManager) -> None: + room = manager.create_room("guild_1", "chan_1", CrystalRoomMode.NUMBER_STATION, "user_1") + assert room.guild_id == "guild_1" + assert room.channel_id == "chan_1" + assert room.mode == CrystalRoomMode.NUMBER_STATION + assert room.state == CrystalRoomState.OPEN + assert "user_1" in room.member_ids + + def test_create_duplicate_room_raises(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + with pytest.raises(ValueError, match="already exists"): + manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_2") + + def test_max_rooms_per_guild(self, manager: CrystalRoomManager) -> None: + for i in range(5): + manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") + with pytest.raises(ValueError, match="Maximum rooms"): + manager.create_room("guild_1", "chan_extra", CrystalRoomMode.WHALE, "user_1") + + def test_max_rooms_per_guild_different_guilds(self, manager: CrystalRoomManager) -> None: + for i in range(5): + manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") + room = manager.create_room("guild_2", "chan_other", CrystalRoomMode.WHALE, "user_1") + assert room.guild_id == "guild_2" + + def test_get_room(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_1") + room = manager.get_room("chan_1") + assert room is not None + assert room.mode == CrystalRoomMode.GHOST + + def test_get_nonexistent_room(self, manager: CrystalRoomManager) -> None: + assert manager.get_room("nonexistent") is None + + def test_add_member(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + room = manager.add_member("chan_1", "user_2") + assert "user_2" in room.member_ids + + def test_add_member_idempotent(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_1") + room = manager.get_room("chan_1") + assert room is not None + assert room.member_ids.count("user_1") == 1 + + def test_add_member_to_sealed_raises(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + with pytest.raises(ValueError, match="sealed"): + manager.add_member("chan_1", "user_4") + + def test_add_member_to_breathing_reopens(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + manager.remove_member("chan_1", "user_1") + manager.remove_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_3") + + room = manager.get_room("chan_1") + assert room is not None + assert room.state == CrystalRoomState.BREATHING + + room = manager.add_member("chan_1", "user_4") + assert room.state == CrystalRoomState.OPEN + + def test_remove_member(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_2") + room = manager.get_room("chan_1") + assert room is not None + assert "user_2" not in room.member_ids + + def test_remove_all_members_from_sealed_enters_breathing( + self, manager: CrystalRoomManager + ) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + manager.remove_member("chan_1", "user_1") + manager.remove_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_3") + + room = manager.get_room("chan_1") + assert room is not None + assert room.state == CrystalRoomState.BREATHING + + def test_vote_seal_quorum(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + + sealed, current, needed = manager.vote_seal("chan_1", "user_1") + assert not sealed + assert current == 1 + assert needed == 3 + + sealed, current, needed = manager.vote_seal("chan_1", "user_2") + assert not sealed + assert current == 2 + + sealed, current, needed = manager.vote_seal("chan_1", "user_3") + assert sealed + assert current == 3 + + room = manager.get_room("chan_1") + assert room is not None + assert room.state == CrystalRoomState.SEALED + assert room.sealed_at is not None + + def test_vote_seal_small_room(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + + sealed, _, needed = manager.vote_seal("chan_1", "user_1") + assert not sealed + assert needed == 2 + + sealed, _, _ = manager.vote_seal("chan_1", "user_2") + assert sealed + + def test_vote_seal_non_member_raises(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + with pytest.raises(ValueError, match="Only room members"): + manager.vote_seal("chan_1", "user_999") + + def test_vote_seal_already_sealed_raises(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + with pytest.raises(ValueError, match="already"): + manager.vote_seal("chan_1", "user_1") + + def test_unseal(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + room = manager.unseal("chan_1", "user_1") + assert room.state == CrystalRoomState.OPEN + assert room.sealed_at is None + + def test_unseal_open_room_raises(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + with pytest.raises(ValueError, match="not sealed"): + manager.unseal("chan_1", "user_1") + + def test_delete_room(self, manager: CrystalRoomManager) -> None: + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.delete_room("chan_1") + assert manager.get_room("chan_1") is None + + def test_delete_nonexistent_room(self, manager: CrystalRoomManager) -> None: + manager.delete_room("nonexistent") diff --git a/tests/test_noosphere/test_engine.py b/tests/test_noosphere/test_engine.py new file mode 100644 index 0000000..4a4fa39 --- /dev/null +++ b/tests/test_noosphere/test_engine.py @@ -0,0 +1,122 @@ +from datetime import UTC, datetime, timedelta +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.engine import NoosphereCog, NoosphereEngine + + +class TestNoosphereEngine: + @pytest.fixture + def settings(self) -> NoosphereSettings: + return NoosphereSettings(enabled=True, dormancy_threshold_hours=48.0) + + @pytest.fixture + def bot(self) -> MagicMock: + bot = MagicMock() + bot.dispatch = MagicMock() + bot.guilds = [] + bot.wait_until_ready = AsyncMock() + return bot + + @pytest.fixture + def engine(self, bot: MagicMock, settings: NoosphereSettings) -> NoosphereEngine: + return NoosphereEngine(bot, "guild_123", settings) + + async def test_initialize(self, engine: NoosphereEngine) -> None: + await engine.initialize() + assert engine.is_active + + async def test_tick_advances_phi(self, engine: NoosphereEngine) -> None: + initial_tick = engine.phi.tick_count + await engine.tick() + assert engine.phi.tick_count == initial_tick + 1 + + async def test_tick_dispatches_event(self, engine: NoosphereEngine, bot: MagicMock) -> None: + await engine.tick() + bot.dispatch.assert_called() + + async def test_tick_inactive_does_nothing(self, engine: NoosphereEngine) -> None: + await engine.shutdown() + initial_tick = engine.phi.tick_count + await engine.tick() + assert engine.phi.tick_count == initial_tick + + async def test_tick_cryptobiotic_does_nothing(self, engine: NoosphereEngine) -> None: + engine._is_cryptobiotic = True + initial_tick = engine.phi.tick_count + await engine.tick() + assert engine.phi.tick_count == initial_tick + + async def test_process_message_updates_activity(self, engine: NoosphereEngine) -> None: + message = MagicMock() + message.author.bot = False + message.content = "test" + message.channel.id = 123 + message.author.id = 456 + + before = engine._last_human_message + await engine.process_message(message) + assert engine._last_human_message >= before + + async def test_process_message_ignores_bots( + self, engine: NoosphereEngine, bot: MagicMock + ) -> None: + message = MagicMock() + message.author.bot = True + await engine.process_message(message) + bot.dispatch.assert_not_called() + + async def test_dormancy_triggers_cryptobiosis( + self, engine: NoosphereEngine, bot: MagicMock + ) -> None: + engine._last_human_message = datetime.now(UTC) - timedelta(hours=49) + await engine._check_dormancy() + assert engine.is_cryptobiotic + bot.dispatch.assert_called_with( + "cryptobiosis_trigger", + guild_id="guild_123", + entering_or_exiting="entering", + ) + + async def test_message_exits_cryptobiosis(self, engine: NoosphereEngine) -> None: + engine._is_cryptobiotic = True + message = MagicMock() + message.author.bot = False + message.content = "hello" + message.channel.id = 123 + message.author.id = 456 + await engine.process_message(message) + assert not engine.is_cryptobiotic + + async def test_shutdown(self, engine: NoosphereEngine) -> None: + await engine.shutdown() + assert not engine.is_active + + +class TestNoosphereCog: + @pytest.fixture + def settings(self) -> NoosphereSettings: + return NoosphereSettings(enabled=True) + + @pytest.fixture + def bot(self) -> MagicMock: + bot = MagicMock() + bot.dispatch = MagicMock() + bot.guilds = [] + bot.wait_until_ready = AsyncMock() + bot.add_cog = AsyncMock() + return bot + + def test_creates_engines_per_guild(self, bot: MagicMock, settings: NoosphereSettings) -> None: + cog = NoosphereCog(bot, settings) + engine = cog._get_or_create_engine("guild_1") + assert engine.guild_id == "guild_1" + assert "guild_1" in cog.engines + + def test_reuses_engine(self, bot: MagicMock, settings: NoosphereSettings) -> None: + cog = NoosphereCog(bot, settings) + e1 = cog._get_or_create_engine("guild_1") + e2 = cog._get_or_create_engine("guild_1") + assert e1 is e2 diff --git a/tests/test_noosphere/test_ghost_channel.py b/tests/test_noosphere/test_ghost_channel.py new file mode 100644 index 0000000..b7e6c17 --- /dev/null +++ b/tests/test_noosphere/test_ghost_channel.py @@ -0,0 +1,45 @@ +import pytest + +from intelstream.noosphere.ghost_channel.oracle import GhostOracle, OracleResponse + + +class TestGhostOracle: + @pytest.fixture + def oracle(self) -> GhostOracle: + return GhostOracle(temperature=0.9, top_p=0.95) + + async def test_template_response_no_fragments(self, oracle: GhostOracle) -> None: + result = await oracle.generate_response("What is meaning?") + assert isinstance(result, OracleResponse) + assert result.question == "What is meaning?" + assert len(result.response) > 0 + assert "meaning" in result.response.lower() + + async def test_template_response_with_fragments(self, oracle: GhostOracle) -> None: + result = await oracle.generate_response( + "What lies beneath?", + fragments=["ancient patterns", "recursive loops", "hidden symmetry"], + ) + assert isinstance(result, OracleResponse) + assert len(result.fragments_used) <= 3 + assert "ancient patterns" in result.response + + async def test_template_response_empty_question(self, oracle: GhostOracle) -> None: + result = await oracle.generate_response("") + assert isinstance(result, OracleResponse) + assert len(result.response) > 0 + + async def test_fragments_limited_to_three(self, oracle: GhostOracle) -> None: + result = await oracle.generate_response( + "test", + fragments=["a", "b", "c", "d", "e"], + ) + assert len(result.fragments_used) <= 3 + + async def test_llm_response_with_invalid_client(self, oracle: GhostOracle) -> None: + result = await oracle.generate_response( + "test question", + anthropic_client="not_a_real_client", + ) + assert isinstance(result, OracleResponse) + assert len(result.response) > 0 diff --git a/tests/test_noosphere/test_mode_manager.py b/tests/test_noosphere/test_mode_manager.py new file mode 100644 index 0000000..973457e --- /dev/null +++ b/tests/test_noosphere/test_mode_manager.py @@ -0,0 +1,68 @@ +import pytest + +from intelstream.noosphere.constants import ComputationMode, PathologyType +from intelstream.noosphere.shared.mode_manager import ModeManager + + +class TestModeManager: + @pytest.fixture + def manager(self) -> ModeManager: + return ModeManager("guild_123") + + def test_initial_mode(self, manager: ModeManager) -> None: + assert manager.current_mode == ComputationMode.INTEGRATIVE + + def test_custom_initial_mode(self) -> None: + manager = ModeManager("guild_123", default_mode=ComputationMode.RESONANT) + assert manager.current_mode == ComputationMode.RESONANT + + def test_set_mode(self, manager: ModeManager) -> None: + transition = manager.set_mode(ComputationMode.STIGMERGIC, reason="test") + assert transition.old_mode == ComputationMode.INTEGRATIVE + assert transition.new_mode == ComputationMode.STIGMERGIC + assert transition.reason == "test" + assert manager.current_mode == ComputationMode.STIGMERGIC + + def test_mode_history(self, manager: ModeManager) -> None: + assert len(manager.history) == 0 + manager.set_mode(ComputationMode.RESONANT) + manager.set_mode(ComputationMode.BROADCAST) + assert len(manager.history) == 2 + assert manager.history[0].new_mode == ComputationMode.RESONANT + assert manager.history[1].new_mode == ComputationMode.BROADCAST + + def test_report_pathology(self, manager: ModeManager) -> None: + manager.report_pathology(PathologyType.GROUPTHINK, 0.8) + assert PathologyType.GROUPTHINK in manager.active_pathologies + assert manager.active_pathologies[PathologyType.GROUPTHINK] == 0.8 + + def test_report_pathology_clamps_severity(self, manager: ModeManager) -> None: + manager.report_pathology(PathologyType.CANCER, 1.5) + assert manager.active_pathologies[PathologyType.CANCER] == 1.0 + + manager.report_pathology(PathologyType.COMA, -0.5) + assert manager.active_pathologies[PathologyType.COMA] == 0.0 + + def test_clear_pathology(self, manager: ModeManager) -> None: + manager.report_pathology(PathologyType.SEIZURE, 0.5) + manager.clear_pathology(PathologyType.SEIZURE) + assert PathologyType.SEIZURE not in manager.active_pathologies + + def test_clear_nonexistent_pathology(self, manager: ModeManager) -> None: + manager.clear_pathology(PathologyType.SCHISM) + + def test_mode_description(self, manager: ModeManager) -> None: + desc = manager.get_mode_description() + assert "Gap junction" in desc + + def test_history_is_copy(self, manager: ModeManager) -> None: + manager.set_mode(ComputationMode.RESONANT) + history = manager.history + history.clear() + assert len(manager.history) == 1 + + def test_active_pathologies_is_copy(self, manager: ModeManager) -> None: + manager.report_pathology(PathologyType.ADDICTION, 0.3) + pathologies = manager.active_pathologies + pathologies.clear() + assert len(manager.active_pathologies) == 1 diff --git a/tests/test_noosphere/test_phi_parameter.py b/tests/test_noosphere/test_phi_parameter.py new file mode 100644 index 0000000..c96d38e --- /dev/null +++ b/tests/test_noosphere/test_phi_parameter.py @@ -0,0 +1,64 @@ +import math + +import pytest + +from intelstream.noosphere.constants import GOLDEN_ANGLE +from intelstream.noosphere.shared.phi_parameter import PhiParameter + + +class TestPhiParameter: + @pytest.fixture + def phi(self) -> PhiParameter: + return PhiParameter() + + def test_initial_state(self, phi: PhiParameter) -> None: + assert phi.phase == 0.0 + assert phi.tick_count == 0 + + def test_advance(self, phi: PhiParameter) -> None: + phi.advance() + assert abs(phi.phase - GOLDEN_ANGLE) < 1e-10 + assert phi.tick_count == 1 + + def test_phase_wraps(self, phi: PhiParameter) -> None: + for _ in range(100): + phi.advance() + assert 0 <= phi.phase < 2 * math.pi + + def test_mode_weights_sum_to_one(self, phi: PhiParameter) -> None: + for _ in range(20): + phi.advance() + weights = phi.mode_weights() + total = sum(weights.values()) + assert abs(total - 1.0) < 1e-10 + + def test_mode_weights_keys(self, phi: PhiParameter) -> None: + weights = phi.mode_weights() + assert set(weights.keys()) == {"crystal", "attractor", "quasicrystal", "ghost"} + + def test_mode_weights_all_positive(self, phi: PhiParameter) -> None: + for _ in range(50): + phi.advance() + weights = phi.mode_weights() + for w in weights.values(): + assert w >= 0 + + def test_set_phase(self, phi: PhiParameter) -> None: + phi.set_phase(math.pi) + assert abs(phi.phase - math.pi) < 1e-10 + + def test_set_phase_wraps(self, phi: PhiParameter) -> None: + phi.set_phase(3 * math.pi) + assert phi.phase < 2 * math.pi + + def test_fibonacci_proximity_near_fraction(self, phi: PhiParameter) -> None: + phi.set_phase(0.0) + proximity = phi._fibonacci_proximity() + assert proximity > 0.5 + + def test_weights_vary_with_phase(self, phi: PhiParameter) -> None: + phi.set_phase(0.0) + w1 = phi.mode_weights() + phi.set_phase(math.pi / 3) + w2 = phi.mode_weights() + assert w1 != w2 diff --git a/tests/test_noosphere/test_pulse.py b/tests/test_noosphere/test_pulse.py new file mode 100644 index 0000000..ae3d377 --- /dev/null +++ b/tests/test_noosphere/test_pulse.py @@ -0,0 +1,60 @@ +import pytest + +from intelstream.noosphere.constants import PHI +from intelstream.noosphere.morphogenetic_field.pulse import ( + MorphogeneticPulseGenerator, + Pulse, + PulseType, +) + + +class TestMorphogeneticPulseGenerator: + @pytest.fixture + def generator(self) -> MorphogeneticPulseGenerator: + return MorphogeneticPulseGenerator(base_interval_minutes=60.0) + + def test_phi_scaling_intervals(self, generator: MorphogeneticPulseGenerator) -> None: + intervals = [generator.next_interval_minutes() for _ in range(8)] + assert abs(intervals[0] - 60.0) < 0.01 + assert abs(intervals[1] - 60.0 * PHI) < 0.01 + assert abs(intervals[2] - 60.0 * PHI**2) < 0.01 + assert abs(intervals[3] - 60.0 * PHI**3) < 0.01 + assert abs(intervals[4] - 60.0) < 0.01 + + def test_step_increments(self, generator: MorphogeneticPulseGenerator) -> None: + assert generator.step == 0 + generator.next_interval_minutes() + assert generator.step == 1 + + def test_generate_pulse(self, generator: MorphogeneticPulseGenerator) -> None: + pulse = generator.generate_pulse("chan_1") + assert isinstance(pulse, Pulse) + assert pulse.target_channel_id == "chan_1" + assert pulse.pulse_type in list(PulseType) + assert len(pulse.content) > 0 + + def test_generate_pulse_with_mode_weights(self, generator: MorphogeneticPulseGenerator) -> None: + weights = {"crystal": 1.0, "attractor": 0.0, "quasicrystal": 0.0, "ghost": 0.0} + pulse = generator.generate_pulse("chan_1", mode_weights=weights) + assert isinstance(pulse, Pulse) + + def test_generate_pulse_with_topics(self, generator: MorphogeneticPulseGenerator) -> None: + pulse = generator.generate_pulse( + "chan_1", + available_topics=["quantum computing", "neural networks"], + ) + assert isinstance(pulse, Pulse) + + def test_generate_pulse_with_questions(self, generator: MorphogeneticPulseGenerator) -> None: + pulse = generator.generate_pulse( + "chan_1", + recent_questions=["What about entropy?"], + ) + assert isinstance(pulse, Pulse) + + def test_pulse_types_selected_by_weights(self, generator: MorphogeneticPulseGenerator) -> None: + type_counts: dict[PulseType, int] = dict.fromkeys(PulseType, 0) + for _ in range(100): + pulse = generator.generate_pulse("chan_1") + type_counts[pulse.pulse_type] += 1 + assert all(count > 0 for count in type_counts.values()) diff --git a/tests/test_noosphere/test_serendipity.py b/tests/test_noosphere/test_serendipity.py new file mode 100644 index 0000000..4f15dac --- /dev/null +++ b/tests/test_noosphere/test_serendipity.py @@ -0,0 +1,76 @@ +import pytest + +from intelstream.noosphere.morphogenetic_field.serendipity import ( + SerendipityBridge, + SerendipityInjector, +) + + +class TestSerendipityInjector: + @pytest.fixture + def injector(self) -> SerendipityInjector: + return SerendipityInjector( + noise_sigma=0.2, + similarity_min=0.3, + similarity_max=0.6, + ) + + def test_find_bridges_empty(self, injector: SerendipityInjector) -> None: + assert injector.find_bridges([], []) == [] + assert injector.find_bridges(["topic"], []) == [] + assert injector.find_bridges([], ["topic"]) == [] + + def test_find_bridges_with_similarities(self, injector: SerendipityInjector) -> None: + similarities = { + ("current", "archived"): 0.45, + } + bridges = injector.find_bridges(["current"], ["archived"], similarities=similarities) + assert len(bridges) <= 3 + + def test_find_bridges_filters_same_topic(self, injector: SerendipityInjector) -> None: + similarities = {("topic", "topic"): 1.0} + bridges = injector.find_bridges(["topic"], ["topic"], similarities=similarities) + assert len(bridges) == 0 + + def test_find_bridges_limits_to_three(self, injector: SerendipityInjector) -> None: + current = ["a", "b", "c", "d"] + archived = ["x", "y", "z", "w"] + similarities = {} + for c in current: + for a in archived: + similarities[(c, a)] = 0.45 + bridges = injector.find_bridges(current, archived, similarities=similarities) + assert len(bridges) <= 3 + + def test_find_bridges_respects_range(self, injector: SerendipityInjector) -> None: + too_similar = {("a", "b"): 0.9} + bridges = injector.find_bridges(["a"], ["b"], similarities=too_similar) + assert len(bridges) == 0 + + too_different = {("a", "b"): 0.05} + bridges = injector.find_bridges(["a"], ["b"], similarities=too_different) + assert len(bridges) == 0 + + def test_select_injection(self, injector: SerendipityInjector) -> None: + similarities = {("current", "archived"): 0.45} + bridge = injector.select_injection(["current"], ["archived"], similarities=similarities) + if bridge is not None: + assert isinstance(bridge, SerendipityBridge) + assert len(bridge.message) > 0 + + def test_select_injection_empty(self, injector: SerendipityInjector) -> None: + result = injector.select_injection([], []) + assert result is None + + def test_bridge_message_generated(self, injector: SerendipityInjector) -> None: + msg = injector._generate_bridge_message("quantum", "biology") + assert "quantum" in msg + assert "biology" in msg + + def test_estimate_similarity(self, injector: SerendipityInjector) -> None: + sim = injector._estimate_similarity("machine learning", "deep learning") + assert 0 <= sim <= 1 + assert sim > 0 + + sim_different = injector._estimate_similarity("cats", "quantum physics") + assert sim_different == 0.0 From 30e3db050447919f90398a2a206545681af2a052 Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 15:03:04 -0500 Subject: [PATCH 2/6] Use int for Discord IDs instead of str Discord IDs are integers. Align all guild_id, channel_id, and member/user ID types from str to int across the noosphere modules to match the canonical shared model type decided by the team. Removes unnecessary str() wrappers in cog methods since Discord objects already expose IDs as int. Adds channel_id None guards in crystal room cog to satisfy mypy after removing str() coercion. --- src/intelstream/noosphere/crystal_room/cog.py | 47 +++-- .../noosphere/crystal_room/manager.py | 34 ++-- src/intelstream/noosphere/engine.py | 16 +- .../noosphere/morphogenetic_field/cog.py | 6 +- .../noosphere/morphogenetic_field/pulse.py | 4 +- .../noosphere/shared/mode_manager.py | 14 +- tests/test_noosphere/test_crystal_room.py | 184 +++++++++--------- tests/test_noosphere/test_engine.py | 14 +- tests/test_noosphere/test_mode_manager.py | 4 +- tests/test_noosphere/test_pulse.py | 12 +- 10 files changed, 171 insertions(+), 164 deletions(-) diff --git a/src/intelstream/noosphere/crystal_room/cog.py b/src/intelstream/noosphere/crystal_room/cog.py index 5c7f6a4..6b8f8c4 100644 --- a/src/intelstream/noosphere/crystal_room/cog.py +++ b/src/intelstream/noosphere/crystal_room/cog.py @@ -67,10 +67,10 @@ async def crystal_create( ) self.manager.create_room( - guild_id=str(interaction.guild.id), - channel_id=str(channel.id), + guild_id=interaction.guild.id, + channel_id=channel.id, mode=room_mode, - creator_id=str(interaction.user.id), + creator_id=interaction.user.id, ) await interaction.followup.send( @@ -87,13 +87,17 @@ async def crystal_create( @crystal.command(name="join", description="Join an existing Crystal Room") async def crystal_join(self, interaction: discord.Interaction) -> None: - if not interaction.guild or not isinstance(interaction.user, discord.Member): + if ( + not interaction.guild + or not isinstance(interaction.user, discord.Member) + or not interaction.channel_id + ): await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = str(interaction.channel_id) + channel_id = interaction.channel_id room = self.manager.get_room(channel_id) if room is None: @@ -103,12 +107,12 @@ async def crystal_join(self, interaction: discord.Interaction) -> None: return try: - self.manager.add_member(channel_id, str(interaction.user.id)) + self.manager.add_member(channel_id, interaction.user.id) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return - channel = interaction.guild.get_channel(int(channel_id)) + channel = interaction.guild.get_channel(channel_id) if isinstance(channel, discord.TextChannel): await grant_access(channel, interaction.user) @@ -118,28 +122,28 @@ async def crystal_join(self, interaction: discord.Interaction) -> None: @crystal.command(name="seal", description="Vote to seal the Crystal Room") async def crystal_seal(self, interaction: discord.Interaction) -> None: - if not interaction.guild: + if not interaction.guild or not interaction.channel_id: await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = str(interaction.channel_id) + channel_id = interaction.channel_id try: - sealed, current, needed = self.manager.vote_seal(channel_id, str(interaction.user.id)) + sealed, current, needed = self.manager.vote_seal(channel_id, interaction.user.id) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return if sealed: - channel = interaction.guild.get_channel(int(channel_id)) + channel = interaction.guild.get_channel(channel_id) if isinstance(channel, discord.TextChannel): await set_sealed_permissions(channel) self.bot.dispatch( "crystal_state_change", - guild_id=str(interaction.guild.id), + guild_id=interaction.guild.id, channel_id=channel_id, new_state=CrystalRoomState.SEALED.value, ) @@ -154,27 +158,27 @@ async def crystal_seal(self, interaction: discord.Interaction) -> None: @crystal.command(name="unseal", description="Unseal the Crystal Room") async def crystal_unseal(self, interaction: discord.Interaction) -> None: - if not interaction.guild: + if not interaction.guild or not interaction.channel_id: await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = str(interaction.channel_id) + channel_id = interaction.channel_id try: - self.manager.unseal(channel_id, str(interaction.user.id)) + self.manager.unseal(channel_id, interaction.user.id) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return - channel = interaction.guild.get_channel(int(channel_id)) + channel = interaction.guild.get_channel(channel_id) if isinstance(channel, discord.TextChannel): await set_open_permissions(channel) self.bot.dispatch( "crystal_state_change", - guild_id=str(interaction.guild.id), + guild_id=interaction.guild.id, channel_id=channel_id, new_state=CrystalRoomState.OPEN.value, ) @@ -183,8 +187,13 @@ async def crystal_unseal(self, interaction: discord.Interaction) -> None: @crystal.command(name="status", description="Show Crystal Room status") async def crystal_status(self, interaction: discord.Interaction) -> None: - channel_id = str(interaction.channel_id) - room = self.manager.get_room(channel_id) + if not interaction.channel_id: + await interaction.response.send_message( + "This command must be used in a channel.", ephemeral=True + ) + return + + room = self.manager.get_room(interaction.channel_id) if room is None: await interaction.response.send_message( diff --git a/src/intelstream/noosphere/crystal_room/manager.py b/src/intelstream/noosphere/crystal_room/manager.py index 061794a..569e2d2 100644 --- a/src/intelstream/noosphere/crystal_room/manager.py +++ b/src/intelstream/noosphere/crystal_room/manager.py @@ -12,14 +12,14 @@ @dataclass class CrystalRoomInfo: - guild_id: str - channel_id: str + guild_id: int + channel_id: int mode: CrystalRoomMode state: CrystalRoomState - member_ids: list[str] + member_ids: list[int] created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) sealed_at: datetime | None = None - sealed_by: list[str] = field(default_factory=list) + sealed_by: list[int] = field(default_factory=list) class CrystalRoomManager: @@ -30,24 +30,24 @@ class CrystalRoomManager: """ def __init__(self, seal_quorum: int = 3, max_rooms_per_guild: int = 5): - self._rooms: dict[str, CrystalRoomInfo] = {} + self._rooms: dict[int, CrystalRoomInfo] = {} self._seal_quorum = seal_quorum self._max_rooms_per_guild = max_rooms_per_guild - self._seal_votes: dict[str, set[str]] = {} + self._seal_votes: dict[int, set[int]] = {} @property - def rooms(self) -> dict[str, CrystalRoomInfo]: + def rooms(self) -> dict[int, CrystalRoomInfo]: return dict(self._rooms) - def guild_room_count(self, guild_id: str) -> int: + def guild_room_count(self, guild_id: int) -> int: return sum(1 for r in self._rooms.values() if r.guild_id == guild_id) def create_room( self, - guild_id: str, - channel_id: str, + guild_id: int, + channel_id: int, mode: CrystalRoomMode, - creator_id: str, + creator_id: int, ) -> CrystalRoomInfo: if self.guild_room_count(guild_id) >= self._max_rooms_per_guild: raise ValueError( @@ -76,10 +76,10 @@ def create_room( ) return room - def get_room(self, channel_id: str) -> CrystalRoomInfo | None: + def get_room(self, channel_id: int) -> CrystalRoomInfo | None: return self._rooms.get(channel_id) - def add_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + def add_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -99,7 +99,7 @@ def add_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: return room - def remove_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + def remove_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -116,7 +116,7 @@ def remove_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: return room - def vote_seal(self, channel_id: str, user_id: str) -> tuple[bool, int, int]: + def vote_seal(self, channel_id: int, user_id: int) -> tuple[bool, int, int]: """Vote to seal a room. Returns (sealed, current_votes, needed).""" room = self._rooms.get(channel_id) if room is None: @@ -148,7 +148,7 @@ def vote_seal(self, channel_id: str, user_id: str) -> tuple[bool, int, int]: return False, current, needed - def unseal(self, channel_id: str, user_id: str) -> CrystalRoomInfo: + def unseal(self, channel_id: int, user_id: int) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -167,6 +167,6 @@ def unseal(self, channel_id: str, user_id: str) -> CrystalRoomInfo: logger.info("Room unsealed", channel_id=channel_id, unsealed_by=user_id) return room - def delete_room(self, channel_id: str) -> None: + def delete_room(self, channel_id: int) -> None: self._rooms.pop(channel_id, None) self._seal_votes.pop(channel_id, None) diff --git a/src/intelstream/noosphere/engine.py b/src/intelstream/noosphere/engine.py index dfa1114..dd21414 100644 --- a/src/intelstream/noosphere/engine.py +++ b/src/intelstream/noosphere/engine.py @@ -23,7 +23,7 @@ class NoosphereEngine: and handles mode transitions. One instance per guild. """ - def __init__(self, bot: commands.Bot, guild_id: str, settings: NoosphereSettings): + def __init__(self, bot: commands.Bot, guild_id: int, settings: NoosphereSettings): self.bot = bot self.guild_id = guild_id self.settings = settings @@ -79,8 +79,8 @@ async def process_message(self, message: discord.Message) -> None: self.bot.dispatch( "message_processed", guild_id=self.guild_id, - channel_id=str(message.channel.id), - author_id=str(message.author.id), + channel_id=message.channel.id, + author_id=message.author.id, content=message.content, ) @@ -125,7 +125,7 @@ class NoosphereCog(commands.Cog, name="Noosphere"): def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: self.bot = bot self.settings = settings or NoosphereSettings() - self.engines: dict[str, NoosphereEngine] = {} + self.engines: dict[int, NoosphereEngine] = {} self._tick_task_running = False async def cog_load(self) -> None: @@ -161,7 +161,7 @@ async def _load_sub_cogs(self) -> None: except Exception: logger.exception("Failed to load noosphere sub-cog", cog=type(cog).__name__) - def _get_or_create_engine(self, guild_id: str) -> NoosphereEngine: + def _get_or_create_engine(self, guild_id: int) -> NoosphereEngine: if guild_id not in self.engines: engine = NoosphereEngine(self.bot, guild_id, self.settings) self.engines[guild_id] = engine @@ -190,8 +190,7 @@ async def on_message(self, message: discord.Message) -> None: return if not self.settings.enabled: return - guild_id = str(message.guild.id) - engine = self._get_or_create_engine(guild_id) + engine = self._get_or_create_engine(message.guild.id) await engine.process_message(message) @commands.Cog.listener() @@ -199,7 +198,6 @@ async def on_ready(self) -> None: if not self.settings.enabled: return for guild in self.bot.guilds: - guild_id = str(guild.id) - engine = self._get_or_create_engine(guild_id) + engine = self._get_or_create_engine(guild.id) await engine.initialize() logger.info("NoosphereEngine initialized for all guilds", count=len(self.engines)) diff --git a/src/intelstream/noosphere/morphogenetic_field/cog.py b/src/intelstream/noosphere/morphogenetic_field/cog.py index 8a559e7..45b372e 100644 --- a/src/intelstream/noosphere/morphogenetic_field/cog.py +++ b/src/intelstream/noosphere/morphogenetic_field/cog.py @@ -49,13 +49,13 @@ async def manual_pulse(self, interaction: discord.Interaction) -> None: return pulse = self.pulse_generator.generate_pulse( - channel_id=str(interaction.channel_id), + channel_id=interaction.channel_id, ) self.bot.dispatch( "pulse_fired", - guild_id=str(interaction.guild.id), - channel_id=str(interaction.channel_id), + guild_id=interaction.guild.id, + channel_id=interaction.channel_id, content=pulse.content, ) diff --git a/src/intelstream/noosphere/morphogenetic_field/pulse.py b/src/intelstream/noosphere/morphogenetic_field/pulse.py index aa72510..7731f3a 100644 --- a/src/intelstream/noosphere/morphogenetic_field/pulse.py +++ b/src/intelstream/noosphere/morphogenetic_field/pulse.py @@ -23,7 +23,7 @@ class PulseType(str, Enum): class Pulse: pulse_type: PulseType content: str - target_channel_id: str + target_channel_id: int timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) @@ -52,7 +52,7 @@ def next_interval_minutes(self) -> float: def generate_pulse( self, - channel_id: str, + channel_id: int, mode_weights: dict[str, float] | None = None, available_topics: list[str] | None = None, recent_questions: list[str] | None = None, diff --git a/src/intelstream/noosphere/shared/mode_manager.py b/src/intelstream/noosphere/shared/mode_manager.py index 5074c1d..0d6fd34 100644 --- a/src/intelstream/noosphere/shared/mode_manager.py +++ b/src/intelstream/noosphere/shared/mode_manager.py @@ -27,7 +27,7 @@ class ModeManager: Phase 4: automatic transitions driven by pathology detection. """ - def __init__(self, guild_id: str, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): + def __init__(self, guild_id: int, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): self.guild_id = guild_id self._current_mode = default_mode self._history: list[ModeTransition] = [] @@ -97,9 +97,9 @@ class ModeManagerCog(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self._managers: dict[str, ModeManager] = {} + self._managers: dict[int, ModeManager] = {} - def get_manager(self, guild_id: str) -> ModeManager: + def get_manager(self, guild_id: int) -> ModeManager: if guild_id not in self._managers: self._managers[guild_id] = ModeManager(guild_id) return self._managers[guild_id] @@ -110,7 +110,7 @@ async def mode_status(self, interaction: discord.Interaction) -> None: await interaction.response.send_message("This command must be used in a server.") return - manager = self.get_manager(str(interaction.guild.id)) + manager = self.get_manager(interaction.guild.id) mode = manager.current_mode description = manager.get_mode_description() @@ -141,12 +141,12 @@ async def mode_set(self, interaction: discord.Interaction, mode: str) -> None: ) return - manager = self.get_manager(str(interaction.guild.id)) + manager = self.get_manager(interaction.guild.id) transition = manager.set_mode(new_mode, reason=f"manual by {interaction.user}") self.bot.dispatch( "mode_transition", - guild_id=str(interaction.guild.id), + guild_id=interaction.guild.id, old_mode=transition.old_mode.value, new_mode=transition.new_mode.value, reason=transition.reason, @@ -162,7 +162,7 @@ async def mode_history(self, interaction: discord.Interaction) -> None: await interaction.response.send_message("This command must be used in a server.") return - manager = self.get_manager(str(interaction.guild.id)) + manager = self.get_manager(interaction.guild.id) history = manager.history if not history: diff --git a/tests/test_noosphere/test_crystal_room.py b/tests/test_noosphere/test_crystal_room.py index 309e0f5..2d606c4 100644 --- a/tests/test_noosphere/test_crystal_room.py +++ b/tests/test_noosphere/test_crystal_room.py @@ -10,178 +10,178 @@ def manager(self) -> CrystalRoomManager: return CrystalRoomManager(seal_quorum=3, max_rooms_per_guild=5) def test_create_room(self, manager: CrystalRoomManager) -> None: - room = manager.create_room("guild_1", "chan_1", CrystalRoomMode.NUMBER_STATION, "user_1") - assert room.guild_id == "guild_1" - assert room.channel_id == "chan_1" + room = manager.create_room(1001, 2001, CrystalRoomMode.NUMBER_STATION, 3001) + assert room.guild_id == 1001 + assert room.channel_id == 2001 assert room.mode == CrystalRoomMode.NUMBER_STATION assert room.state == CrystalRoomState.OPEN - assert "user_1" in room.member_ids + assert 3001 in room.member_ids def test_create_duplicate_room_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) with pytest.raises(ValueError, match="already exists"): - manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_2") + manager.create_room(1001, 2001, CrystalRoomMode.GHOST, 3002) def test_max_rooms_per_guild(self, manager: CrystalRoomManager) -> None: for i in range(5): - manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") + manager.create_room(1001, 2000 + i, CrystalRoomMode.WHALE, 3001) with pytest.raises(ValueError, match="Maximum rooms"): - manager.create_room("guild_1", "chan_extra", CrystalRoomMode.WHALE, "user_1") + manager.create_room(1001, 2099, CrystalRoomMode.WHALE, 3001) def test_max_rooms_per_guild_different_guilds(self, manager: CrystalRoomManager) -> None: for i in range(5): - manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") - room = manager.create_room("guild_2", "chan_other", CrystalRoomMode.WHALE, "user_1") - assert room.guild_id == "guild_2" + manager.create_room(1001, 2000 + i, CrystalRoomMode.WHALE, 3001) + room = manager.create_room(1002, 2099, CrystalRoomMode.WHALE, 3001) + assert room.guild_id == 1002 def test_get_room(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_1") - room = manager.get_room("chan_1") + manager.create_room(1001, 2001, CrystalRoomMode.GHOST, 3001) + room = manager.get_room(2001) assert room is not None assert room.mode == CrystalRoomMode.GHOST def test_get_nonexistent_room(self, manager: CrystalRoomManager) -> None: - assert manager.get_room("nonexistent") is None + assert manager.get_room(9999) is None def test_add_member(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - room = manager.add_member("chan_1", "user_2") - assert "user_2" in room.member_ids + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + room = manager.add_member(2001, 3002) + assert 3002 in room.member_ids def test_add_member_idempotent(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_1") - room = manager.get_room("chan_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3001) + room = manager.get_room(2001) assert room is not None - assert room.member_ids.count("user_1") == 1 + assert room.member_ids.count(3001) == 1 def test_add_member_to_sealed_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") - manager.vote_seal("chan_1", "user_1") - manager.vote_seal("chan_1", "user_2") - manager.vote_seal("chan_1", "user_3") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) + manager.vote_seal(2001, 3001) + manager.vote_seal(2001, 3002) + manager.vote_seal(2001, 3003) with pytest.raises(ValueError, match="sealed"): - manager.add_member("chan_1", "user_4") + manager.add_member(2001, 3004) def test_add_member_to_breathing_reopens(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") - manager.vote_seal("chan_1", "user_1") - manager.vote_seal("chan_1", "user_2") - manager.vote_seal("chan_1", "user_3") - - manager.remove_member("chan_1", "user_1") - manager.remove_member("chan_1", "user_2") - manager.remove_member("chan_1", "user_3") - - room = manager.get_room("chan_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) + manager.vote_seal(2001, 3001) + manager.vote_seal(2001, 3002) + manager.vote_seal(2001, 3003) + + manager.remove_member(2001, 3001) + manager.remove_member(2001, 3002) + manager.remove_member(2001, 3003) + + room = manager.get_room(2001) assert room is not None assert room.state == CrystalRoomState.BREATHING - room = manager.add_member("chan_1", "user_4") + room = manager.add_member(2001, 3004) assert room.state == CrystalRoomState.OPEN def test_remove_member(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.remove_member("chan_1", "user_2") - room = manager.get_room("chan_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.remove_member(2001, 3002) + room = manager.get_room(2001) assert room is not None - assert "user_2" not in room.member_ids + assert 3002 not in room.member_ids def test_remove_all_members_from_sealed_enters_breathing( self, manager: CrystalRoomManager ) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") - manager.vote_seal("chan_1", "user_1") - manager.vote_seal("chan_1", "user_2") - manager.vote_seal("chan_1", "user_3") - - manager.remove_member("chan_1", "user_1") - manager.remove_member("chan_1", "user_2") - manager.remove_member("chan_1", "user_3") - - room = manager.get_room("chan_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) + manager.vote_seal(2001, 3001) + manager.vote_seal(2001, 3002) + manager.vote_seal(2001, 3003) + + manager.remove_member(2001, 3001) + manager.remove_member(2001, 3002) + manager.remove_member(2001, 3003) + + room = manager.get_room(2001) assert room is not None assert room.state == CrystalRoomState.BREATHING def test_vote_seal_quorum(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) - sealed, current, needed = manager.vote_seal("chan_1", "user_1") + sealed, current, needed = manager.vote_seal(2001, 3001) assert not sealed assert current == 1 assert needed == 3 - sealed, current, needed = manager.vote_seal("chan_1", "user_2") + sealed, current, needed = manager.vote_seal(2001, 3002) assert not sealed assert current == 2 - sealed, current, needed = manager.vote_seal("chan_1", "user_3") + sealed, current, needed = manager.vote_seal(2001, 3003) assert sealed assert current == 3 - room = manager.get_room("chan_1") + room = manager.get_room(2001) assert room is not None assert room.state == CrystalRoomState.SEALED assert room.sealed_at is not None def test_vote_seal_small_room(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) - sealed, _, needed = manager.vote_seal("chan_1", "user_1") + sealed, _, needed = manager.vote_seal(2001, 3001) assert not sealed assert needed == 2 - sealed, _, _ = manager.vote_seal("chan_1", "user_2") + sealed, _, _ = manager.vote_seal(2001, 3002) assert sealed def test_vote_seal_non_member_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) with pytest.raises(ValueError, match="Only room members"): - manager.vote_seal("chan_1", "user_999") + manager.vote_seal(2001, 9999) def test_vote_seal_already_sealed_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") - manager.vote_seal("chan_1", "user_1") - manager.vote_seal("chan_1", "user_2") - manager.vote_seal("chan_1", "user_3") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) + manager.vote_seal(2001, 3001) + manager.vote_seal(2001, 3002) + manager.vote_seal(2001, 3003) with pytest.raises(ValueError, match="already"): - manager.vote_seal("chan_1", "user_1") + manager.vote_seal(2001, 3001) def test_unseal(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.add_member("chan_1", "user_2") - manager.add_member("chan_1", "user_3") - manager.vote_seal("chan_1", "user_1") - manager.vote_seal("chan_1", "user_2") - manager.vote_seal("chan_1", "user_3") - - room = manager.unseal("chan_1", "user_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.add_member(2001, 3002) + manager.add_member(2001, 3003) + manager.vote_seal(2001, 3001) + manager.vote_seal(2001, 3002) + manager.vote_seal(2001, 3003) + + room = manager.unseal(2001, 3001) assert room.state == CrystalRoomState.OPEN assert room.sealed_at is None def test_unseal_open_room_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) with pytest.raises(ValueError, match="not sealed"): - manager.unseal("chan_1", "user_1") + manager.unseal(2001, 3001) def test_delete_room(self, manager: CrystalRoomManager) -> None: - manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") - manager.delete_room("chan_1") - assert manager.get_room("chan_1") is None + manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.delete_room(2001) + assert manager.get_room(2001) is None def test_delete_nonexistent_room(self, manager: CrystalRoomManager) -> None: - manager.delete_room("nonexistent") + manager.delete_room(9999) diff --git a/tests/test_noosphere/test_engine.py b/tests/test_noosphere/test_engine.py index 4a4fa39..9cdc376 100644 --- a/tests/test_noosphere/test_engine.py +++ b/tests/test_noosphere/test_engine.py @@ -22,7 +22,7 @@ def bot(self) -> MagicMock: @pytest.fixture def engine(self, bot: MagicMock, settings: NoosphereSettings) -> NoosphereEngine: - return NoosphereEngine(bot, "guild_123", settings) + return NoosphereEngine(bot, 123, settings) async def test_initialize(self, engine: NoosphereEngine) -> None: await engine.initialize() @@ -76,7 +76,7 @@ async def test_dormancy_triggers_cryptobiosis( assert engine.is_cryptobiotic bot.dispatch.assert_called_with( "cryptobiosis_trigger", - guild_id="guild_123", + guild_id=123, entering_or_exiting="entering", ) @@ -111,12 +111,12 @@ def bot(self) -> MagicMock: def test_creates_engines_per_guild(self, bot: MagicMock, settings: NoosphereSettings) -> None: cog = NoosphereCog(bot, settings) - engine = cog._get_or_create_engine("guild_1") - assert engine.guild_id == "guild_1" - assert "guild_1" in cog.engines + engine = cog._get_or_create_engine(1) + assert engine.guild_id == 1 + assert 1 in cog.engines def test_reuses_engine(self, bot: MagicMock, settings: NoosphereSettings) -> None: cog = NoosphereCog(bot, settings) - e1 = cog._get_or_create_engine("guild_1") - e2 = cog._get_or_create_engine("guild_1") + e1 = cog._get_or_create_engine(1) + e2 = cog._get_or_create_engine(1) assert e1 is e2 diff --git a/tests/test_noosphere/test_mode_manager.py b/tests/test_noosphere/test_mode_manager.py index 973457e..449c359 100644 --- a/tests/test_noosphere/test_mode_manager.py +++ b/tests/test_noosphere/test_mode_manager.py @@ -7,13 +7,13 @@ class TestModeManager: @pytest.fixture def manager(self) -> ModeManager: - return ModeManager("guild_123") + return ModeManager(123) def test_initial_mode(self, manager: ModeManager) -> None: assert manager.current_mode == ComputationMode.INTEGRATIVE def test_custom_initial_mode(self) -> None: - manager = ModeManager("guild_123", default_mode=ComputationMode.RESONANT) + manager = ModeManager(123, default_mode=ComputationMode.RESONANT) assert manager.current_mode == ComputationMode.RESONANT def test_set_mode(self, manager: ModeManager) -> None: diff --git a/tests/test_noosphere/test_pulse.py b/tests/test_noosphere/test_pulse.py index ae3d377..8256ed4 100644 --- a/tests/test_noosphere/test_pulse.py +++ b/tests/test_noosphere/test_pulse.py @@ -27,27 +27,27 @@ def test_step_increments(self, generator: MorphogeneticPulseGenerator) -> None: assert generator.step == 1 def test_generate_pulse(self, generator: MorphogeneticPulseGenerator) -> None: - pulse = generator.generate_pulse("chan_1") + pulse = generator.generate_pulse(2001) assert isinstance(pulse, Pulse) - assert pulse.target_channel_id == "chan_1" + assert pulse.target_channel_id == 2001 assert pulse.pulse_type in list(PulseType) assert len(pulse.content) > 0 def test_generate_pulse_with_mode_weights(self, generator: MorphogeneticPulseGenerator) -> None: weights = {"crystal": 1.0, "attractor": 0.0, "quasicrystal": 0.0, "ghost": 0.0} - pulse = generator.generate_pulse("chan_1", mode_weights=weights) + pulse = generator.generate_pulse(2001, mode_weights=weights) assert isinstance(pulse, Pulse) def test_generate_pulse_with_topics(self, generator: MorphogeneticPulseGenerator) -> None: pulse = generator.generate_pulse( - "chan_1", + 2001, available_topics=["quantum computing", "neural networks"], ) assert isinstance(pulse, Pulse) def test_generate_pulse_with_questions(self, generator: MorphogeneticPulseGenerator) -> None: pulse = generator.generate_pulse( - "chan_1", + 2001, recent_questions=["What about entropy?"], ) assert isinstance(pulse, Pulse) @@ -55,6 +55,6 @@ def test_generate_pulse_with_questions(self, generator: MorphogeneticPulseGenera def test_pulse_types_selected_by_weights(self, generator: MorphogeneticPulseGenerator) -> None: type_counts: dict[PulseType, int] = dict.fromkeys(PulseType, 0) for _ in range(100): - pulse = generator.generate_pulse("chan_1") + pulse = generator.generate_pulse(2001) type_counts[pulse.pulse_type] += 1 assert all(count > 0 for count in type_counts.values()) From 4ce61f4fb798d41c666965684140edb56ed9a101 Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 15:06:52 -0500 Subject: [PATCH 3/6] Align with dev-foundation PR #187 canonical shared modules - constants.py: Match foundation's enum.Enum base (not str, Enum), add MessageClassification and Phase 0 constants (embedding, archive, output governor), keep Crystal Room enums as additive-only additions - shared/phi_parameter.py: Replace with foundation's canonical version (no tick_count or set_phase -- engine tracks its own tick count) - test_constants.py: Keep only Crystal Room enum tests, defer shared constant/enum tests to foundation - test_phi_parameter.py: Remove tick_count and set_phase tests - test_engine.py: Use engine._tick_count instead of phi.tick_count - test_serendipity.py: Fix flaky range test by using zero-noise injector --- src/intelstream/noosphere/constants.py | 31 +++++++++++++++--- .../noosphere/shared/phi_parameter.py | 19 ++++------- tests/test_noosphere/test_constants.py | 32 ++----------------- tests/test_noosphere/test_engine.py | 12 +++---- tests/test_noosphere/test_phi_parameter.py | 22 ------------- tests/test_noosphere/test_serendipity.py | 8 +++-- 6 files changed, 46 insertions(+), 78 deletions(-) diff --git a/src/intelstream/noosphere/constants.py b/src/intelstream/noosphere/constants.py index 5053647..a4776c7 100644 --- a/src/intelstream/noosphere/constants.py +++ b/src/intelstream/noosphere/constants.py @@ -1,24 +1,39 @@ +import enum import math -from enum import Enum PHI: float = (1 + math.sqrt(5)) / 2 GOLDEN_ANGLE: float = 2 * math.pi / (PHI**2) + FIBONACCI_SEQ: list[int] = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] +EMBEDDING_MODEL_MULTILINGUAL: str = "paraphrase-multilingual-MiniLM-L12-v2" +EMBEDDING_MODEL_ENGLISH: str = "all-MiniLM-L6-v2" +EMBEDDING_DIM: int = 384 + +MIN_MSG_THRESHOLD: int = 20 +OUTPUT_GAIN_RECOMPUTE_INTERVAL: float = 300.0 +HARD_COOLDOWN_SECONDS: float = 30.0 +DEFAULT_ANTHROPHONY_THRESHOLD: float = 0.15 +DEFAULT_GAIN_RATIO: float = 4.0 + +ARCHIVE_BASE_HALF_LIFE_HOURS: float = 168.0 +ARCHIVE_REFERENCE_EXTENSION: float = 1.5 +ARCHIVE_FIDELITY_FLOOR: float = 0.01 -class CrystalRoomMode(str, Enum): + +class CrystalRoomMode(enum.Enum): NUMBER_STATION = "number_station" WHALE = "whale" GHOST = "ghost" -class CrystalRoomState(str, Enum): +class CrystalRoomState(enum.Enum): OPEN = "open" SEALED = "sealed" BREATHING = "breathing" -class ComputationMode(str, Enum): +class ComputationMode(enum.Enum): SUBTRACTIVE = "subtractive" BROADCAST = "broadcast" RESONANT = "resonant" @@ -31,7 +46,7 @@ class ComputationMode(str, Enum): TOPOLOGICAL = "topological" -class PathologyType(str, Enum): +class PathologyType(enum.Enum): CANCER = "non_terminating_pruning" CYTOKINE_STORM = "receiver_saturation" SEIZURE = "destructive_sync" @@ -42,3 +57,9 @@ class PathologyType(str, Enum): COMA = "irreversible_suspension" MISUNDERSTANDING = "irrecoverable_dim_loss" SCHISM = "topological_damage" + + +class MessageClassification(enum.Enum): + ANTHROPHONY = "anthrophony" + BIOPHONY = "biophony" + GEOPHONY = "geophony" diff --git a/src/intelstream/noosphere/shared/phi_parameter.py b/src/intelstream/noosphere/shared/phi_parameter.py index ea91360..7917719 100644 --- a/src/intelstream/noosphere/shared/phi_parameter.py +++ b/src/intelstream/noosphere/shared/phi_parameter.py @@ -28,19 +28,13 @@ class PhiParameter: def __init__(self) -> None: self._phase = 0.0 - self._tick_count = 0 @property def phase(self) -> float: return self._phase - @property - def tick_count(self) -> int: - return self._tick_count - def advance(self) -> None: self._phase = (self._phase + GOLDEN_ANGLE) % (2 * math.pi) - self._tick_count += 1 def mode_weights(self) -> dict[str, float]: proximity = self._fibonacci_proximity() @@ -49,8 +43,13 @@ def mode_weights(self) -> dict[str, float]: attractor_w = math.sin(self._phase) * 0.3 + 0.3 ghost_w = math.cos(self._phase * PHI) * 0.2 + 0.2 total = crystal_w + quasicrystal_w + attractor_w + ghost_w - if total <= 0: - return {"crystal": 0.25, "attractor": 0.25, "quasicrystal": 0.25, "ghost": 0.25} + if total < 1e-10: + return { + "crystal": 0.25, + "attractor": 0.25, + "quasicrystal": 0.25, + "ghost": 0.25, + } return { "crystal": crystal_w / total, "attractor": attractor_w / total, @@ -59,7 +58,6 @@ def mode_weights(self) -> dict[str, float]: } def _fibonacci_proximity(self) -> float: - """How close the current phase is to any Fibonacci fraction of 2*pi.""" min_dist = float("inf") for p, q in self.FIBONACCI_FRACTIONS: frac_phase = (2 * math.pi * p / q) % (2 * math.pi) @@ -69,6 +67,3 @@ def _fibonacci_proximity(self) -> float: ) min_dist = min(min_dist, dist) return 1.0 - (min_dist / math.pi) - - def set_phase(self, phase: float) -> None: - self._phase = phase % (2 * math.pi) diff --git a/tests/test_noosphere/test_constants.py b/tests/test_noosphere/test_constants.py index e29650e..e21f50e 100644 --- a/tests/test_noosphere/test_constants.py +++ b/tests/test_noosphere/test_constants.py @@ -1,29 +1,7 @@ -import math +from intelstream.noosphere.constants import CrystalRoomMode, CrystalRoomState -from intelstream.noosphere.constants import ( - FIBONACCI_SEQ, - GOLDEN_ANGLE, - PHI, - ComputationMode, - CrystalRoomMode, - CrystalRoomState, - PathologyType, -) - - -class TestConstants: - def test_phi_value(self) -> None: - assert abs(PHI - 1.618033988749895) < 1e-10 - - def test_golden_angle_value(self) -> None: - expected = 2 * math.pi / (PHI**2) - assert abs(GOLDEN_ANGLE - expected) < 1e-10 - - def test_fibonacci_sequence(self) -> None: - assert FIBONACCI_SEQ == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] - for i in range(2, len(FIBONACCI_SEQ)): - assert FIBONACCI_SEQ[i] == FIBONACCI_SEQ[i - 1] + FIBONACCI_SEQ[i - 2] +class TestCrystalRoomConstants: def test_crystal_room_modes(self) -> None: assert CrystalRoomMode.NUMBER_STATION.value == "number_station" assert CrystalRoomMode.WHALE.value == "whale" @@ -33,9 +11,3 @@ def test_crystal_room_states(self) -> None: assert CrystalRoomState.OPEN.value == "open" assert CrystalRoomState.SEALED.value == "sealed" assert CrystalRoomState.BREATHING.value == "breathing" - - def test_computation_modes_count(self) -> None: - assert len(ComputationMode) == 10 - - def test_pathology_types_count(self) -> None: - assert len(PathologyType) == 10 diff --git a/tests/test_noosphere/test_engine.py b/tests/test_noosphere/test_engine.py index 9cdc376..2ef409a 100644 --- a/tests/test_noosphere/test_engine.py +++ b/tests/test_noosphere/test_engine.py @@ -29,9 +29,9 @@ async def test_initialize(self, engine: NoosphereEngine) -> None: assert engine.is_active async def test_tick_advances_phi(self, engine: NoosphereEngine) -> None: - initial_tick = engine.phi.tick_count + initial_tick = engine._tick_count await engine.tick() - assert engine.phi.tick_count == initial_tick + 1 + assert engine._tick_count == initial_tick + 1 async def test_tick_dispatches_event(self, engine: NoosphereEngine, bot: MagicMock) -> None: await engine.tick() @@ -39,15 +39,15 @@ async def test_tick_dispatches_event(self, engine: NoosphereEngine, bot: MagicMo async def test_tick_inactive_does_nothing(self, engine: NoosphereEngine) -> None: await engine.shutdown() - initial_tick = engine.phi.tick_count + initial_tick = engine._tick_count await engine.tick() - assert engine.phi.tick_count == initial_tick + assert engine._tick_count == initial_tick async def test_tick_cryptobiotic_does_nothing(self, engine: NoosphereEngine) -> None: engine._is_cryptobiotic = True - initial_tick = engine.phi.tick_count + initial_tick = engine._tick_count await engine.tick() - assert engine.phi.tick_count == initial_tick + assert engine._tick_count == initial_tick async def test_process_message_updates_activity(self, engine: NoosphereEngine) -> None: message = MagicMock() diff --git a/tests/test_noosphere/test_phi_parameter.py b/tests/test_noosphere/test_phi_parameter.py index c96d38e..c0ea1e5 100644 --- a/tests/test_noosphere/test_phi_parameter.py +++ b/tests/test_noosphere/test_phi_parameter.py @@ -13,12 +13,10 @@ def phi(self) -> PhiParameter: def test_initial_state(self, phi: PhiParameter) -> None: assert phi.phase == 0.0 - assert phi.tick_count == 0 def test_advance(self, phi: PhiParameter) -> None: phi.advance() assert abs(phi.phase - GOLDEN_ANGLE) < 1e-10 - assert phi.tick_count == 1 def test_phase_wraps(self, phi: PhiParameter) -> None: for _ in range(100): @@ -42,23 +40,3 @@ def test_mode_weights_all_positive(self, phi: PhiParameter) -> None: weights = phi.mode_weights() for w in weights.values(): assert w >= 0 - - def test_set_phase(self, phi: PhiParameter) -> None: - phi.set_phase(math.pi) - assert abs(phi.phase - math.pi) < 1e-10 - - def test_set_phase_wraps(self, phi: PhiParameter) -> None: - phi.set_phase(3 * math.pi) - assert phi.phase < 2 * math.pi - - def test_fibonacci_proximity_near_fraction(self, phi: PhiParameter) -> None: - phi.set_phase(0.0) - proximity = phi._fibonacci_proximity() - assert proximity > 0.5 - - def test_weights_vary_with_phase(self, phi: PhiParameter) -> None: - phi.set_phase(0.0) - w1 = phi.mode_weights() - phi.set_phase(math.pi / 3) - w2 = phi.mode_weights() - assert w1 != w2 diff --git a/tests/test_noosphere/test_serendipity.py b/tests/test_noosphere/test_serendipity.py index 4f15dac..a675465 100644 --- a/tests/test_noosphere/test_serendipity.py +++ b/tests/test_noosphere/test_serendipity.py @@ -42,13 +42,15 @@ def test_find_bridges_limits_to_three(self, injector: SerendipityInjector) -> No bridges = injector.find_bridges(current, archived, similarities=similarities) assert len(bridges) <= 3 - def test_find_bridges_respects_range(self, injector: SerendipityInjector) -> None: + def test_find_bridges_respects_range(self) -> None: + noiseless = SerendipityInjector(noise_sigma=0.0, similarity_min=0.3, similarity_max=0.6) + too_similar = {("a", "b"): 0.9} - bridges = injector.find_bridges(["a"], ["b"], similarities=too_similar) + bridges = noiseless.find_bridges(["a"], ["b"], similarities=too_similar) assert len(bridges) == 0 too_different = {("a", "b"): 0.05} - bridges = injector.find_bridges(["a"], ["b"], similarities=too_different) + bridges = noiseless.find_bridges(["a"], ["b"], similarities=too_different) assert len(bridges) == 0 def test_select_injection(self, injector: SerendipityInjector) -> None: From b5eb34ca31828c7fe736cd5de69e749f48a29af5 Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 15:08:37 -0500 Subject: [PATCH 4/6] Revert Discord IDs back to str (agreed convention) Reverts the int ID change from 30e3db0. The team convention is str for all Discord IDs. dev-foundation is being asked to align to str. --- src/intelstream/noosphere/crystal_room/cog.py | 47 ++--- .../noosphere/crystal_room/manager.py | 34 ++-- src/intelstream/noosphere/engine.py | 16 +- .../noosphere/morphogenetic_field/cog.py | 6 +- .../noosphere/morphogenetic_field/pulse.py | 4 +- .../noosphere/shared/mode_manager.py | 14 +- tests/test_noosphere/test_crystal_room.py | 184 +++++++++--------- tests/test_noosphere/test_engine.py | 14 +- tests/test_noosphere/test_mode_manager.py | 4 +- tests/test_noosphere/test_pulse.py | 12 +- 10 files changed, 164 insertions(+), 171 deletions(-) diff --git a/src/intelstream/noosphere/crystal_room/cog.py b/src/intelstream/noosphere/crystal_room/cog.py index 6b8f8c4..5c7f6a4 100644 --- a/src/intelstream/noosphere/crystal_room/cog.py +++ b/src/intelstream/noosphere/crystal_room/cog.py @@ -67,10 +67,10 @@ async def crystal_create( ) self.manager.create_room( - guild_id=interaction.guild.id, - channel_id=channel.id, + guild_id=str(interaction.guild.id), + channel_id=str(channel.id), mode=room_mode, - creator_id=interaction.user.id, + creator_id=str(interaction.user.id), ) await interaction.followup.send( @@ -87,17 +87,13 @@ async def crystal_create( @crystal.command(name="join", description="Join an existing Crystal Room") async def crystal_join(self, interaction: discord.Interaction) -> None: - if ( - not interaction.guild - or not isinstance(interaction.user, discord.Member) - or not interaction.channel_id - ): + if not interaction.guild or not isinstance(interaction.user, discord.Member): await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = interaction.channel_id + channel_id = str(interaction.channel_id) room = self.manager.get_room(channel_id) if room is None: @@ -107,12 +103,12 @@ async def crystal_join(self, interaction: discord.Interaction) -> None: return try: - self.manager.add_member(channel_id, interaction.user.id) + self.manager.add_member(channel_id, str(interaction.user.id)) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return - channel = interaction.guild.get_channel(channel_id) + channel = interaction.guild.get_channel(int(channel_id)) if isinstance(channel, discord.TextChannel): await grant_access(channel, interaction.user) @@ -122,28 +118,28 @@ async def crystal_join(self, interaction: discord.Interaction) -> None: @crystal.command(name="seal", description="Vote to seal the Crystal Room") async def crystal_seal(self, interaction: discord.Interaction) -> None: - if not interaction.guild or not interaction.channel_id: + if not interaction.guild: await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = interaction.channel_id + channel_id = str(interaction.channel_id) try: - sealed, current, needed = self.manager.vote_seal(channel_id, interaction.user.id) + sealed, current, needed = self.manager.vote_seal(channel_id, str(interaction.user.id)) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return if sealed: - channel = interaction.guild.get_channel(channel_id) + channel = interaction.guild.get_channel(int(channel_id)) if isinstance(channel, discord.TextChannel): await set_sealed_permissions(channel) self.bot.dispatch( "crystal_state_change", - guild_id=interaction.guild.id, + guild_id=str(interaction.guild.id), channel_id=channel_id, new_state=CrystalRoomState.SEALED.value, ) @@ -158,27 +154,27 @@ async def crystal_seal(self, interaction: discord.Interaction) -> None: @crystal.command(name="unseal", description="Unseal the Crystal Room") async def crystal_unseal(self, interaction: discord.Interaction) -> None: - if not interaction.guild or not interaction.channel_id: + if not interaction.guild: await interaction.response.send_message( "This command must be used in a server.", ephemeral=True ) return - channel_id = interaction.channel_id + channel_id = str(interaction.channel_id) try: - self.manager.unseal(channel_id, interaction.user.id) + self.manager.unseal(channel_id, str(interaction.user.id)) except ValueError as e: await interaction.response.send_message(str(e), ephemeral=True) return - channel = interaction.guild.get_channel(channel_id) + channel = interaction.guild.get_channel(int(channel_id)) if isinstance(channel, discord.TextChannel): await set_open_permissions(channel) self.bot.dispatch( "crystal_state_change", - guild_id=interaction.guild.id, + guild_id=str(interaction.guild.id), channel_id=channel_id, new_state=CrystalRoomState.OPEN.value, ) @@ -187,13 +183,8 @@ async def crystal_unseal(self, interaction: discord.Interaction) -> None: @crystal.command(name="status", description="Show Crystal Room status") async def crystal_status(self, interaction: discord.Interaction) -> None: - if not interaction.channel_id: - await interaction.response.send_message( - "This command must be used in a channel.", ephemeral=True - ) - return - - room = self.manager.get_room(interaction.channel_id) + channel_id = str(interaction.channel_id) + room = self.manager.get_room(channel_id) if room is None: await interaction.response.send_message( diff --git a/src/intelstream/noosphere/crystal_room/manager.py b/src/intelstream/noosphere/crystal_room/manager.py index 569e2d2..061794a 100644 --- a/src/intelstream/noosphere/crystal_room/manager.py +++ b/src/intelstream/noosphere/crystal_room/manager.py @@ -12,14 +12,14 @@ @dataclass class CrystalRoomInfo: - guild_id: int - channel_id: int + guild_id: str + channel_id: str mode: CrystalRoomMode state: CrystalRoomState - member_ids: list[int] + member_ids: list[str] created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) sealed_at: datetime | None = None - sealed_by: list[int] = field(default_factory=list) + sealed_by: list[str] = field(default_factory=list) class CrystalRoomManager: @@ -30,24 +30,24 @@ class CrystalRoomManager: """ def __init__(self, seal_quorum: int = 3, max_rooms_per_guild: int = 5): - self._rooms: dict[int, CrystalRoomInfo] = {} + self._rooms: dict[str, CrystalRoomInfo] = {} self._seal_quorum = seal_quorum self._max_rooms_per_guild = max_rooms_per_guild - self._seal_votes: dict[int, set[int]] = {} + self._seal_votes: dict[str, set[str]] = {} @property - def rooms(self) -> dict[int, CrystalRoomInfo]: + def rooms(self) -> dict[str, CrystalRoomInfo]: return dict(self._rooms) - def guild_room_count(self, guild_id: int) -> int: + def guild_room_count(self, guild_id: str) -> int: return sum(1 for r in self._rooms.values() if r.guild_id == guild_id) def create_room( self, - guild_id: int, - channel_id: int, + guild_id: str, + channel_id: str, mode: CrystalRoomMode, - creator_id: int, + creator_id: str, ) -> CrystalRoomInfo: if self.guild_room_count(guild_id) >= self._max_rooms_per_guild: raise ValueError( @@ -76,10 +76,10 @@ def create_room( ) return room - def get_room(self, channel_id: int) -> CrystalRoomInfo | None: + def get_room(self, channel_id: str) -> CrystalRoomInfo | None: return self._rooms.get(channel_id) - def add_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: + def add_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -99,7 +99,7 @@ def add_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: return room - def remove_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: + def remove_member(self, channel_id: str, user_id: str) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -116,7 +116,7 @@ def remove_member(self, channel_id: int, user_id: int) -> CrystalRoomInfo: return room - def vote_seal(self, channel_id: int, user_id: int) -> tuple[bool, int, int]: + def vote_seal(self, channel_id: str, user_id: str) -> tuple[bool, int, int]: """Vote to seal a room. Returns (sealed, current_votes, needed).""" room = self._rooms.get(channel_id) if room is None: @@ -148,7 +148,7 @@ def vote_seal(self, channel_id: int, user_id: int) -> tuple[bool, int, int]: return False, current, needed - def unseal(self, channel_id: int, user_id: int) -> CrystalRoomInfo: + def unseal(self, channel_id: str, user_id: str) -> CrystalRoomInfo: room = self._rooms.get(channel_id) if room is None: raise ValueError(f"No room for channel {channel_id}") @@ -167,6 +167,6 @@ def unseal(self, channel_id: int, user_id: int) -> CrystalRoomInfo: logger.info("Room unsealed", channel_id=channel_id, unsealed_by=user_id) return room - def delete_room(self, channel_id: int) -> None: + def delete_room(self, channel_id: str) -> None: self._rooms.pop(channel_id, None) self._seal_votes.pop(channel_id, None) diff --git a/src/intelstream/noosphere/engine.py b/src/intelstream/noosphere/engine.py index dd21414..dfa1114 100644 --- a/src/intelstream/noosphere/engine.py +++ b/src/intelstream/noosphere/engine.py @@ -23,7 +23,7 @@ class NoosphereEngine: and handles mode transitions. One instance per guild. """ - def __init__(self, bot: commands.Bot, guild_id: int, settings: NoosphereSettings): + def __init__(self, bot: commands.Bot, guild_id: str, settings: NoosphereSettings): self.bot = bot self.guild_id = guild_id self.settings = settings @@ -79,8 +79,8 @@ async def process_message(self, message: discord.Message) -> None: self.bot.dispatch( "message_processed", guild_id=self.guild_id, - channel_id=message.channel.id, - author_id=message.author.id, + channel_id=str(message.channel.id), + author_id=str(message.author.id), content=message.content, ) @@ -125,7 +125,7 @@ class NoosphereCog(commands.Cog, name="Noosphere"): def __init__(self, bot: commands.Bot, settings: NoosphereSettings | None = None) -> None: self.bot = bot self.settings = settings or NoosphereSettings() - self.engines: dict[int, NoosphereEngine] = {} + self.engines: dict[str, NoosphereEngine] = {} self._tick_task_running = False async def cog_load(self) -> None: @@ -161,7 +161,7 @@ async def _load_sub_cogs(self) -> None: except Exception: logger.exception("Failed to load noosphere sub-cog", cog=type(cog).__name__) - def _get_or_create_engine(self, guild_id: int) -> NoosphereEngine: + def _get_or_create_engine(self, guild_id: str) -> NoosphereEngine: if guild_id not in self.engines: engine = NoosphereEngine(self.bot, guild_id, self.settings) self.engines[guild_id] = engine @@ -190,7 +190,8 @@ async def on_message(self, message: discord.Message) -> None: return if not self.settings.enabled: return - engine = self._get_or_create_engine(message.guild.id) + guild_id = str(message.guild.id) + engine = self._get_or_create_engine(guild_id) await engine.process_message(message) @commands.Cog.listener() @@ -198,6 +199,7 @@ async def on_ready(self) -> None: if not self.settings.enabled: return for guild in self.bot.guilds: - engine = self._get_or_create_engine(guild.id) + guild_id = str(guild.id) + engine = self._get_or_create_engine(guild_id) await engine.initialize() logger.info("NoosphereEngine initialized for all guilds", count=len(self.engines)) diff --git a/src/intelstream/noosphere/morphogenetic_field/cog.py b/src/intelstream/noosphere/morphogenetic_field/cog.py index 45b372e..8a559e7 100644 --- a/src/intelstream/noosphere/morphogenetic_field/cog.py +++ b/src/intelstream/noosphere/morphogenetic_field/cog.py @@ -49,13 +49,13 @@ async def manual_pulse(self, interaction: discord.Interaction) -> None: return pulse = self.pulse_generator.generate_pulse( - channel_id=interaction.channel_id, + channel_id=str(interaction.channel_id), ) self.bot.dispatch( "pulse_fired", - guild_id=interaction.guild.id, - channel_id=interaction.channel_id, + guild_id=str(interaction.guild.id), + channel_id=str(interaction.channel_id), content=pulse.content, ) diff --git a/src/intelstream/noosphere/morphogenetic_field/pulse.py b/src/intelstream/noosphere/morphogenetic_field/pulse.py index 7731f3a..aa72510 100644 --- a/src/intelstream/noosphere/morphogenetic_field/pulse.py +++ b/src/intelstream/noosphere/morphogenetic_field/pulse.py @@ -23,7 +23,7 @@ class PulseType(str, Enum): class Pulse: pulse_type: PulseType content: str - target_channel_id: int + target_channel_id: str timestamp: datetime = field(default_factory=lambda: datetime.now(UTC)) @@ -52,7 +52,7 @@ def next_interval_minutes(self) -> float: def generate_pulse( self, - channel_id: int, + channel_id: str, mode_weights: dict[str, float] | None = None, available_topics: list[str] | None = None, recent_questions: list[str] | None = None, diff --git a/src/intelstream/noosphere/shared/mode_manager.py b/src/intelstream/noosphere/shared/mode_manager.py index 0d6fd34..5074c1d 100644 --- a/src/intelstream/noosphere/shared/mode_manager.py +++ b/src/intelstream/noosphere/shared/mode_manager.py @@ -27,7 +27,7 @@ class ModeManager: Phase 4: automatic transitions driven by pathology detection. """ - def __init__(self, guild_id: int, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): + def __init__(self, guild_id: str, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): self.guild_id = guild_id self._current_mode = default_mode self._history: list[ModeTransition] = [] @@ -97,9 +97,9 @@ class ModeManagerCog(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self._managers: dict[int, ModeManager] = {} + self._managers: dict[str, ModeManager] = {} - def get_manager(self, guild_id: int) -> ModeManager: + def get_manager(self, guild_id: str) -> ModeManager: if guild_id not in self._managers: self._managers[guild_id] = ModeManager(guild_id) return self._managers[guild_id] @@ -110,7 +110,7 @@ async def mode_status(self, interaction: discord.Interaction) -> None: await interaction.response.send_message("This command must be used in a server.") return - manager = self.get_manager(interaction.guild.id) + manager = self.get_manager(str(interaction.guild.id)) mode = manager.current_mode description = manager.get_mode_description() @@ -141,12 +141,12 @@ async def mode_set(self, interaction: discord.Interaction, mode: str) -> None: ) return - manager = self.get_manager(interaction.guild.id) + manager = self.get_manager(str(interaction.guild.id)) transition = manager.set_mode(new_mode, reason=f"manual by {interaction.user}") self.bot.dispatch( "mode_transition", - guild_id=interaction.guild.id, + guild_id=str(interaction.guild.id), old_mode=transition.old_mode.value, new_mode=transition.new_mode.value, reason=transition.reason, @@ -162,7 +162,7 @@ async def mode_history(self, interaction: discord.Interaction) -> None: await interaction.response.send_message("This command must be used in a server.") return - manager = self.get_manager(interaction.guild.id) + manager = self.get_manager(str(interaction.guild.id)) history = manager.history if not history: diff --git a/tests/test_noosphere/test_crystal_room.py b/tests/test_noosphere/test_crystal_room.py index 2d606c4..309e0f5 100644 --- a/tests/test_noosphere/test_crystal_room.py +++ b/tests/test_noosphere/test_crystal_room.py @@ -10,178 +10,178 @@ def manager(self) -> CrystalRoomManager: return CrystalRoomManager(seal_quorum=3, max_rooms_per_guild=5) def test_create_room(self, manager: CrystalRoomManager) -> None: - room = manager.create_room(1001, 2001, CrystalRoomMode.NUMBER_STATION, 3001) - assert room.guild_id == 1001 - assert room.channel_id == 2001 + room = manager.create_room("guild_1", "chan_1", CrystalRoomMode.NUMBER_STATION, "user_1") + assert room.guild_id == "guild_1" + assert room.channel_id == "chan_1" assert room.mode == CrystalRoomMode.NUMBER_STATION assert room.state == CrystalRoomState.OPEN - assert 3001 in room.member_ids + assert "user_1" in room.member_ids def test_create_duplicate_room_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") with pytest.raises(ValueError, match="already exists"): - manager.create_room(1001, 2001, CrystalRoomMode.GHOST, 3002) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_2") def test_max_rooms_per_guild(self, manager: CrystalRoomManager) -> None: for i in range(5): - manager.create_room(1001, 2000 + i, CrystalRoomMode.WHALE, 3001) + manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") with pytest.raises(ValueError, match="Maximum rooms"): - manager.create_room(1001, 2099, CrystalRoomMode.WHALE, 3001) + manager.create_room("guild_1", "chan_extra", CrystalRoomMode.WHALE, "user_1") def test_max_rooms_per_guild_different_guilds(self, manager: CrystalRoomManager) -> None: for i in range(5): - manager.create_room(1001, 2000 + i, CrystalRoomMode.WHALE, 3001) - room = manager.create_room(1002, 2099, CrystalRoomMode.WHALE, 3001) - assert room.guild_id == 1002 + manager.create_room("guild_1", f"chan_{i}", CrystalRoomMode.WHALE, "user_1") + room = manager.create_room("guild_2", "chan_other", CrystalRoomMode.WHALE, "user_1") + assert room.guild_id == "guild_2" def test_get_room(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.GHOST, 3001) - room = manager.get_room(2001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.GHOST, "user_1") + room = manager.get_room("chan_1") assert room is not None assert room.mode == CrystalRoomMode.GHOST def test_get_nonexistent_room(self, manager: CrystalRoomManager) -> None: - assert manager.get_room(9999) is None + assert manager.get_room("nonexistent") is None def test_add_member(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - room = manager.add_member(2001, 3002) - assert 3002 in room.member_ids + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + room = manager.add_member("chan_1", "user_2") + assert "user_2" in room.member_ids def test_add_member_idempotent(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3001) - room = manager.get_room(2001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_1") + room = manager.get_room("chan_1") assert room is not None - assert room.member_ids.count(3001) == 1 + assert room.member_ids.count("user_1") == 1 def test_add_member_to_sealed_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) - manager.vote_seal(2001, 3001) - manager.vote_seal(2001, 3002) - manager.vote_seal(2001, 3003) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") with pytest.raises(ValueError, match="sealed"): - manager.add_member(2001, 3004) + manager.add_member("chan_1", "user_4") def test_add_member_to_breathing_reopens(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) - manager.vote_seal(2001, 3001) - manager.vote_seal(2001, 3002) - manager.vote_seal(2001, 3003) - - manager.remove_member(2001, 3001) - manager.remove_member(2001, 3002) - manager.remove_member(2001, 3003) - - room = manager.get_room(2001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + manager.remove_member("chan_1", "user_1") + manager.remove_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_3") + + room = manager.get_room("chan_1") assert room is not None assert room.state == CrystalRoomState.BREATHING - room = manager.add_member(2001, 3004) + room = manager.add_member("chan_1", "user_4") assert room.state == CrystalRoomState.OPEN def test_remove_member(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.remove_member(2001, 3002) - room = manager.get_room(2001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_2") + room = manager.get_room("chan_1") assert room is not None - assert 3002 not in room.member_ids + assert "user_2" not in room.member_ids def test_remove_all_members_from_sealed_enters_breathing( self, manager: CrystalRoomManager ) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) - manager.vote_seal(2001, 3001) - manager.vote_seal(2001, 3002) - manager.vote_seal(2001, 3003) - - manager.remove_member(2001, 3001) - manager.remove_member(2001, 3002) - manager.remove_member(2001, 3003) - - room = manager.get_room(2001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + manager.remove_member("chan_1", "user_1") + manager.remove_member("chan_1", "user_2") + manager.remove_member("chan_1", "user_3") + + room = manager.get_room("chan_1") assert room is not None assert room.state == CrystalRoomState.BREATHING def test_vote_seal_quorum(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") - sealed, current, needed = manager.vote_seal(2001, 3001) + sealed, current, needed = manager.vote_seal("chan_1", "user_1") assert not sealed assert current == 1 assert needed == 3 - sealed, current, needed = manager.vote_seal(2001, 3002) + sealed, current, needed = manager.vote_seal("chan_1", "user_2") assert not sealed assert current == 2 - sealed, current, needed = manager.vote_seal(2001, 3003) + sealed, current, needed = manager.vote_seal("chan_1", "user_3") assert sealed assert current == 3 - room = manager.get_room(2001) + room = manager.get_room("chan_1") assert room is not None assert room.state == CrystalRoomState.SEALED assert room.sealed_at is not None def test_vote_seal_small_room(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") - sealed, _, needed = manager.vote_seal(2001, 3001) + sealed, _, needed = manager.vote_seal("chan_1", "user_1") assert not sealed assert needed == 2 - sealed, _, _ = manager.vote_seal(2001, 3002) + sealed, _, _ = manager.vote_seal("chan_1", "user_2") assert sealed def test_vote_seal_non_member_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") with pytest.raises(ValueError, match="Only room members"): - manager.vote_seal(2001, 9999) + manager.vote_seal("chan_1", "user_999") def test_vote_seal_already_sealed_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) - manager.vote_seal(2001, 3001) - manager.vote_seal(2001, 3002) - manager.vote_seal(2001, 3003) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") with pytest.raises(ValueError, match="already"): - manager.vote_seal(2001, 3001) + manager.vote_seal("chan_1", "user_1") def test_unseal(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.add_member(2001, 3002) - manager.add_member(2001, 3003) - manager.vote_seal(2001, 3001) - manager.vote_seal(2001, 3002) - manager.vote_seal(2001, 3003) - - room = manager.unseal(2001, 3001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.add_member("chan_1", "user_2") + manager.add_member("chan_1", "user_3") + manager.vote_seal("chan_1", "user_1") + manager.vote_seal("chan_1", "user_2") + manager.vote_seal("chan_1", "user_3") + + room = manager.unseal("chan_1", "user_1") assert room.state == CrystalRoomState.OPEN assert room.sealed_at is None def test_unseal_open_room_raises(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") with pytest.raises(ValueError, match="not sealed"): - manager.unseal(2001, 3001) + manager.unseal("chan_1", "user_1") def test_delete_room(self, manager: CrystalRoomManager) -> None: - manager.create_room(1001, 2001, CrystalRoomMode.WHALE, 3001) - manager.delete_room(2001) - assert manager.get_room(2001) is None + manager.create_room("guild_1", "chan_1", CrystalRoomMode.WHALE, "user_1") + manager.delete_room("chan_1") + assert manager.get_room("chan_1") is None def test_delete_nonexistent_room(self, manager: CrystalRoomManager) -> None: - manager.delete_room(9999) + manager.delete_room("nonexistent") diff --git a/tests/test_noosphere/test_engine.py b/tests/test_noosphere/test_engine.py index 2ef409a..5ee0e22 100644 --- a/tests/test_noosphere/test_engine.py +++ b/tests/test_noosphere/test_engine.py @@ -22,7 +22,7 @@ def bot(self) -> MagicMock: @pytest.fixture def engine(self, bot: MagicMock, settings: NoosphereSettings) -> NoosphereEngine: - return NoosphereEngine(bot, 123, settings) + return NoosphereEngine(bot, "guild_123", settings) async def test_initialize(self, engine: NoosphereEngine) -> None: await engine.initialize() @@ -76,7 +76,7 @@ async def test_dormancy_triggers_cryptobiosis( assert engine.is_cryptobiotic bot.dispatch.assert_called_with( "cryptobiosis_trigger", - guild_id=123, + guild_id="guild_123", entering_or_exiting="entering", ) @@ -111,12 +111,12 @@ def bot(self) -> MagicMock: def test_creates_engines_per_guild(self, bot: MagicMock, settings: NoosphereSettings) -> None: cog = NoosphereCog(bot, settings) - engine = cog._get_or_create_engine(1) - assert engine.guild_id == 1 - assert 1 in cog.engines + engine = cog._get_or_create_engine("guild_1") + assert engine.guild_id == "guild_1" + assert "guild_1" in cog.engines def test_reuses_engine(self, bot: MagicMock, settings: NoosphereSettings) -> None: cog = NoosphereCog(bot, settings) - e1 = cog._get_or_create_engine(1) - e2 = cog._get_or_create_engine(1) + e1 = cog._get_or_create_engine("guild_1") + e2 = cog._get_or_create_engine("guild_1") assert e1 is e2 diff --git a/tests/test_noosphere/test_mode_manager.py b/tests/test_noosphere/test_mode_manager.py index 449c359..973457e 100644 --- a/tests/test_noosphere/test_mode_manager.py +++ b/tests/test_noosphere/test_mode_manager.py @@ -7,13 +7,13 @@ class TestModeManager: @pytest.fixture def manager(self) -> ModeManager: - return ModeManager(123) + return ModeManager("guild_123") def test_initial_mode(self, manager: ModeManager) -> None: assert manager.current_mode == ComputationMode.INTEGRATIVE def test_custom_initial_mode(self) -> None: - manager = ModeManager(123, default_mode=ComputationMode.RESONANT) + manager = ModeManager("guild_123", default_mode=ComputationMode.RESONANT) assert manager.current_mode == ComputationMode.RESONANT def test_set_mode(self, manager: ModeManager) -> None: diff --git a/tests/test_noosphere/test_pulse.py b/tests/test_noosphere/test_pulse.py index 8256ed4..ae3d377 100644 --- a/tests/test_noosphere/test_pulse.py +++ b/tests/test_noosphere/test_pulse.py @@ -27,27 +27,27 @@ def test_step_increments(self, generator: MorphogeneticPulseGenerator) -> None: assert generator.step == 1 def test_generate_pulse(self, generator: MorphogeneticPulseGenerator) -> None: - pulse = generator.generate_pulse(2001) + pulse = generator.generate_pulse("chan_1") assert isinstance(pulse, Pulse) - assert pulse.target_channel_id == 2001 + assert pulse.target_channel_id == "chan_1" assert pulse.pulse_type in list(PulseType) assert len(pulse.content) > 0 def test_generate_pulse_with_mode_weights(self, generator: MorphogeneticPulseGenerator) -> None: weights = {"crystal": 1.0, "attractor": 0.0, "quasicrystal": 0.0, "ghost": 0.0} - pulse = generator.generate_pulse(2001, mode_weights=weights) + pulse = generator.generate_pulse("chan_1", mode_weights=weights) assert isinstance(pulse, Pulse) def test_generate_pulse_with_topics(self, generator: MorphogeneticPulseGenerator) -> None: pulse = generator.generate_pulse( - 2001, + "chan_1", available_topics=["quantum computing", "neural networks"], ) assert isinstance(pulse, Pulse) def test_generate_pulse_with_questions(self, generator: MorphogeneticPulseGenerator) -> None: pulse = generator.generate_pulse( - 2001, + "chan_1", recent_questions=["What about entropy?"], ) assert isinstance(pulse, Pulse) @@ -55,6 +55,6 @@ def test_generate_pulse_with_questions(self, generator: MorphogeneticPulseGenera def test_pulse_types_selected_by_weights(self, generator: MorphogeneticPulseGenerator) -> None: type_counts: dict[PulseType, int] = dict.fromkeys(PulseType, 0) for _ in range(100): - pulse = generator.generate_pulse(2001) + pulse = generator.generate_pulse("chan_1") type_counts[pulse.pulse_type] += 1 assert all(count > 0 for count in type_counts.values()) From 72f258259d2a397b11a7a1ebeb5acb3efe809c24 Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 15:14:44 -0500 Subject: [PATCH 5/6] Fix event dispatch signatures, bound mode history, fix logging level - Engine now dispatches CommunityStateVector and ProcessedMessage objects instead of kwargs, matching Phase 2 cog listener signatures - Add data_models.py with CommunityStateVector and ProcessedMessage (matching dev-foundation PR #187 canonical definitions) - Cap ModeManager._history at 100 entries to prevent unbounded growth - Split noosphere loading exception handling: ImportError -> debug, other exceptions -> warning with traceback --- src/intelstream/bot.py | 4 +- src/intelstream/noosphere/engine.py | 21 +++++--- .../noosphere/shared/data_models.py | 49 +++++++++++++++++++ .../noosphere/shared/mode_manager.py | 4 ++ tests/test_noosphere/test_engine.py | 27 +++++++++- tests/test_noosphere/test_mode_manager.py | 6 +++ 6 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 src/intelstream/noosphere/shared/data_models.py diff --git a/src/intelstream/bot.py b/src/intelstream/bot.py index 2597738..2524a29 100644 --- a/src/intelstream/bot.py +++ b/src/intelstream/bot.py @@ -161,8 +161,10 @@ async def setup_hook(self) -> None: if noosphere_settings.enabled: await self.add_cog(NoosphereCog(self, noosphere_settings)) logger.info("Noosphere Engine cog loaded") + except ImportError: + logger.debug("Noosphere Engine not loaded (missing dependencies)") except Exception: - logger.debug("Noosphere Engine not loaded (disabled or missing dependencies)") + logger.warning("Noosphere Engine failed to load", exc_info=True) guild = discord.Object(id=self.settings.discord_guild_id) self.tree.copy_global_to(guild=guild) diff --git a/src/intelstream/noosphere/engine.py b/src/intelstream/noosphere/engine.py index dfa1114..3ce43b1 100644 --- a/src/intelstream/noosphere/engine.py +++ b/src/intelstream/noosphere/engine.py @@ -7,6 +7,8 @@ from discord.ext import commands, tasks from intelstream.noosphere.config import NoosphereSettings +from intelstream.noosphere.constants import MessageClassification +from intelstream.noosphere.shared.data_models import CommunityStateVector, ProcessedMessage from intelstream.noosphere.shared.mode_manager import ModeManager from intelstream.noosphere.shared.phi_parameter import PhiParameter @@ -57,13 +59,13 @@ async def tick(self) -> None: mode_weights = self.phi.mode_weights() - self.bot.dispatch( - "state_vector_updated", + csv = CommunityStateVector( guild_id=self.guild_id, - mode_weights=mode_weights, - tick_count=self._tick_count, + timestamp=datetime.now(UTC), ) + self.bot.dispatch("state_vector_updated", csv, mode_weights, self._tick_count) + await self._check_dormancy() async def process_message(self, message: discord.Message) -> None: @@ -76,14 +78,19 @@ async def process_message(self, message: discord.Message) -> None: if self._is_cryptobiotic: await self._exit_cryptobiosis() - self.bot.dispatch( - "message_processed", + processed = ProcessedMessage( guild_id=self.guild_id, channel_id=str(message.channel.id), - author_id=str(message.author.id), + user_id=str(message.author.id), + message_id=str(message.id), content=message.content, + timestamp=datetime.now(UTC), + is_bot=False, + classification=MessageClassification.ANTHROPHONY, ) + self.bot.dispatch("message_processed", processed) + async def _check_dormancy(self) -> None: """Check if the guild should enter cryptobiosis.""" now = datetime.now(UTC) diff --git a/src/intelstream/noosphere/shared/data_models.py b/src/intelstream/noosphere/shared/data_models.py new file mode 100644 index 0000000..e8fae5f --- /dev/null +++ b/src/intelstream/noosphere/shared/data_models.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import datetime + + import numpy as np # type: ignore[import-not-found] + +from intelstream.noosphere.constants import MessageClassification # noqa: TC001 + + +@dataclass +class ProcessedMessage: + guild_id: str + channel_id: str + user_id: str + message_id: str + content: str + timestamp: datetime + is_bot: bool + classification: MessageClassification + embedding: np.ndarray | None = field(default=None, repr=False) + topic_cluster: int | None = None + + +@dataclass +class CommunityStateVector: + guild_id: str + timestamp: datetime + semantic_coherence: float = 0.0 + vocab_convergence: float = 0.0 + topic_entropy: float = 0.0 + activity_rate: float = 0.0 + anthrophony_ratio: float = 0.0 + biophony_ratio: float = 0.0 + geophony_ratio: float = 0.0 + semantic_momentum: float = 0.0 + topic_churn: float = 0.0 + reply_depth: float = 0.0 + activity_entropy: float = 0.0 + egregore_index: float = 0.0 + sentiment_alignment: float = math.nan + interaction_modularity: float = math.nan + fractal_dimension: float = math.nan + lyapunov_exponent: float = math.nan + gromov_curvature: float = math.nan diff --git a/src/intelstream/noosphere/shared/mode_manager.py b/src/intelstream/noosphere/shared/mode_manager.py index 5074c1d..33b103b 100644 --- a/src/intelstream/noosphere/shared/mode_manager.py +++ b/src/intelstream/noosphere/shared/mode_manager.py @@ -27,6 +27,8 @@ class ModeManager: Phase 4: automatic transitions driven by pathology detection. """ + MAX_HISTORY = 100 + def __init__(self, guild_id: str, default_mode: ComputationMode = ComputationMode.INTEGRATIVE): self.guild_id = guild_id self._current_mode = default_mode @@ -54,6 +56,8 @@ def set_mode(self, new_mode: ComputationMode, reason: str = "manual") -> ModeTra ) self._current_mode = new_mode self._history.append(transition) + if len(self._history) > self.MAX_HISTORY: + self._history = self._history[-self.MAX_HISTORY :] logger.info( "Mode transition", guild_id=self.guild_id, diff --git a/tests/test_noosphere/test_engine.py b/tests/test_noosphere/test_engine.py index 5ee0e22..ac91ed0 100644 --- a/tests/test_noosphere/test_engine.py +++ b/tests/test_noosphere/test_engine.py @@ -5,6 +5,7 @@ from intelstream.noosphere.config import NoosphereSettings from intelstream.noosphere.engine import NoosphereCog, NoosphereEngine +from intelstream.noosphere.shared.data_models import CommunityStateVector, ProcessedMessage class TestNoosphereEngine: @@ -33,9 +34,31 @@ async def test_tick_advances_phi(self, engine: NoosphereEngine) -> None: await engine.tick() assert engine._tick_count == initial_tick + 1 - async def test_tick_dispatches_event(self, engine: NoosphereEngine, bot: MagicMock) -> None: + async def test_tick_dispatches_state_vector( + self, engine: NoosphereEngine, bot: MagicMock + ) -> None: await engine.tick() bot.dispatch.assert_called() + args = bot.dispatch.call_args[0] + assert args[0] == "state_vector_updated" + assert isinstance(args[1], CommunityStateVector) + assert args[1].guild_id == "guild_123" + + async def test_process_message_dispatches_processed_message( + self, engine: NoosphereEngine, bot: MagicMock + ) -> None: + message = MagicMock() + message.author.bot = False + message.content = "test" + message.channel.id = 123 + message.author.id = 456 + message.id = 789 + await engine.process_message(message) + args = bot.dispatch.call_args[0] + assert args[0] == "message_processed" + assert isinstance(args[1], ProcessedMessage) + assert args[1].guild_id == "guild_123" + assert args[1].content == "test" async def test_tick_inactive_does_nothing(self, engine: NoosphereEngine) -> None: await engine.shutdown() @@ -55,6 +78,7 @@ async def test_process_message_updates_activity(self, engine: NoosphereEngine) - message.content = "test" message.channel.id = 123 message.author.id = 456 + message.id = 789 before = engine._last_human_message await engine.process_message(message) @@ -87,6 +111,7 @@ async def test_message_exits_cryptobiosis(self, engine: NoosphereEngine) -> None message.content = "hello" message.channel.id = 123 message.author.id = 456 + message.id = 789 await engine.process_message(message) assert not engine.is_cryptobiotic diff --git a/tests/test_noosphere/test_mode_manager.py b/tests/test_noosphere/test_mode_manager.py index 973457e..e425815 100644 --- a/tests/test_noosphere/test_mode_manager.py +++ b/tests/test_noosphere/test_mode_manager.py @@ -66,3 +66,9 @@ def test_active_pathologies_is_copy(self, manager: ModeManager) -> None: pathologies = manager.active_pathologies pathologies.clear() assert len(manager.active_pathologies) == 1 + + def test_history_bounded(self, manager: ModeManager) -> None: + modes = [ComputationMode.RESONANT, ComputationMode.BROADCAST] + for i in range(ModeManager.MAX_HISTORY + 20): + manager.set_mode(modes[i % 2], reason=f"test_{i}") + assert len(manager.history) == ModeManager.MAX_HISTORY From 0407d6327d510d3e711ca4d8d7d96a553491e330 Mon Sep 17 00:00:00 2001 From: user1303836 Date: Fri, 6 Feb 2026 15:16:06 -0500 Subject: [PATCH 6/6] Use str,Enum base class for all enums (JSON serialization) --- src/intelstream/noosphere/constants.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/intelstream/noosphere/constants.py b/src/intelstream/noosphere/constants.py index a4776c7..89bb5a5 100644 --- a/src/intelstream/noosphere/constants.py +++ b/src/intelstream/noosphere/constants.py @@ -21,19 +21,19 @@ ARCHIVE_FIDELITY_FLOOR: float = 0.01 -class CrystalRoomMode(enum.Enum): +class CrystalRoomMode(str, enum.Enum): NUMBER_STATION = "number_station" WHALE = "whale" GHOST = "ghost" -class CrystalRoomState(enum.Enum): +class CrystalRoomState(str, enum.Enum): OPEN = "open" SEALED = "sealed" BREATHING = "breathing" -class ComputationMode(enum.Enum): +class ComputationMode(str, enum.Enum): SUBTRACTIVE = "subtractive" BROADCAST = "broadcast" RESONANT = "resonant" @@ -46,7 +46,7 @@ class ComputationMode(enum.Enum): TOPOLOGICAL = "topological" -class PathologyType(enum.Enum): +class PathologyType(str, enum.Enum): CANCER = "non_terminating_pruning" CYTOKINE_STORM = "receiver_saturation" SEIZURE = "destructive_sync" @@ -59,7 +59,7 @@ class PathologyType(enum.Enum): SCHISM = "topological_damage" -class MessageClassification(enum.Enum): +class MessageClassification(str, enum.Enum): ANTHROPHONY = "anthrophony" BIOPHONY = "biophony" GEOPHONY = "geophony"