From 4e2dcbf3c9f8922aed2537070e9afee1b1770fb7 Mon Sep 17 00:00:00 2001 From: Citation fixture Date: Fri, 23 Oct 2026 09:25:02 -0400 Subject: [PATCH 1/2] Hash every class that produces projection rows into the projection digest The projection digest from #2773 hashed only the record family, the claim transitions and their capability classes. The code that decides row content also lives on MessageKind.route_rows, the message roles' activity_rows, CommandTarget.direct_names, ChannelActivity.from_message, the claim scopes and the response policies. None of those were hashed, so editing them left stale projections marked fresh. Proven with the interpreter on main: changing ConversationRole.activity_rows left the digest at 3a79e1f3ed68fff9. The capability is now declared on the classes themselves. ProjectionSource (projection_source.py) registers every class that inherits it, and the digest hashes the produced rows plus every ProjectionSource class. The (family, capability) pairs and producer_classes are deleted. BusProjectionTables declares only the package it produces from. The classes were found by profiling a full rebuild of a copy of the live bus (19,503 records): 40 classes outside the old digest ran row-producing code. Each family is marked at its root. The old 31 owners are all still covered; the digest now has 156 owners. test_every_class_that_produces_projection_rows_is_in_the_digest profiles a rebuild and fails on any class that ran and is not in the digest, apart from the decode, store and dispatch mechanism modules. With the MessageKind mark removed it fails and names MessageKind. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/activity_clocks.py | 3 +- src/agent_comms/audience_manifest.py | 5 +- src/agent_comms/channel_targets.py | 5 +- src/agent_comms/channels.py | 3 +- src/agent_comms/declared_family.py | 4 +- src/agent_comms/envelope_claim_transitions.py | 11 +-- src/agent_comms/messages.py | 32 +++----- src/agent_comms/projection_source.py | 25 ++++++ src/agent_comms/read_basis.py | 3 +- src/agent_comms/response_policy.py | 7 +- src/agent_comms/task_sources.py | 3 +- src/agent_comms/thread_identity.py | 7 +- src/agent_comms/typed_table.py | 41 ++++------ tests/test_bus.py | 80 +++++++++++++++++++ 14 files changed, 165 insertions(+), 64 deletions(-) create mode 100644 src/agent_comms/projection_source.py diff --git a/src/agent_comms/activity_clocks.py b/src/agent_comms/activity_clocks.py index edec2e274b..40e0c2b33f 100644 --- a/src/agent_comms/activity_clocks.py +++ b/src/agent_comms/activity_clocks.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field, fields from typing import TYPE_CHECKING, Self +from .projection_source import ProjectionSource from .field_codec import FieldCodec from .typed_table import TypedRow @@ -14,7 +15,7 @@ @dataclass(frozen=True, slots=True) -class ChannelActivity(TypedRow): +class ChannelActivity(TypedRow, ProjectionSource): last_message: float = 0 last_user_input: float = 0 diff --git a/src/agent_comms/audience_manifest.py b/src/agent_comms/audience_manifest.py index d16b2ff225..7cf336a505 100644 --- a/src/agent_comms/audience_manifest.py +++ b/src/agent_comms/audience_manifest.py @@ -17,6 +17,7 @@ from agent_comms.coordination_contracts import MAX_IDENTIFIER_CHARS +from .projection_source import ProjectionSource from .channel_targets import exact_stored_target from .field_codec import FieldCodec, MalformedRecord, Sha256Text from .messages import Message @@ -57,7 +58,7 @@ def _digest(domain: bytes, payload: bytes) -> str: @dataclass(frozen=True, slots=True) -class FrozenRecipient: +class FrozenRecipient(ProjectionSource): """Stable coordination lookup paired with its send-time canonical name.""" recipient_lookup: str @@ -69,7 +70,7 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True) -class FrozenAudience: +class FrozenAudience(ProjectionSource): """Immutable precommit snapshot of the full delivered audience, not a acknowledgement. Neither the digest nor direct construction attests a bus append. At recovery, diff --git a/src/agent_comms/channel_targets.py b/src/agent_comms/channel_targets.py index b853676832..f2fccc8b1c 100644 --- a/src/agent_comms/channel_targets.py +++ b/src/agent_comms/channel_targets.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, ClassVar +from .projection_source import ProjectionSource from .activity_clocks import ActivityClocks from .declared_family import DeclaredFamily from .errors import HumanSendRefusedError, RelationViolationError, UnregisteredThreadError @@ -103,7 +104,7 @@ def exact_stored_target(name: str) -> bool: if name in BuiltinChannelView.addresses() else True) -class CommandTarget(ABC): +class CommandTarget(ABC, ProjectionSource): """A command destination owns its binding and catalog participation.""" needs_channel_state: ClassVar[bool] = False @@ -573,7 +574,7 @@ def bindings[C: CliCommand]( @dataclass(frozen=True, slots=True) -class Tag: +class Tag(ProjectionSource): name: str def __post_init__(self) -> None: diff --git a/src/agent_comms/channels.py b/src/agent_comms/channels.py index 935a9776e2..24637c7af8 100644 --- a/src/agent_comms/channels.py +++ b/src/agent_comms/channels.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, ClassVar, Annotated +from .projection_source import ProjectionSource from .channel_targets import ChannelTarget, Tag from .cli_result import CliResult from .declared_family import DeclaredFamily @@ -47,7 +48,7 @@ def __missing__(self, tag: str) -> tuple[Thread, ...]: @dataclass(frozen=True, slots=True) -class Channel(DeclaredFamily, CliResult, ToolResult, affix="Channel"): +class Channel(DeclaredFamily, CliResult, ToolResult, ProjectionSource, affix="Channel"): """A captured channel presentation with behavior supplied by its declaration.""" name: str diff --git a/src/agent_comms/declared_family.py b/src/agent_comms/declared_family.py index 2c19507ebb..2cbe52dda0 100644 --- a/src/agent_comms/declared_family.py +++ b/src/agent_comms/declared_family.py @@ -217,12 +217,12 @@ def _member_declarations(cls) -> tuple[type[DeclaredFamily], ...]: @classmethod def discover_members(cls, package: ModuleType, capability: type) -> tuple[type[Self], ...]: """Acquire a producer package before deriving its declared capability members.""" - DeclaredFamily._discover_package(package) + DeclaredFamily.discover_package(package) return cls.members_with(capability) @staticmethod @cache - def _discover_package(package: ModuleType) -> None: + def discover_package(package: ModuleType) -> None: """Import each module of the package once, so its members register themselves.""" for module in pkgutil.iter_modules(package.__path__, package.__name__ + "."): if not module.ispkg: diff --git a/src/agent_comms/envelope_claim_transitions.py b/src/agent_comms/envelope_claim_transitions.py index 999c9d8579..fff16a8d7c 100644 --- a/src/agent_comms/envelope_claim_transitions.py +++ b/src/agent_comms/envelope_claim_transitions.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Annotated, ClassVar, Literal +from .projection_source import ProjectionSource from .coordination_errors import IdentityConflict from .field_codec import ( BoundedText, @@ -115,7 +116,7 @@ def validate_leaf(self, path: Path) -> None: @dataclass(frozen=True, slots=True) -class AssignmentBinding: +class AssignmentBinding(ProjectionSource): """The selected claim's revision and source, used for permission checks and its SQL row.""" assignment_id: Annotated[str, BoundedText] @@ -135,7 +136,7 @@ def __post_init__(self) -> None: @dataclass(frozen=True) -class ClaimScope(SubclassUnion, ABC, affix=""): +class ClaimScope(SubclassUnion, ABC, ProjectionSource, affix=""): """What one acquisition belongs to; its member answers reuse, liveness and operation use.""" def reuses(self, other: RecordedScope) -> bool: @@ -253,7 +254,7 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True) -class ClaimRelease: +class ClaimRelease(ProjectionSource): resource: Annotated[str, ResourcePathText] generation: Annotated[str, Id128Text] @@ -276,7 +277,7 @@ class AcquiringClaim(ClaimIdentity): @dataclass(frozen=True) -class ClaimTransition(ClaimIdentity, SubclassUnion, ABC, affix=""): +class ClaimTransition(ClaimIdentity, SubclassUnion, ABC, ProjectionSource, affix=""): """One original resource transition; its member declares what it changes.""" max_resources: ClassVar[int] = 32 @@ -459,7 +460,7 @@ def live(self, registry: RegistrySnapshot) -> bool: @dataclass(frozen=True, slots=True) -class ClaimProjection(Mapping[str, ClaimOwner]): +class ClaimProjection(Mapping[str, ClaimOwner], ProjectionSource): """Immutable sorted claim table plus the last processed envelope sequence.""" last_seq: Annotated[int, ProjectionSequence] = 0 diff --git a/src/agent_comms/messages.py b/src/agent_comms/messages.py index 23ea32a61a..9630245a6f 100644 --- a/src/agent_comms/messages.py +++ b/src/agent_comms/messages.py @@ -3,12 +3,14 @@ from __future__ import annotations import agent_comms +from .projection_source import ProjectionSource from .turn_origin import TurnOrigin import builtins import hashlib import re import sqlite3 import time +from types import ModuleType from abc import ABC, abstractmethod from collections.abc import Iterable, Iterator, Mapping, Sequence from contextlib import closing @@ -72,16 +74,8 @@ class BusProjectionTables(ProducedTable, SharedFileTables, DerivedTables, declar """Reverse lookups are derived from the record, in its append transaction.""" @classmethod - def producer_capabilities(cls) -> tuple[tuple[type[DeclaredFamily], type], ...]: - return ((BusRecord, BusCapability), (ClaimTransition, ClaimTransition)) - - @classmethod - def producer_classes(cls) -> tuple[type[DeclaredFamily], ...]: - return tuple( - member - for family, capability in cls.producer_capabilities() - for member in family.discover_members(agent_comms, capability) - ) + def producer_package(cls) -> ModuleType: + return agent_comms @classmethod def create_schema(cls, db: sqlite3.Connection) -> None: @@ -204,7 +198,7 @@ def apply( holders.advance(removed, projected) -class ProjectionPass(ABC): +class ProjectionPass(ABC, ProjectionSource): """How one projection pass writes the rows its records derive: an append writes its few rows at once.""" @@ -274,7 +268,7 @@ class CapabilityRow(BusTables, declared_name=None): seq: Annotated[int, Column(primary_key=True, references=TableReference(BusRow, ("seq",)))] -class _CapabilityTable: +class _CapabilityTable(ProjectionSource): """A class's table is derived once, after its dataclass decorator has run.""" def __get__(self, instance: BusCapability | None, owner: type[BusCapability]) -> type[CapabilityRow]: @@ -295,7 +289,7 @@ def for_class(self, owner: type[BusCapability]) -> type[CapabilityRow]: @dataclass(frozen=True) -class BusCapability(TypedRow): +class BusCapability(TypedRow, ProjectionSource): """The hierarchy and each dataclass declaration own their table union.""" table = _CapabilityTable() @@ -374,7 +368,7 @@ class Addressed(BusCapability): indexes: ClassVar[tuple[Index, ...]] = (Index(("sender", "target", "seq")),) -class MessageKind(DeclaredFamily, affix="Kind"): +class MessageKind(DeclaredFamily, ProjectionSource, affix="Kind"): """A declared message kind; each member owns the validation it requires. Members are the kind themselves, never instances: the wire carries the @@ -488,7 +482,7 @@ def require_publishable(cls, message: Message, worktree: Path) -> None: message.report.require_references_exist(worktree) -class MembershipTransition(DeclaredFamily, affix="Membership"): +class MembershipTransition(DeclaredFamily, ProjectionSource, affix="Membership"): @classmethod def conversation_role(cls) -> MembershipConversationRole: return MembershipConversationRole(cls) @@ -515,13 +509,13 @@ def applies(previous: bool, current: bool) -> bool: @dataclass(frozen=True, slots=True) -class NoMembershipChange(NullMember): +class NoMembershipChange(NullMember, ProjectionSource): def conversation_role(self): return ConversationRole() @dataclass(frozen=True, slots=True) -class MessageRoleWire: +class MessageRoleWire(ProjectionSource): """The unchanged external fields, decoded once into a message role.""" change: Annotated[ @@ -536,7 +530,7 @@ def role(self): return role.as_notice() if self.notice else role -class MessageRole(InlineRecord): +class MessageRole(InlineRecord, ProjectionSource): category: ClassVar[type[MessageCategory]] @abstractmethod @@ -576,7 +570,7 @@ class SendableRole(MessageRole, DeclaredFamily, affix="Role"): pass -class InformationalMessage: +class InformationalMessage(ProjectionSource): def response_policy(self, message): return InformationalPolicy.instance() diff --git a/src/agent_comms/projection_source.py b/src/agent_comms/projection_source.py new file mode 100644 index 0000000000..4c913fa383 --- /dev/null +++ b/src/agent_comms/projection_source.py @@ -0,0 +1,25 @@ +"""The declared capability of code that produces derived projection rows.""" + +from __future__ import annotations + +from typing import ClassVar + + +class ProjectionSource: + """A class whose code decides projection row content. + + Its source, and every subclass's source, belongs to the digest that tells a + stored projection it is still fresh. A class reached while rows are produced + that does not inherit this capability is a hole in that digest. + """ + + __slots__ = () + _sources: ClassVar[list[type[ProjectionSource]]] = [] + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + ProjectionSource._sources.append(cls) + + @staticmethod + def sources() -> tuple[type[ProjectionSource], ...]: + return tuple(ProjectionSource._sources) diff --git a/src/agent_comms/read_basis.py b/src/agent_comms/read_basis.py index cded4de71e..0c04744d1a 100644 --- a/src/agent_comms/read_basis.py +++ b/src/agent_comms/read_basis.py @@ -19,6 +19,7 @@ from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Self, TextIO +from .projection_source import ProjectionSource from .activity_clocks import ActivityClocks, ChannelActivity from .channel_targets import CommandTarget, ThreadTarget from .coordination_errors import DMPaintedBoundChanged, DMScopeChanged, HistoricalSourceChanged @@ -817,7 +818,7 @@ def validate_identity( self.displayed.validate(self.viewer, snapshot, bus_identity) -class MessageDisplayScope(DeclaredFamily, ABC, affix="DisplayScope"): +class MessageDisplayScope(DeclaredFamily, ABC, ProjectionSource, affix="DisplayScope"): """Captured inclusion and its exact predicate on the bus capability tables.""" def read_key(self, sender: str, target: str, snapshot: RegistryProvenance) -> Conversation: diff --git a/src/agent_comms/response_policy.py b/src/agent_comms/response_policy.py index 82ef826e5b..8549a28d1d 100644 --- a/src/agent_comms/response_policy.py +++ b/src/agent_comms/response_policy.py @@ -8,6 +8,7 @@ from functools import cache from typing import TYPE_CHECKING +from .projection_source import ProjectionSource from .declared_family import DeclaredFamily if TYPE_CHECKING: @@ -16,7 +17,7 @@ @dataclass(frozen=True) -class ResponsePolicy(DeclaredFamily, affix="Policy"): +class ResponsePolicy(DeclaredFamily, ProjectionSource, affix="Policy"): starts_turn = False separate_turn = False @@ -52,7 +53,7 @@ def disposition_key(self, message: Message, owner: Thread) -> str: return f"bus:{message.seq}:owner:{float(owner.created_at).hex()}" -class NoChannelRecipients: +class NoChannelRecipients(ProjectionSource): def recipients( self, message: Message, audience: Sequence[str], *, aliases: Mapping[str, str] | None = None ) -> tuple[str, ...]: @@ -119,7 +120,7 @@ def guidance(self, message: Message, *, aliases: Mapping[str, str] | None = None @dataclass(frozen=True, slots=True) -class ResponseEligibility: +class ResponseEligibility(ProjectionSource): policy: ResponsePolicy recipients: tuple[str, ...] = () diff --git a/src/agent_comms/task_sources.py b/src/agent_comms/task_sources.py index d4f89c512c..66b46d262b 100644 --- a/src/agent_comms/task_sources.py +++ b/src/agent_comms/task_sources.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Annotated +from .projection_source import ProjectionSource from .channel_targets import CommandTarget from .declared_family import DeclaredFamily from .errors import RelationViolationError @@ -185,7 +186,7 @@ def root_source(self, message: Message, originals: dict[MessageReference, Messag raise RelationViolationError("Retained correction lacks its original source") from error -class TaskAttachment(DeclaredFamily, affix="TaskAttachment"): +class TaskAttachment(DeclaredFamily, ProjectionSource, affix="TaskAttachment"): """Absent attachments have no publication, fact or author effects.""" retains_authored_task = False diff --git a/src/agent_comms/thread_identity.py b/src/agent_comms/thread_identity.py index 4ad62bcb8d..3c97744df6 100644 --- a/src/agent_comms/thread_identity.py +++ b/src/agent_comms/thread_identity.py @@ -16,6 +16,7 @@ from agent_comms.participant_identity import ParticipantIdentity +from .projection_source import ProjectionSource from .cli_options import JsonFlagRecord from .declared_family import DeclaredFamily, UniqueExternalSelector, UnknownFamilyMember, declared_words from .errors import RelationViolationError @@ -421,7 +422,7 @@ def decode(cls, name: str, birth: float) -> ThreadIncarnation: @dataclass(frozen=True, slots=True) -class TurnId: +class TurnId(ProjectionSource): value: str @classmethod @@ -525,7 +526,7 @@ def __eq__(self, other: object) -> bool: @dataclass(slots=True) -class GenerationCounter: +class GenerationCounter(ProjectionSource): """One monotonic allocation domain, including deleted-name tombstones.""" counter: int = 0 @@ -558,7 +559,7 @@ def rename(self, old: str, new: str) -> None: self.generations[new] = self.generations.pop(old) -class ThreadRole(DeclaredFamily, affix="ThreadRole"): +class ThreadRole(DeclaredFamily, ProjectionSource, affix="ThreadRole"): """Whether a thread executes turns is the member's type; its wire name is its spelling.""" origin_label: ClassVar[str] diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index bc0f9276a7..5013ef6ec7 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -64,6 +64,7 @@ ) from .maintenance_barrier import HOLDER_NONCE, MaintenanceBarrier from .package_stamp import STAMP +from .projection_source import ProjectionSource from .store_files import _store_lock if TYPE_CHECKING: @@ -1424,17 +1425,12 @@ class ProjectionProducerTables(SharedFileTables, DerivedTables): class ProducedRow(ABC): - """Rows rebuilt when their declared producing modules change.""" + """Rows rebuilt when the source of a class producing them changes.""" @classmethod @abstractmethod - def producer_capabilities(cls) -> tuple[tuple[type[DeclaredFamily], type], ...]: - """Each record family and the capability whose declarations produce these rows.""" - - @classmethod - @abstractmethod - def producer_classes(cls) -> tuple[type[DeclaredFamily], ...]: - """The record family's declarations that implement this projection's production.""" + def producer_package(cls) -> types.ModuleType: + """The package whose ProjectionSource classes produce these rows.""" @classmethod @abstractmethod @@ -1974,34 +1970,31 @@ class ProducedTable(ProducedRow, TypedTable, declared_name=None): @classmethod def producer_digest(cls) -> str: for family in cls.__mro__: - if issubclass(family, ProducedTable) and "producer_classes" in vars(family): + if issubclass(family, ProducedTable) and "producer_package" in vars(family): return family._producer_digest() raise TypeError(f"{cls.__name__} has no producing family declaration") @classmethod @cache def _producer_digest(cls) -> str: - producers = cls.producer_classes() + source = "\0".join( + f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}" + for owner in cls.producer_owners() + ) + return hashlib.sha256(source.encode()).hexdigest() + + @classmethod + def producer_owners(cls) -> tuple[type, ...]: + """Every class whose source the produced rows depend on, in a stable order.""" + DeclaredFamily.discover_package(cls.producer_package()) owners: set[type] = { owner for row in (cls, *cls.members_with(cls)) for owner in row.__mro__ if issubclass(owner, cls) } - # The capability's classes outside the record family (Message, Claimed) hold the - # production code itself, so they belong to the digest beside the family. - capabilities = tuple(capability for _, capability in cls.producer_capabilities()) - for producer in producers: - owners.update( - owner - for owner in producer.__mro__ - if issubclass(owner, (cls, producer.family_root(), *capabilities)) - ) - source = "\0".join( - f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}" - for owner in sorted(owners, key=lambda owner: (owner.__module__, owner.__qualname__)) - ) - return hashlib.sha256(source.encode()).hexdigest() + owners.update(ProjectionSource.sources()) + return tuple(sorted(owners, key=lambda owner: (owner.__module__, owner.__qualname__))) @dataclass(frozen=True) diff --git a/tests/test_bus.py b/tests/test_bus.py index 1cdc58038d..0016220969 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -155,6 +155,26 @@ def test_projection_digest_ignores_infrastructure_source( "HeldResources.apply(db, self.claim_transition, projected, holders)", "HeldResources.apply(db, self.claim_transition, projected, holders)\n del projected", ), + ( + "messages.py", + "return (SentActivity(message.sender, message.timestamp),)", + "return (SentActivity(message.target, message.timestamp),)", + ), + ( + "messages.py", + "return (BusRouteCounts.from_message(message),)", + "return (BusRouteCounts.from_message(message), BusRouteCounts.from_message(message))", + ), + ( + "channel_targets.py", + " def direct_names(self) -> tuple[str, ...]:\n return self.names\n", + " def direct_names(self) -> tuple[str, ...]:\n return self.names[:1]\n", + ), + ( + "activity_clocks.py", + "message.sender_role.last_user_input(0.0, message.timestamp),", + "message.sender_role.last_user_input(1.0, message.timestamp),", + ), ), ) def test_projection_digest_tracks_producing_source( @@ -171,6 +191,66 @@ def test_projection_digest_tracks_producing_source( assert projection_source_digest(projection_source) != before +# Mechanisms that decode, store and dispatch rows; their edits never change row content. +PROJECTION_MECHANISM_MODULES = frozenset({ + "agent_comms.core_stamp", + "agent_comms.declared_family", + "agent_comms.field_codec", + "agent_comms.mro_dispatch", + "agent_comms.projection_source", + "agent_comms.typed_table", +}) + + +def projection_rebuild_classes(bus: Bus) -> set[type]: + """Every agent_comms class whose own code ran while the projections were rebuilt.""" + codes = set() + + def profile(frame, event, argument): + if event == "call": + codes.add(frame.f_code) + + with bus.writing() as db: + BusProjectionTables.reset_schema(db) # imports and caches outside the profile + sys.setprofile(profile) + try: + BusProjectionTables.reset_schema(db) + finally: + sys.setprofile(None) + modules = { + module.__file__: module for name, module in tuple(sys.modules.items()) + if name.startswith("agent_comms.") and getattr(module, "__file__", None) + } + classes = set() + for code in codes: + if code.co_filename not in modules: + continue + owner = modules[code.co_filename] + for part in code.co_qualname.split(".")[:-1]: + if part == "": + break + owner = getattr(owner, part) + if isinstance(owner, type): + classes.add(owner) + return classes + + +def test_every_class_that_produces_projection_rows_is_in_the_digest(bus): + first = bus.append(lambda seq, db: PublicRecord.from_message( + Message("author", "worker", "direct", InfoKind, seq=seq), + )) + bus.append(lambda seq, db: PublicRecord.from_message( + Message("worker", "author", "reply", InfoKind, seq=seq, answers=(first.reference,)), + )) + owners = set(BusProjectionTables.producer_owners()) + missing = sorted( + f"{owner.__module__}.{owner.__qualname__}" + for owner in projection_rebuild_classes(bus) + if owner.__module__ not in PROJECTION_MECHANISM_MODULES and owner not in owners + ) + assert missing == [] + + def test_bus_attachment_and_failed_write_do_not_install_a_schema(tmp_path): bus = Bus(Bus.path_in(tmp_path / "not-installed")) assert not bus.path.parent.exists() From 2c5c85464b2833871bc8bfa4d563b163835580d1 Mon Sep 17 00:00:00 2001 From: Citation fixture Date: Fri, 23 Oct 2026 09:27:44 -0400 Subject: [PATCH 2/2] Sort the ProjectionSource imports where ruff counted them as unsorted Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/activity_clocks.py | 2 +- src/agent_comms/audience_manifest.py | 2 +- src/agent_comms/channel_targets.py | 2 +- src/agent_comms/read_basis.py | 2 +- src/agent_comms/response_policy.py | 2 +- src/agent_comms/task_sources.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/agent_comms/activity_clocks.py b/src/agent_comms/activity_clocks.py index 40e0c2b33f..0ffc61ae8b 100644 --- a/src/agent_comms/activity_clocks.py +++ b/src/agent_comms/activity_clocks.py @@ -5,8 +5,8 @@ from dataclasses import dataclass, field, fields from typing import TYPE_CHECKING, Self -from .projection_source import ProjectionSource from .field_codec import FieldCodec +from .projection_source import ProjectionSource from .typed_table import TypedRow if TYPE_CHECKING: diff --git a/src/agent_comms/audience_manifest.py b/src/agent_comms/audience_manifest.py index 7cf336a505..4139bf4f4c 100644 --- a/src/agent_comms/audience_manifest.py +++ b/src/agent_comms/audience_manifest.py @@ -17,10 +17,10 @@ from agent_comms.coordination_contracts import MAX_IDENTIFIER_CHARS -from .projection_source import ProjectionSource from .channel_targets import exact_stored_target from .field_codec import FieldCodec, MalformedRecord, Sha256Text from .messages import Message +from .projection_source import ProjectionSource AUDIENCE_VERSION: Final = 1 MAX_RECIPIENTS: Final = 4096 diff --git a/src/agent_comms/channel_targets.py b/src/agent_comms/channel_targets.py index f2fccc8b1c..7ebe16713c 100644 --- a/src/agent_comms/channel_targets.py +++ b/src/agent_comms/channel_targets.py @@ -6,12 +6,12 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, ClassVar -from .projection_source import ProjectionSource from .activity_clocks import ActivityClocks from .declared_family import DeclaredFamily from .errors import HumanSendRefusedError, RelationViolationError, UnregisteredThreadError from .field_codec import NullMember, SingleFieldValue, SubclassUnion from .history_result import DmHistoryPageResult, HistoryPageResult +from .projection_source import ProjectionSource from .response_policy import DirectPolicy, MentionedOnlyPolicy from .wake_policy import BoundedTriageWake, FullWake, MessageAudience, UnmentionedWake diff --git a/src/agent_comms/read_basis.py b/src/agent_comms/read_basis.py index 0c04744d1a..15e6780e89 100644 --- a/src/agent_comms/read_basis.py +++ b/src/agent_comms/read_basis.py @@ -19,7 +19,6 @@ from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Self, TextIO -from .projection_source import ProjectionSource from .activity_clocks import ActivityClocks, ChannelActivity from .channel_targets import CommandTarget, ThreadTarget from .coordination_errors import DMPaintedBoundChanged, DMScopeChanged, HistoricalSourceChanged @@ -29,6 +28,7 @@ from .mentions import ThreadMention from .message_reference import MessageReference from .private_path import FileObservation, MissingFile +from .projection_source import ProjectionSource from .recorded_bus import RecordedBus, RecordedBusIdentity from .store_files import SharedLock from .thread_identity import RecordedThreadIncarnation, ThreadIncarnation diff --git a/src/agent_comms/response_policy.py b/src/agent_comms/response_policy.py index 8549a28d1d..0eb4f0acf2 100644 --- a/src/agent_comms/response_policy.py +++ b/src/agent_comms/response_policy.py @@ -8,8 +8,8 @@ from functools import cache from typing import TYPE_CHECKING -from .projection_source import ProjectionSource from .declared_family import DeclaredFamily +from .projection_source import ProjectionSource if TYPE_CHECKING: from .messages import Message diff --git a/src/agent_comms/task_sources.py b/src/agent_comms/task_sources.py index 66b46d262b..db46bcce06 100644 --- a/src/agent_comms/task_sources.py +++ b/src/agent_comms/task_sources.py @@ -6,7 +6,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Annotated -from .projection_source import ProjectionSource from .channel_targets import CommandTarget from .declared_family import DeclaredFamily from .errors import RelationViolationError @@ -14,6 +13,7 @@ from .goals import GoalDeclaration, GoalRevision from .message_reference import MessageReference from .owner_requests import OwnerRequestId, RequestDirective +from .projection_source import ProjectionSource from .thread_identity import ( RecordedThreadIncarnation, ThreadIncarnation,