Scry · Conformance review

Conformance review

A guardrail agent that reads your DSL files, reactor code, and live scrolls and flags anti-patterns before they harden into production incidents. Runs in-loop with Claude Code, not as a post-hoc linter. Doesn't just flag — proposes the refactor.

Why a guardrail, not a reviewer

A post-hoc linter catches what's already written. By the time the flag fires, the pattern is embedded in the shape of the reactor, the signature of the event, the expectation of the downstream fold. Removing it costs a refactor.

A guardrail catches the intent before it crystallizes. Claude Code is about to write a handler that calls repo.Get() inside a HandleFunc; the reviewer stops it and suggests the fold. Claude Code proposes an event with a field string payload; the reviewer counter-proposes one topic per field. The cost of the correction is seconds, not days.

This only works because weave is introspectable: the DSL names every event, the reactor declaration names every role, and the scroll contains every outcome. A language without this surface can't support the same loop.

Static and dynamic

Static review

Reads the .weave file, the generated Go types, and the reactor declarations. Pure code analysis — no runtime data required, fast enough to run on save.

  • Discriminator fields in events
  • Missing role annotations
  • Catch-up barrier calls
  • Projection queries inside reactor handlers
  • Fire-and-forget dispatch patterns
  • Producer/consumer interface leakage

Dynamic review

Reads the live scroll (or a recorded one) and checks ordering, completeness, and invariants that only emerge in the event stream. Runs on a branch or against a corpus.

  • Out-of-order signal acceptance
  • Proposals without matching verdicts
  • Signal_accepted without a gate_checked predecessor
  • Reactors processing events before their source scroll caught up
  • Domain writes without matching scroll events
  • Prompt versions that produced divergent outputs on identical inputs

The anti-pattern catalog

Each rule is versioned, documented, and carries a suggested fix. This list is the starting set — extracted from real incidents in production weave deployments. New rules land as new shapes of mistake get observed.

Projection read inside a reactor handler

static + dynamic

Why it hurts. Forces catch-up barriers and reintroduces the race the reactor model was supposed to eliminate. A reactor is a fold over scroll events; reading its own downstream projection inverts the dataflow.

Fix. Replace the projection query with scroll.FoldEvents over the source scroll. Consistent with the latest append by construction, no barrier needed.

Event with a discriminator field

static

Why it hurts. Events like quest_detail_proposed { field, value } force every consumer to switch on field. The topic stops being the schema; type safety evaporates; conflict detection requires payload inspection.

Fix. Split into one event per atomic fact: quest_title_proposed, quest_reward_proposed, quest_location_proposed. Folds become trivial; reactors route by topic.

Synchronous catch-up barrier (projection.CatchUp)

static

Why it hurts. Symptom of sync thinking in an async system. The caller is waiting for a projection to consume before reading — which means it should have subscribed to the producing scroll instead.

Fix. Invert: subscribe to the source topic, not the projection. If the UI needs the barrier, the UI is the only legitimate caller — reactors never are.

Fire-and-forget reactor dispatch

static + dynamic

Why it hurts. The RPC returns before the reactor pipeline finishes. UI races the projector. Every later fix of the form 'force synchronous CatchUp' is a downstream symptom.

Fix. Use consumer-group subscription via scroll-server. Completion is a scroll event, not a goroutine return.

Producer/Consumer leakage

static

Why it hurts. A reconciler accidentally emits a proposal event, or the innkeeper agent writes its own acceptance verdict. The write-side separation has no enforcement.

Fix. Annotate events with role: producer | consumer | marker | lifecycle in the .weave file. DSL generator emits role-specific interfaces; violations fail to compile.

Domain write outside scroll (DB side-channel)

static + dynamic

Why it hurts. Projector writes both a domain scroll event and a database document. Crash between them and the system diverges from itself.

Fix. Domain scroll is source of truth. The database is a read projection maintained by a separate fold. Projectors only write scroll events.

Undeclared reactor role

static

Why it hurts. NewReactor without a declared role (reconciler, projector, enricher, conflict detector, intent applier) means reviewers and tooling can't verify the reactor follows the right post-conditions.

Fix. Declare weave.Reconciler(...), weave.Projector(...), etc. Framework enforces shape — reconcilers must emit a verdict per input, projectors must be idempotent, enrichers must handle per-item failures.

Out-of-order signal acceptance

dynamic

Why it hurts. A signal_accepted event lands on the signals scroll before the corresponding gate_checked — the gate was bypassed. Only visible in the live event stream.

