Skip to content

refactor(stella-protocol): generate the signal-consumer ledger from the tag table, making totality a compile error - #2737

Merged
macanderson merged 4 commits into
mainfrom
worktree-signal-consumer-tags-macro
Aug 10, 2026
Merged

refactor(stella-protocol): generate the signal-consumer ledger from the tag table, making totality a compile error#2737
macanderson merged 4 commits into
mainfrom
worktree-signal-consumer-tags-macro

Conversation

@macanderson

@macanderson macanderson commented Aug 10, 2026

Copy link
Copy Markdown
Owner

#2720 has merged; this now targets main directly. It was authored stacked on that PR, so the merge resolution here kept the generated ledger from this branch and the MissingRow message fix that landed on main with #2720.

What & why

#2720 shipped the signal-consumer ledger as a hand-maintained table enforced by a test. That was strictly weaker than the E0004 tripwire agent_event_tags! already gave the tag list, and it shipped that way for exactly one reason, recorded in its module doc at the time: event.rs sat 35 lines under the 1500-line ratchet with no baseline entry, and the postures are worth roughly a hundred.

This removes that constraint. The variant table moves to crates/stella-protocol/src/event/tags.rs and grows a ConsumerPosture per row, so SIGNAL_CONSUMERS is generated from the same list as type_tag() and KNOWN_TYPE_TAGS.

event.rs drops from 1465 to 1350 lines. No baseline entry was added and none movedstella-protocol still has zero grandfathered files, which is the state to keep it in.

The witness

Adding an AgentEvent variant with no declared consumer is now a build failure, not a test failure. Verified by adding a probe variant and running cargo build (not cargo test):

error[E0004]: non-exhaustive patterns: `&AgentEvent::HaltFired { .. }` not covered
  --> crates/stella-protocol/src/event/tags.rs:95:23
   |
