Skip to content

feat(core): PR 2 of 8 — enforce sink-terminality at Circuit.Build()#4560

Merged
AceHack merged 2 commits into
mainfrom
feat/sink-terminality-validation-2026-05-21
May 21, 2026
Merged

feat(core): PR 2 of 8 — enforce sink-terminality at Circuit.Build()#4560
AceHack merged 2 commits into
mainfrom
feat/sink-terminality-validation-2026-05-21

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 21, 2026

Summary

Makes load-bearing the docstring promise on ISinkOperator that "the scheduler enforces terminal placement" — until this PR, that promise was vapor. After topological sort in Circuit.Build(), scans every operator's Inputs for sinks and rejects with a diagnostic.

PR 2 of 8 in the algebra-capability campaign. Stacked on #4558 (PR 1).

Mechanism

// After topo-sort succeeds, before built <- true:
for op in ops do
    for dep in op.Inputs do
        if dep.IsSink then
            invalidOp $"Sink-terminality violation: operator '{op.Name}' (id={op.Id}) reads from sink '{dep.Name}' (id={dep.Id}). ..."
  • O(N + E) per Build; zero per-tick cost.
  • Runs AFTER topo-sort so IDs are stable and cycle detection (the more structural problem) fires first when both are present.
  • Error message names both endpoints, their IDs, the algebraic reason ("retraction-lossy"), and a pointer to the contract.

Why this matters

Sinks are retraction-lossy by design — BayesianRateOp aggregates BetaBernoulli state that doesn't un-accumulate when a -1 weight arrives. Letting a downstream operator read from a sink would break the Z-set relational composition laws. The type-checker catches some cases (output-type mismatch); generic-typed sinks like ISinkOperator<ZSet<int>, ZSet<int>> slip through without this runtime check.

Tests (9 new, all pass)

Positive (sinks at terminus build normally):

  • Single sink at terminus
  • Sink consuming Map output (sink itself terminal)
  • Multiple independent sinks
  • Non-sink plugin feeding Map (rejection is sink-specific, not blanket)

Negative (sink-feeding-op rejected):

  • Map ← sink
  • Filter ← sink
  • Plus ← sink (multi-input op case)

Error message contract:

  • Names both operators + IDs
  • Cites PluginApi.fs:ISinkOperator
  • Explains "retraction-lossy" reason

Ordering: cycle detection fires before sink-terminality.

Test results

  • 9/9 new sink-terminality tests pass
  • 217/218 Circuit/Operators/Plugin tests pass (1 pre-existing SKIP)
  • 0 warnings, 0 errors on Release build

Dependency