Fix. Scry diagnoses the divergence causally: which reactor emitted without its gate, which fold didn't include the gate event, which ordering invariant was violated.

The refactor proposer

Detection is half the value. The other half is the before/after diff. The reviewer emits:

  • The .weave change — which events get split, which roles get annotated, which scrolls get renamed.
  • The reactor code change — which folds replace which reads, which handlers move from fire-and-forget to consumer groups, which role types replace raw NewReactor.
  • The rationale — linked to the rule that triggered, with a one-paragraph explanation.
  • The impact estimate — how many callers change, how many tests get invalidated, whether a corpus replay is required to verify.

Claude Code applies the diff, re-runs the reviewer, and sees the finding clear. Or pushes back with context the rule didn't have — and the rule learns.

In-loop and batch

Two modes, different cost profiles.

# In-loop (runs on save or on tool call)
scry review --file internal/tavern/reactor.go --fast
 returns in < 500ms
 scope: one file, no scroll access
 catches: 80% of static anti-patterns

# Batch (runs on a branch + corpus)
scry review --repo . --corpus ./scrolls --out results/
 runs in ~minutes for a repo-scale sweep
 scope: cross-file + scroll-aware
 catches: everything the in-loop version misses
                  + dynamic violations
                  + prompt-version divergence

In-loop runs as a Claude Code hook or editor LSP extension. Batch runs in CI or on demand. Both share the same rule catalog and refactor proposer; only the evidence they see differs.

Example: a signal_accepted race

A live quest-intake pipeline emits signal_accepted before the corresponding gate_checked event on the signals scroll. The grounding gate was bypassed. Static review sees nothing wrong — the reactor code looks fine. Dynamic review, reading the scroll, spots the ordering violation:

sequence 42: quest_proposed       { signalId: s_9 }
sequence 43: signal_accepted     { signalId: s_9 } problem
sequence 44: gate_checked        { signalId: s_9, evidence: }

Violation: quest-evidence-gate
  signal s_9 accepted at sequence 43 before gate check at 44.
  Invariant: every signal_accepted must be preceded by a
  matching gate_checked on the same signalId.

Root cause (proposed):
  reconcile-quest handler emits signal_accepted directly on
  verdict=accept, without awaiting the gate's output. The gate
  is a separate reactor; its cursor can lag.

Refactor:
  Split the acceptance into two stages:
    1. reconcile-quest emits signal_pending_gate
    2. gate reactor folds, emits gate_checked + signal_accepted
  OR: make the gate a synchronous fold inside reconcile-quest.

Impact: 1 reactor file, 1 .weave file, 2 callers, 0 fixtures
  invalidated. Corpus replay recommended.

Why this is the bootstrap capability

Every other Scry capability — replay, test generation, bisection, corpus analysis — reasons over scroll and code shapes. If those shapes are inconsistent, ambiguous, or drifting, the downstream tools reason over garbage. The conformance reviewer is what keeps the substrate clean enough for everything above it to work.

It's also what makes Claude Code viable as an author of production weave code. Without the reviewer, the implementation guide has to live in every prompt. With it, the rules live in the framework — coached in-loop, enforced at the boundary, refactored automatically when they drift.

Status

The reviewer is a Scry v1 capability. Its rules are the shape the implementation guide already documents, but the agent and integration surface are not yet shipped.

Static review: .weave + reactor code
designed
Reads the DSL file, generated types, and reactor declarations. Emits a JSON punch list — one entry per violation with file, line, pattern, and proposed fix.
Dynamic review: scroll shape
designed
Reads the live or recorded scroll, checks ordering invariants, detects gate bypass, surfaces signals accepted without evidence or without a projection marker.
Refactor proposer
designed
For each finding, emit the before/after .weave diff and the before/after reactor Go diff. Claude Code applies, review re-runs, verdict changes to clean.
In-loop mode (Claude Code hook)
designed
Runs on save or on tool call. Fast, narrow, single-file scope. Keeps the feedback loop under 500ms so it doesn't disrupt editing.
Batch mode (branch sweep)
designed
Runs against a repo + a scroll corpus. Deep, cross-file, cross-scroll. Produces a report that gates merge.
Rule catalog
designed
The pattern library the agent reasons over. Each rule is versioned, documented, and can be disabled per-repo via .weave.yaml. Extensible: app-specific invariants can be added alongside the built-ins.