95 |                   match self {
   |                         ^^^^ pattern `&AgentEvent::HaltFired { .. }` not covered

That is the whole point of the change and the only evidence it landed, so it is quoted rather than asserted. (The probe used HaltFired — the variant #2704 will actually add. It will hit this and need one row.)

What generation does not buy

Three LedgerViolation kinds — MissingRow, UnknownTag, DuplicateRow — become unrepresentable for the real ledger. They are deliberately kept, and the module doc now says why: audit_ledger takes its ledger and tag list as parameters precisely so the negative controls can hand it broken input. Deleting the structural rules because one call site can no longer trip them would cost the harness its ability to prove itself with them — the vacuous-harness failure content_free.rs exists to prevent.

The semantic half still runs and still matters: that every gap posture cites a real #1234 reference, that a Behavioral row names somewhere to look, and that the posture agrees with surfaces. Those are judgements about a row's content, and no macro can hold an author to them.

Compatibility

KNOWN_TYPE_TAGS keeps its path through event via a re-export, so crate::KNOWN_TYPE_TAGS, schema_export.rs, tests/wire_contract.rs, and every existing intra-doc citation are untouched. The "Adding a variant?" propagation comment — the map to the four compile-enforced and two silent downstream matchers — moved with the table rather than being stranded in event.rs away from what it describes.

Deleted tests

None.

The gate

  • cargo build -p stella-protocol
  • cargo test -p stella-protocol — 128 tests, all green
  • cargo test --workspace — green
  • make lint (clippy -D warnings, workspace, all targets)
  • RUSTDOCFLAGS="-D warnings" cargo doc — green (run unpiped; a piped cargo doc can report a false green)
  • scripts/check-file-size.sh — OK, nothing went over, no baseline change
  • cargo fmt --all

Closes #2730
Refs #2701
Refs #2702

…riant declares what reads it

"Produced and not consumed" is a repo-wide shape, and every instance so far
was found by a bench run paying for it rather than by a test going red:
flip.json written with nothing reading it (#1536); verify_done confirmations
tallied but not feeding the halt, which cost solved_then_timeout four times
on one certification panel before #2661 wired it; the flip transition still
emitting nothing durable, so a shipped halt cannot be measured in the field.
Each was fixed as whack-a-mole.

This makes the class structural, copying content_free.rs element for element:
a reviewed table, tests enforcing it from both sides, and negative controls
proving the harness can fail. Every wire tag gets a row declaring a
ConsumerPosture — Behavioral names the code that branches on it, Surfaced
names the surfaces that select it, RecordedOnly and Unclassified each cite
the issue where the gap is being decided. Unclassified sits under a down-only
ratchet so a new variant cannot be filed away unread.

Three design calls that differ from the issue's sketch, each forced by the code:

- Posture and surfaces are orthogonal fields, not one enum. tool_result is
  both Behavioral and on the observatory whitelist; folding them together
  would have made #2707's parity test unsatisfiable on day one.
- Surface names only the observatory and serve. The TUI and replay match
  AgentEvent exhaustively, so membership is compiler-guaranteed for every
  variant and a per-row claim would record nothing.
- The ledger is a hand-maintained table rather than a third output of
  agent_event_tags!. That macro is the wire decoder's source of truth and
  event.rs sits 35 lines under the ratchet; both reasons are in the module doc
  so the next author does not re-litigate it.

The tool_result row is the ledger earning its keep already: the intuitive
consumer is loop detection, which is wrong — the detector reads the
transcript, a different plane. The real event-plane consumer is the store's
tool_calls projection.

Refs #2701
Refs #2703
…TYPE_TAGS

The symbol is in scope via the module's own use, so the bare intra-doc label
already resolves and rustdoc's redundant-explicit-links lint rejects the
spelled-out path. Caught by CI's doc-warnings step, which the local run of
this change skipped.
…he tag table

Moves the variant table out of event.rs into event/tags.rs and extends it to
carry a ConsumerPosture per variant, so SIGNAL_CONSUMERS is generated from the
same list as type_tag() and KNOWN_TYPE_TAGS.

The point is where the enforcement lives. The ledger shipped as a
hand-maintained table checked by a test, which was strictly weaker than the
E0004 tripwire the tag list already had. It shipped that way for one reason:
event.rs sat 35 lines under the 1500-line ratchet with no baseline entry, and
the postures are worth roughly a hundred. Splitting the table out removes that
constraint — event.rs drops to 1350 lines and no baseline moved.

Adding an AgentEvent variant without declaring what consumes it is now a
compile error rather than a failing test:

  error[E0004]: non-exhaustive patterns: `&AgentEvent::HaltFired { .. }` not covered
    --> crates/stella-protocol/src/event/tags.rs:95:23

Three LedgerViolation kinds (MissingRow, UnknownTag, DuplicateRow) become
unrepresentable for the real ledger and are deliberately kept: audit_ledger
takes its inputs as parameters so the negative controls can hand it broken
input, and a check that cannot be shown to fail is a claim rather than a
check. The semantic rules it still enforces — issue-citation shape, a named
site for a Behavioral claim, posture/surfaces coherence — are judgements about
a row's content that no macro can hold an author to.

KNOWN_TYPE_TAGS keeps its path via a re-export, so crate::KNOWN_TYPE_TAGS and
every existing citation are unchanged.

Closes #2730
Refs #2701
Refs #2702
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stella-cli-docs Ready Ready Preview Aug 10, 2026 9:38pm

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the AgentEvent tag table into a dedicated module that now also encodes consumer postures, generating SIGNAL_CONSUMERS, KNOWN_TYPE_TAGS, and type_tag() from a single macro-backed source of truth, so missing consumer rows become compile-time errors while preserving existing semantics and public APIs.

Flow diagram for generating AgentEvent metadata from a single tags table

flowchart LR
    tags_table["agent_event_tags! rows in event_tags"]

    tags_table --> agent_event_tags_macro

    agent_event_tags_macro --> AgentEvent_type_tag["AgentEvent::type_tag"]
    agent_event_tags_macro --> KNOWN_TYPE_TAGS
    agent_event_tags_macro --> SIGNAL_CONSUMERS

    SIGNAL_CONSUMERS --> consumers_module["event::consumers::SIGNAL_CONSUMERS (re-export)"]
    KNOWN_TYPE_TAGS --> event_module["event::KNOWN_TYPE_TAGS (re-export)"]

    SIGNAL_CONSUMERS --> audit_ledger["audit_ledger"]

    subgraph event_module_scope["event.rs"]
      AgentEvent_type_tag
      KNOWN_TYPE_TAGS
    end

    subgraph consumers_module_scope["consumers.rs"]
      consumers_module
      audit_ledger
    end

    subgraph tags_module_scope["tags.rs"]
      tags_table
      agent_event_tags_macro
      SIGNAL_CONSUMERS
    end
Loading

File-Level Changes

Change Details Files
Move the AgentEvent tag table into a new tags module and extend the macro to generate both tag metadata and the signal-consumer ledger.
  • Introduce crates/stella-protocol/src/event/tags.rs with the agent_event_tags! macro taking variant, tag, posture, and surfaces.
  • Implement AgentEvent::type_tag and KNOWN_TYPE_TAGS inside tags.rs using the shared macro.
  • Generate a const SIGNAL_CONSUMERS slice from the same macro table, including ConsumerPosture and Surface data for each variant.
crates/stella-protocol/src/event/tags.rs
Replace the hand-maintained SIGNAL_CONSUMERS table with a re-export of the generated ledger and update documentation around what generation enforces.
  • Delete the manual SIGNAL_CONSUMERS array definition from consumers.rs.
  • Add a pub use super::tags::SIGNAL_CONSUMERS; re-export, documenting that totality is now compile-enforced.
  • Clarify module docs in consumers.rs about what generation provides, why structural LedgerViolation kinds are retained, and what audit_ledger still checks.
crates/stella-protocol/src/event/consumers.rs
Wire the new tags module into the event module while keeping the public KNOWN_TYPE_TAGS API stable.
  • Add mod tags; to event.rs alongside existing modules.
  • Re-export tags::KNOWN_TYPE_TAGS from event.rs with comments explaining path stability for crate::KNOWN_TYPE_TAGS and existing references.
  • Remove the inline agent_event_tags! macro and its table definition from event.rs, delegating all tag-related generation to tags.rs.
crates/stella-protocol/src/event.rs

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Base automatically changed from worktree-signal-consumer-ledger to main August 10, 2026 21:29
…mer-tags-macro

# Conflicts:
#	AGENTS.md
#	crates/stella-protocol/src/event.rs
#	crates/stella-protocol/src/event/consumers.rs
@macanderson
macanderson merged commit d2fe72f into main Aug 10, 2026
16 checks passed
@macanderson
macanderson deleted the worktree-signal-consumer-tags-macro branch August 10, 2026 21:52
macanderson added a commit that referenced this pull request Aug 10, 2026
…build error (#2755)

## What & why

Invariant #10 says the signal-consumer ledger's totality is enforced by
tests — "adding a variant without declaring what consumes it is a red
test." That was true when #2720 landed and stopped being true two
commits later: #2737 generated the ledger rows from the tag table, so
the check is now an `E0004` at `cargo build`.

Caught by re-reading main after both merges landed. Per CLAUDE.md a
stale doc is a bug, and this one is mine — I wrote the prose in #2720
and changed the mechanism in #2737 without chasing the claim.

The correction is not cosmetic: it **understates** the guarantee, and it
misdirects the next person adding an `AgentEvent` variant about where
the failure will surface.

It also tightens the closing "what it does not prove" paragraph, which
is the part most worth keeping exact — totality is now
compiler-enforced, while issue citation and posture coherence remain
test-enforced. Saying "tests" for both blurs the line that paragraph
exists to draw.

## The witness

- [x] No witness needed — documentation only, correcting a claim about
existing behavior. The behavior it now describes is witnessed in #2737,
where a probe variant produces `error[E0004]: non-exhaustive patterns`.

## Deleted tests

None.

## The gate

- [x] `scripts/check-invariants.sh` — OK, 10 invariants, one normative
home, 28 citations resolve

Refs #2701
Refs #2730

## Summary by Sourcery

Documentation:
- Clarify that adding an AgentEvent variant without a consumer now
triggers a compile-time E0004 error rather than a failing test and
tighten the distinction between compiler- and test-enforced guarantees
in the invariant description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

protocol: split the tag macro out of event.rs so the signal-consumer ledger can be compile-enforced instead of test-enforced

1 participant