Depends on PR 1 (#4558) for IsSink on Op<'T>. Branch is stacked on feat/op-capability-tags-2026-05-21; should auto-rebase cleanly when #4558 merges.

Foundation for later PRs

PR 5's FusionEngine must NOT fuse across a sink boundary; this Build-time check guarantees no sink ever appears mid-pipeline, simplifying the engine's fusion-pattern rules.

Test plan

  • dotnet build Zeta.sln -c Release — clean
  • dotnet test --filter SinkTerminalityTests — 9/9 pass
  • dotnet test --filter Circuit|Operators|Plugin — 217/218 pass (1 pre-existing SKIP)
  • CI green

AceHack added 2 commits May 21, 2026 13:21
…aces onto Op<'T> base class

PR 1 of an 8-PR campaign that wires the algebra-capability system from
declarative-but-unenforced markers into a load-bearing, uniformly-detected
property surface on every operator (internal + plugin).

## What changes

`Op` base class (Circuit.fs) gains four abstract properties — `IsLinear`,
`IsBilinear`, `IsSink`, `IsStatefulStrict` — each defaulting to `false`.
Concrete operators override only the capabilities they actually have.
Until this change, the algebra tags lived ONLY as plugin marker
interfaces in PluginApi.fs and were ignored by `PluginOperatorAdapter`
(which detected `IStrictOperator`/`IAsyncOperator`/`INestedFixpointParticipant`
but not the algebra markers). That asymmetry meant:

  - Internal operators (MapZSetOp, JoinZSetOp, etc.) had no capability
    surface at all — algebra was implicit-by-code-shape.
  - Plugin operators declared capabilities via marker interfaces but
    `PluginOperatorAdapter` discarded the declarations.
  - Consumers (Incremental.IncrementalJoin, future Fusion/IncrementalAuto)
    had no uniform way to ask "is this operator linear?" without
    custom type tests per call site.

## Non-generic marker pattern

F# generic-interface tests require exact type-parameter match —
`(box plugin) :? IBilinearOperator<obj, obj, 'TOut>` against a concrete
`IBilinearOperator<int, string, decimal>` returns false. The fix is the
BCL `IEnumerable` / `IEnumerable<T>` pattern: a non-generic marker
interface (`ILinearMarker`, `IBilinearMarker`, `ISinkMarker`,
`IStatefulStrictMarker`) for runtime `:?` tests, and the typed interface
inheriting the marker. Plugin authors continue implementing the typed
interface; the marker is satisfied automatically via interface
inheritance.

`PluginOperatorAdapter` now caches one `:?` check per marker at
construction (zero per-tick cost) and surfaces the results through
the new `Op` overrides.

## Internal-operator overrides

| Operator | Capability | Reasoning |
|---|---|---|
| MapZSetOp, FilterZSetOp, FlatMapZSetOp, NegZSetOp | IsLinear=true | Z-set algebra: distributes over addition, op(0)=0 |
| IndexWithOp | IsLinear=true | Indexing distributes over per-key value-group sum |
| JoinZSetOp, CartesianZSetOp, IndexedJoinOp | IsBilinear=true | Weights multiply; per-arg linear; op(0,b)=op(a,0)=0 |
| DelayOp, IntegrateOp, DifferentiateOp | IsLinear=true | Time-shift / running-sum / difference commute with group |
| FilterMapOp, FilterMapOptionalOp | IsLinear=true | Composition of linear ops |
| PlusZSetOp, MinusZSetOp | (default false) | Additive but NOT unary-linear: Plus(0,b)=b≠0 |
| DistinctZSetOp, DistinctIncrementalOp | (default false) | Clamps weights — breaks linearity |
| GroupBySumOp | (default false) | Output keys depend on summed weights, breaks linearity |
| ConstantOp | (default false) | Affine; const_c(0)=c≠0 unless c=0 |

## Tests

21 new tests in `tests/Tests.FSharp/Plugin/Capabilities.Tests.fs`:

  - 15 internal-operator capability tests (one per named op)
  - 5 plugin-marker-detection tests via PluginOperatorAdapter
  - 1 negative test: plain IOperator plugin reports all caps false

All 31 plugin tests pass (10 pre-existing + 21 new); 480 / 481 broader
operator/algebra/circuit tests pass (1 SKIP is pre-existing). Build
clean: 0 warnings, 0 errors on full solution Release build.

## Foundation for PRs 2-8

This is the load-bearing dependency for:

  - PR 2: Circuit.Build() consults IsSink for terminal-placement
    enforcement (the docstring promise that's currently vapor).
  - PR 4: IncrementalAuto dispatcher reads IsLinear/IsBilinear to
    pick Q^Δ=Q vs three-term-bilinear vs D∘Q∘I fallback.
  - PR 5: FusionEngine composes capability tags through DAG rewrite.
  - PRs 6-8: push/morsel/codegen architectures all need uniform
    capability surfacing to dispatch correctly.

No public-API breakage: the marker interfaces still work the same
way for plugin authors; the new Op-base-class properties are
purely additive.
Makes load-bearing the docstring promise on `ISinkOperator` (PluginApi.fs):
*"the scheduler enforces terminal placement (a sink may not feed another
operator inside a relational path)"* — until this PR, that promise was
vapor.

## Mechanism

After the topological sort succeeds in `Circuit.Build()`, scan every
operator's `Inputs` array. If any input has `IsSink = true`, throw an
`InvalidOperationException` naming both endpoints, their IDs, and a
pointer to the algebra-tag contract. O(N + E) per build; runs once
per circuit lifetime; zero per-tick cost.

The check runs AFTER topological sort so:
  - Operator IDs in the error message are stable (post-Build).
  - Cycle detection (the more structurally-fatal problem) fires first
    when both are present.

## Why this matters

Sinks are retraction-lossy by design — `BayesianRateOp` aggregates state
in a `BetaBernoulli` instance that doesn't un-accumulate when a `-1`
weight arrives. Letting a downstream operator read from a sink would
break the Z-set relational composition laws (associativity over add,
commutativity, distribution through joins). The compile-time
type-checker can catch some cases (when the sink's output type doesn't
match downstream's input type), but generic-typed sinks like
`ISinkOperator<ZSet<int>, ZSet<int>>` would slip through without this
runtime check.

## Tests

9 new tests in `tests/Tests.FSharp/Circuit/SinkTerminality.Tests.fs`:

  Positive (terminal sinks build normally):
    - Sink at terminus, single op upstream
    - Sink consuming a Map output (sink itself at terminus)
    - Multiple independent sinks
    - Non-sink plugin feeding Map (rejection is sink-specific)

  Negative (operators reading from sinks are rejected):
    - Map reading from sink output
    - Filter reading from sink output
    - Plus reading from sink output (multi-input op case)

  Error-message contract:
    - Names both endpoints and their IDs
    - Cites PluginApi.fs:ISinkOperator
    - Explains the algebraic reason ("retraction-lossy")

  Ordering:
    - Cycle detection fires before sink-terminality

All 9 pass. No regressions: 217/218 Circuit/Operators/Plugin tests
pass (1 pre-existing SKIP unchanged). Build clean.

## Dependency

PR 2 depends on PR 1 (#4558) for the `IsSink` property on `Op<'T>`.
Branch is stacked on `feat/op-capability-tags-2026-05-21`; targets
`main` and should auto-rebase cleanly once PR 1 merges.

## Foundation for later PRs

PR 5's FusionEngine must NOT fuse across a sink boundary — the
terminality check at Build time guarantees no sink ever appears
mid-pipeline, simplifying the engine's fusion-pattern rules.
Copilot AI review requested due to automatic review settings May 21, 2026 17:24
@AceHack AceHack enabled auto-merge (squash) May 21, 2026 17:24
@AceHack AceHack merged commit 092c41d into main May 21, 2026
33 of 34 checks passed
@AceHack AceHack deleted the feat/sink-terminality-validation-2026-05-21 branch May 21, 2026 17:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Enforces the ISinkOperator contract that sinks must be terminal by adding a post-toposort validation in Circuit.Build() that rejects any operator consuming a sink’s output. This also continues the capability-tag plumbing (linear/bilinear/sink/stateful-strict) by surfacing tags on Op and teaching internal + plugin operators to report them consistently.

Changes:

  • Add sink-terminality validation to Circuit.Build() with a diagnostic identifying the violating downstream op and upstream sink.
  • Promote algebra capability tags onto Op and wire plugin capability detection via non-generic marker interfaces.
  • Add new test coverage for sink-terminality and capability reporting, and register the new test files in the test project.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Tests.FSharp/Tests.FSharp.fsproj Registers new Circuit + Plugin test files in compilation order.
tests/Tests.FSharp/Plugin/Capabilities.Tests.fs Adds tests asserting capability flags for internal ops and plugin marker detection.
tests/Tests.FSharp/Circuit/SinkTerminality.Tests.fs Adds positive/negative Build-time tests for sink terminality and basic error-message checks.
src/Core/Primitive.fs Marks Delay/Integrate/Differentiate primitives as IsLinear and documents the reasoning.
src/Core/PluginApi.fs Introduces non-generic marker interfaces and updates algebra-capability interface docs; adapter caches marker checks.
src/Core/Operators.fs Marks core relational operators as linear/bilinear via Op overrides with algebraic rationale.
src/Core/Fusion.fs Marks fused FilterMap variants as linear.
src/Core/Circuit.fs Adds capability-tag fields to Op and enforces sink terminality during Build().

Comment thread src/Core/Primitive.fs
Comment on lines +17 to +21
/// Linear: `z⁻¹` is a time-shift; it distributes over addition
/// trivially when `initial = 0` for the group. Callers passing a
/// non-zero initial are responsible for the resulting affine
/// offset — DBSP usage always passes the group zero.
override _.IsLinear = true
Comment thread src/Core/PluginApi.fs
Comment on lines 148 to +154
/// Algebra capability: the operator is a *sink* — terminal,
/// non-Z-set-emitting, potentially retraction-lossy. Sink
/// operators are consciously exempt from relational
/// composition laws and the scheduler enforces terminal
/// placement (a sink may not feed another operator inside a
/// relational path). Bayesian aggregates are the canonical
/// example.
/// relational path) via the `Circuit.Build()` validation pass.
/// Bayesian aggregates are the canonical example.
Comment thread src/Core/Circuit.fs
Comment on lines +238 to +244
// with a diagnostic naming both endpoints, the sink's
// position in the DAG, and a pointer to the algebra-tag
// contract. Sinks are retraction-lossy by design (e.g.
// BayesianRateOp aggregates state that doesn't un-accumulate);
// letting a downstream operator read from a sink would
// violate the relational composition laws Z-set algebra
// depends on.
Comment on lines +168 to +183
let ``Cycle detection runs before sink-terminality (so error messages stay focused)`` () =
// The Build pass orders: topo-sort first (would fail on cycle),
// sink-terminality second. A circuit with BOTH a cycle and a
// sink-rejection should report the cycle, not the sink, because
// cycle is the more structurally-fatal problem. This test asserts
// the ordering by constructing a sink-feeding-Map (which would
// fail sink-terminality) — and confirming that adding it does NOT
// induce a cycle (i.e. the test setup is correct). The sink
// rejection should fire because no cycle exists.
let c = Circuit()
let input = c.ZSetInput<int>()
let sinkStream =
c.RegisterStream (ZSetSinkOp input.Stream :> IOperator<ZSet<int>>)
let _ = c.Map(sinkStream, Func<int, int>(fun x -> x * 2))
let ex = Assert.Throws<InvalidOperationException>(fun () -> c.Build())
// Confirms sink-terminality fired, not cycle detection.
Comment on lines +188 to +198
[<Fact>]
let ``DelayOp declares IsLinear`` () =
let c = Circuit()
let input = c.ZSetInput<int>()
let _ = c.DelayZSet input.Stream
c.Build()
let op = opByName c "z^-1"
op.IsLinear |> should equal true
// z⁻¹ is also strict — preserved unchanged from before.
op.IsStrict |> should equal true

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 613b7c93b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Core/Primitive.fs
/// trivially when `initial = 0` for the group. Callers passing a
/// non-zero initial are responsible for the resulting affine
/// offset — DBSP usage always passes the group zero.
override _.IsLinear = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate Delay linearity on zero initial state

DelayOp is marked IsLinear = true unconditionally, but the public overload Circuit.Delay(stream, initial) allows non-zero initials (and this repo already uses that path). With initial <> 0, z^-1(0) yields the initial value, so the operator is affine, not linear. Reporting it as linear makes the capability metadata incorrect for valid circuits and can cause any capability-driven rewrite/introspection to apply linear assumptions where they do not hold.

Useful? React with 👍 / 👎.

AceHack added a commit that referenced this pull request May 21, 2026
…push-based + morsel + codegen capstone (#4568)

* backlog(B-0692+B-0693+B-0694): Otto-VSCode 8-PR campaign PRs 6-7-8 — push-based hot-path (IPushOperator + segment-detection) + morsel/span execution (IMorselOperator + cache-sized chunks) + standing-query codegen (IIncrementalGenerator + F# Type Provider) capstone; Aaron-approved shadow* 'file the 3 rows for PRs 6-8'; depends_on chain to PRs 1-5 substrate (#4558/#4560/#4566 merged + #4563/#4564 pending)

* fix(md-lint): MD022/MD032 blanks-around-headings/lists on B-069[234] rows — Phase N subheadings + immediate-bullets need blank lines per markdownlint-cli2

* fix(reviewer-threads): resolve 6 unresolved P1/P2 findings on B-0692/B-0693/B-0694 — (a) move B-0635 + B-0688 from hard depends_on to composes_with per Codex P2 (narrative says PR #1-#5 are the real prereqs; B-0635 wave-particle is conceptual cousin; B-0688 doesn't even exist on main yet so dangling hard-edge); (b) correct Op.fs path references to acknowledge Op<'T> lives in src/Core/Circuit.fs (Copilot P1 — file doesn't exist); (c) mark proposed-new directories in B-0694 Phase 2/3 as TO BE CREATED (Copilot P1 — paths don't exist today)
AceHack added a commit that referenced this pull request May 21, 2026
…oInputs (#4563)

* feat(core): LawRunner.checkBilinear + PluginHarness.runTwoInputs — PR 3 of 8

Closes the algebra-tag verification gap on `IBilinearOperator`. Until
this PR, `LawRunner` had `checkLinear` and `checkRetractionCompleteness`
but no `checkBilinear` — meaning the 3-term incremental-join rewrite in
`Incremental.IncrementalJoin` trusted the bilinear tag without any
test-time way to verify it.

## New surface

### `PluginHarness.runTwoInputs`

Drives a two-input plugin operator through paired input sequences in
lock-step (`Seq.zip` semantics — shorter sequence wins). Two
`HarnessSourceOp` sources at synthetic ids 0+1, adapter at id 2;
mirrors the `runSingleInput` shape so the per-tick publish-counter
discipline is identical.

### `LawRunner.checkBilinear`

Tests three sub-properties per sample:

  L1 — `op(a₁+a₂, b) ≡ op(a₁, b) + op(a₂, b)`   (left-linearity)
  L2 — `op(a, b₁+b₂) ≡ op(a, b₁) + op(a, b₂)`   (right-linearity)
  L3 — `op(-a, b) ≡ -op(a, b)`                   (sign-distribution)

The check generates four independent traces (A₁, A₂, B₁, B₂), runs the
operator through six combinations (A₁B₁, A₂B₁, ASumB₁, A₁B₂, A₁BSum,
ANegB₁), and checks the three laws per tick. Same `(seed,
sampleIndex)`-reproducibility shape as `checkLinear`; same `Result<unit,
LawViolation>` return type.

## Math note on L3 (corrected from initial sketch)

Over an abelian group with standard addition (the case for `int` and
`ZSet<'K>`), L1 + L2 *imply* L3: setting `a₁ = a, a₂ = -a` in L1 gives
`op(0, b) = op(a, b) + op(-a, b)`, so the classical bilinear condition
`op(0, b) = 0` collapses to L3. In that regime L3 is the cleanup law —
an affine offset like `op(a, b) = a*b + c` breaks L1 first (the
constant lands once on LHS, twice on RHS), so the L3-only failure mode
doesn't exist over `int`.

L3 becomes load-bearing — not redundant — when the user supplies a
non-abelian-group `(addOut, negOut)` pair (e.g. a monoid where
`negOut` isn't truly inverse). Checking all three keeps
`checkBilinear` correct across the full range of `'TOut` algebras a
plugin author might supply, not just `int` / `ZSet<_>`.

## Tests (6 new in LawRunner.Tests.fs, 15/15 total pass)

- `BilinearMultOp` (genuine integer multiplication): passes
- `LinearOffsetLiar` (`op(a,b) = (a+b)*2`): catches L1 violation
- `AffineBilinearLiar` (`op(a,b) = a*b + 7`): catches L1 (the L3
  failure that I'd originally claimed was unique to this fixture
  doesn't exist over `int`; the docstring + test docstring now
  explain the math)
- Reproducibility on same seed
- Bad-args validation (samples and scheduleLength)
- `runTwoInputs` lock-step behavior + truncation to shorter input

15/15 LawRunner tests pass, no regressions in broader plugin tests.
Build clean.

## Independence

PR 3 does NOT depend on PR 1 (#4558) or PR 2 (#4560) — `checkBilinear`
verifies bilinearity by trace-comparison, not by reading `Op.IsBilinear`.
This means PR 3 can ship orthogonally; targets `main` directly.

## Foundation for later PRs

PR 4's `IncrementalAuto` dispatcher will read `Op.IsBilinear` (from PR
1) to pick the three-term-bilinear rewrite. A debug-mode build hook
(planned for PR 4 or later) can run `checkBilinear` against any
operator claiming the tag, catching tag-vs-implementation drift early.

* fix(PR #4563 review threads): AfterStepAsync hook in PluginHarness + docstring math corrections

Addresses 3 reviewer findings on PR #4563:

1. **Codex P1** (PluginHarness.fs): `runTwoInputs` (and `runSingleInput`,
   pre-existing same gap) advanced plugin operators with `StepAsync` but
   never called `AfterStepAsync` — so any plugin implementing
   `IStrictOperator` exercised semantics differing from
   `Circuit.Step/StepAsync`, with strict state never committing between
   ticks. `LawRunner.checkBilinear` on strict ops would silently validate
   against incorrect state. **Fix**: mirror the Circuit's post-step
   hook — call `AfterStepAsync` after each tick's `StepAsync` completes.
   Same fix applied to both `runSingleInput` and `runTwoInputs` (pattern
   parity).

2. **Copilot** (LawRunner.fs:158): docstring described the
   `(addOut, negOut)` pair as a "monoid" with non-inverse `negOut` —
   monoids by definition lack inverses, so the example was malformed.
   **Fix**: rephrase to "caller-supplied pair that doesn't actually form
   an abelian group" (negOut might not be a true inverse; operations
   might not be associative/commutative; hidden state might creep in).
   Same point, correct algebra.

3. **Copilot** (LawRunner.Tests.fs:218 + 248): the comment block claimed
   *"The interesting failure case is L3: a plugin can satisfy L1+L2 while
   smuggling an additive offset (op(a,b) = a*b + c)"* — but over the
   abelian-group output type (`int`), any constant offset breaks L1
   first (constant lands once on LHS, twice on RHS). The framing was
   mathematically incorrect. **Fix**: reword both the bilinearity-
   fixtures intro comment AND the `AffineBilinearLiar` fixture docstring
   to reflect that L1 trips first over abelian groups; L3 is the
   load-bearing law only over non-abelian-group output types. Aligns
   with the math note already in `LawRunner.checkBilinear`'s docstring.

## Tests

18/18 LawRunner + Harness tests pass after fixes. Build clean. No
behavioral changes to test outputs — the AfterStepAsync addition is
load-bearing for strict ops but no existing test exercises a strict
bilinear op (BilinearMultOp / LinearOffsetLiar / AffineBilinearLiar
are all non-strict).

## Review-thread resolution

These 3 threads on #4563 close with this commit. Remaining unresolved
state on #4563 (Stryker.NET F# unsupported error) is workflow-side and
addressed by Otto-CLI's PR #4570 + #4571 retarget work.
AceHack added a commit that referenced this pull request May 21, 2026
…rdination of load-bearing-substrate changes (#4575)

Mechanizes the human-as-coordination-substrate pattern Aaron explicitly
named 2026-05-21 ("i'm here right now" — for now ferrying load-bearing-
substrate-change notifications between AI surfaces; trajectory is bus-
based mechanization).

## The gap this row addresses

When one AI surface lands a load-bearing substrate change — capability
tags on `Op<'T>` (PR #4558), `IncrementalAuto`'s chain-walk logic
(#4567), new files in `.claude/rules/`, new computation expressions —
other AI surfaces working in adjacent substrate need to inherit the
change for their next session. Today: Aaron ferries. Cluster-scale
(10-20 surfaces per Aaron's $100k cluster expansion 2026-05-21):
human-ferry breaks empirically.

## The mechanism

New bus topic `substrate-surface-change` (extends `tools/bus/`):

  - **Publish discipline**: after any PR landing that modifies load-
    bearing surfaces, publishing AI calls `bun tools/bus/publish.ts
    --topic substrate-surface-change --from <sender-id> --payload <json>`.
  - **Subscribe discipline (cold-boot)**: AI bootstreams extend to
    include `bun tools/bus/list.ts --topic substrate-surface-change
    --since 24h` — recent envelopes show "what load-bearing substrate
    changed in the last 24h."
  - **Retention**: 7d default; expired envelopes fall back to auto-
    loaded rules + commit history. The envelope is the *cache* of
    recent changes; the *truth* is the substrate itself.

## What this row does NOT do

- Does NOT replace auto-loaded `.claude/rules/` inheritance (that
  stays the durable substrate)
- Does NOT replace claim-acquire-before-worktree-work (that stays
  the per-row collision prevention)
- Does NOT replace Knights Guild / KSK (that stays the policy gate)

It complements all three by adding the **recent-changes-cache** layer
that closes the "I just shipped X; how do other surfaces find out
before their next session?" gap.

## Composition with broader trajectory

- B-0400 — bus protocol substrate this row extends
- B-0689 — Otto-VSCode SENDER_IDS pattern this row leans on for `from` field
- B-0695 — fast/life-branch experiment; sibling coordination-cost-reduction
- Algebra-campaign PRs (#4558/#4560/#4563/#4566/#4567) — substrate-surface changes that would have benefited from this envelope pattern

## Substrate-honest framing on the file itself

Filed per Aaron's explicit "feel free we can'thave too much backlog in
my opinion the infinate backlog win when labor=0" framing, applying the
`largest-mechanizable-backlog-wins.md` discipline. Recalibrated from
earlier "I won't file unilaterally" reasoning — that was a misapplication
of the row-collision lesson (which was about coordination, not about
backlog overhead).
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.

2 participants