diff --git a/docs/craft/subjects/zeta/operator-composition/module.md b/docs/craft/subjects/zeta/operator-composition/module.md new file mode 100644 index 00000000..70015f7b --- /dev/null +++ b/docs/craft/subjects/zeta/operator-composition/module.md @@ -0,0 +1,341 @@ +# Operator composition — snapping LEGO blocks into a pipeline + +**Subject:** zeta +**Level:** applied (default) + theoretical (opt-in) +**Audience:** contributors building / reading Zeta +pipelines + +**Prerequisites:** + +- `subjects/zeta/zset-basics/` (forthcoming — Z-sets are + what most operators consume and produce; until that + module lands, see `docs/ARCHITECTURE.md` and + `openspec/specs/operator-algebra/spec.md` for the + Z-set definition) +- `subjects/zeta/retraction-intuition/` (operators must + preserve retraction for IVM to work) + +**Next suggested:** `subjects/zeta/semiring-basics/` +(forthcoming — pluggable algebras behind operators) + +--- + +## The anchor — snapping LEGO blocks + +A LEGO block has a fixed set of studs on top and a fixed +set of sockets on the bottom. Any block can snap onto any +other block *because the interface is standardised*. You +don't re-engineer the studs each time; you rely on them +being compatible. + +A **Zeta operator** is a LEGO block for data pipelines. +Its *studs* are the typed inputs it accepts; its +*sockets* are the typed outputs it produces. Many core +operators transform `Stream>` to +`Stream>`, but composition is more general: one +operator can snap downstream of another whenever the +upstream operator's output type matches the downstream +operator's input type. Some operators (for example +`count`, `sum`) emit scalar streams (`Stream`) +rather than Z-set streams; these compose with operators +that accept scalars. + +**Composition is the act of snapping blocks together.** +A pipeline is a stack of blocks; the stack computes the +same thing each time the input changes, because the +algebra guarantees the composition. + +--- + +## Applied track — when / how / why composition matters + +### Why it matters + +In a retraction-native world, building pipelines *by +composition* (rather than one big hand-written query) +has three practical benefits: + +1. **Each block is testable in isolation.** A `filter` + block can be unit-tested on tiny inputs, without + spinning up the full pipeline. +2. **Retraction flows through automatically.** If every + block is retraction-preserving (per the prerequisite + module), the whole stack is retraction-preserving + by composition. +3. **Swaps are cheap.** If you need to replace a + `count` block with a `sum` block, the socket above + and stud below stay the same; only the middle + changes. + +This is the difference between a LEGO set and a carved +wooden model. Both can look the same; only one is +reconfigurable. + +### The core operators — what snaps to what + +Zeta ships a small core that covers most pipelines: + +| Operator | What it does | Input | Output | +|---|---|---|---| +| `D` (delta) | Extract the change (insertions + retractions) from a Z-set stream | Z-set(t) | ΔZ-set(t) | +| `I` (integral) | Reconstruct state by accumulating changes | ΔZ-set(t) | Z-set(t) | +| `z⁻¹` (delay) | Hold last-tick value; one-step lookback | Z-set(t) | Z-set(t-1) | +| `H` (`distinct^Δ`) | Incremental-distinct boundary-crossing operator (per `openspec/specs/operator-algebra/spec.md`) | ΔZ-set(t) | ΔZ-set(t) (with multiplicities clamped to {0,1}) | +| `filter` | Keep only entries satisfying a predicate | Z-set(t) | Z-set(t) | +| `map` | Transform keys via a function | Z-set(t) | Z-set(t) | +| `count` | Sum weights to a scalar | Z-set(t) | ℤ | + +Note: nested / recursive composition (one pipeline as +an element of another's input) is provided via the +`Circuit.Nest` / `Circuit.NestWithHandle` extension +methods (implemented in `src/Core/NestedCircuit.fs` +under `NestedCircuitExtensions`) and the +`circuit-recursion` / `retraction-safe-recursion` specs, +not via `H`. See the "Nested / recursive circuits" +section in the theoretical track below. + +Each input-type matches the previous operator's output- +type. Snap them. + +### How to use composition in Zeta + +A common pattern — "count the running total of apples +sold today, with retractions applied": + +```fsharp +// Pipeline composition using the real F# surface +// (Pipeline is [] and each +// function takes the Circuit as its first argument). +let c = circuit // a Zeta.Core Circuit value + +// Apples-only filter, integrated as a Z-set stream: +let applesOverTime = + input + |> Zeta.Core.Pipeline.filter c (fun k -> k.category = "fruit") + |> Zeta.Core.Pipeline.filter c (fun k -> k.name = "apple") + |> Zeta.Core.Pipeline.integrate c // Z-set integral + +// Running scalar count of the integrated apples Z-set: +let runningCount = + applesOverTime + |> Zeta.Core.Pipeline.count c // Stream +``` + +Each `|>` is a LEGO snap. Each step's output type +becomes the next step's input type. Note the order: +`integrate` is a Z-set-to-Z-set operator +(`Stream> -> Stream>`), so it must run +*before* `count` collapses the Z-set to a scalar; once +you have a `Stream`, the Z-set-typed operators +no longer apply. No hand-written glue code; the +type-checker enforces the sockets line up. + +**When retraction arrives**, each Z-set block forwards +the negative weight through. The `integrate` step folds +the deltas into running state correctly; downstream +scalar aggregations stay exact; the whole pipeline +"just works." + +### Why composition — compared to alternatives + +| Alternative | Problem | +|---|---| +| One big hand-written SQL query | Hard to test parts; impossible to swap a subsection; no guarantee about retraction semantics | +| Monolithic procedural code | Same as above, with less declarative reasoning available | +| Lambda architecture (speed + batch layers) | Maintains two separate pipelines that must agree; consistency bugs on their own | +| ETL pipeline frameworks | Composition is present but often retraction-unaware; stateful transforms need explicit re-processing logic | + +Composition wins when (a) the operators are algebraically +well-defined, (b) retraction semantics are preserved by +each, and (c) you want pipeline fragments to be +swappable. Zeta is built for exactly this case. + +### How to tell if your composition is right + +Three self-check questions: + +1. **Does each block's output type match the next + block's input type?** The F# compiler catches this. + If you hit a type error, the blocks don't snap — + fix the mismatch; don't bolt on glue. +2. **Is each block retraction-preserving?** Check the + operator's documentation against the + retraction-safety constraints in the + `retraction-intuition` module and + `openspec/specs/operator-algebra/spec.md`. If the + operator's documented semantics do not preserve + retraction (or only preserve it under qualifications + such as time-invariance or z-linearity), your + pipeline needs explicit care. +3. **Would you be comfortable swapping any one block + for its replacement?** If yes, the composition is + honouring LEGO-style modularity. If you'd have to + rewrite surrounding code, something is coupled + that shouldn't be. + +--- + +## Prerequisites check (self-assessment gate) + +Before the next module, you should be able to answer: + +- Why does the `|>` pipeline operator in F# work as a + composition mechanism for Zeta operators? (Hint: each + operator's output type is what the next one consumes.) +- Give an example pipeline where `filter` comes *before* + `count`. Then explain why a `map` *after* `count` + cannot type-check against the documented F# surface + (`Pipeline.count` produces `Stream` while + `Pipeline.map` consumes `Stream>`) — what + does this tell you about the order in which scalar + aggregations and Z-set transformations must appear? +- What happens downstream when an operator in the middle + of a pipeline receives a retraction (negative-weight + entry)? Do the downstream operators need to know they + received a retraction, or does it "just flow"? + +--- + +## Theoretical track — opt-in (for learners who really care) + +*If applied is enough, stop here. The below is for those +going deep.* + +### Operators as categorical arrows + +In category-theoretic terms, a Zeta operator `Q : ZSet K +→ ZSet L` is an arrow in a category whose objects are +Z-set types. Composition `Q_2 ∘ Q_1` is arrow +composition. The LEGO anchor is literally categorical: +the *studs* and *sockets* are the types, the *blocks* are +the arrows, and *snapping* is composition. + +### The DBSP operator signatures + +From Budiu et al. VLDB 2023 §2: + +- `D : Stream → Stream` (pointwise delta) +- `I : Stream → Stream` (pointwise + cumulative integral) +- `z⁻¹ : Stream → Stream` (one-step + delay) +- `lift(f) : Stream → Stream` where + `f : ZSet K → ZSet L` is a function (the point-lift) +- Bilinear operator / join: `⋈ : Stream × + Stream → Stream` + +These compose via arrow composition. The DBSP paper +proves several identities that let us *rewrite* a +composition into an equivalent, often cheaper form — the +basis of Zeta's query-plan optimiser. + +### Key identities + +- **For causal streams with a declared zero at `t=0`, + D ∘ I = I ∘ D = id** (integral and delta invert each + other on that domain; the algebra's fundamental + theorem — see `openspec/specs/operator-algebra/spec.md` + for the precondition) +- **Q^Δ = D ∘ Q ∘ I** (the incremental form of any + query `Q`; this is the rewrite the optimiser uses to + turn a batch query into one whose work-per-tick is + proportional to the change size — see + `src/Core/Incremental.fs`) +- **D ∘ Q = Q ∘ D** when `Q` is a time-invariant linear + operator (delta commutes with such `Q`; this is the + non-trivial commutation law; bare associativity of + composition is not what enables incrementalisation) +- **I ∘ (Q_1 + Q_2) = (I ∘ Q_1) + (I ∘ Q_2)** (integral + is linear) + +These identities enable incremental maintenance: given a +query `Q = Q_n ∘ ... ∘ Q_1`, its incremental version is +`D ∘ Q ∘ I`, which (by the identities) can be rewritten +into a form whose work-per-tick is proportional to the +change size, not the state size. + +### Where composition fails + +Not every Zeta operator composes trivially. Specifically: + +1. **Non-z-linear operators** (per the retraction- + intuition module): composing them may break + retraction-preservation. Zeta flags these; compose + with explicit care. +2. **Stateful operators with side channels** (e.g., some + sketches that track auxiliary state): composition is + sound only if the composition respects the + operator's state semantics. +3. **Typing across semirings**: the same shape of + operator may not compose when applied over different + semirings; see the semiring-basics module + (forthcoming) for the parameterised picture. + +### Nested / recursive circuits + +Zeta supports composing pipelines as values — one +pipeline becomes an element of another's input Z-set, +and circuits can refer to themselves through a feedback +loop. This is how Zeta handles nested aggregations, +group-by-of-group-by, and recursive queries. The +implementation lives behind `NestedCircuit.Nest` (see +`src/Core/NestedCircuit.fs`); the `H` symbol from the +operator table above is reserved for incremental +distinct (`distinct^Δ`) per +`openspec/specs/operator-algebra/spec.md`. The +theoretical treatment of nesting / recursion is in: + +- `openspec/specs/circuit-recursion/spec.md` +- `openspec/specs/retraction-safe-recursion/spec.md` + +### Theoretical prerequisites (if going deeper) + +- Category theory — arrows, composition, functors, + natural transformations +- Stream algebra — time-indexed values, lift / delay / + integral / delta as stream operators +- DBSP paper — Budiu et al. VLDB 2023 is the primary + reference + +--- + +## Composes with + +- `subjects/zeta/zset-basics/` — inputs and outputs +- `subjects/zeta/retraction-intuition/` — each block + must preserve retraction for the composition to + preserve retraction +- `docs/ALIGNMENT.md` HC-2 — retraction-native + operations contract +- `docs/TECH-RADAR.md` — DBSP operator algebra Adopt +- `docs/ARCHITECTURE.md` §operator-algebra — full + architectural treatment +- `src/Core/Circuit.fs` — reference implementation of + composition +- `src/Core/NestedCircuit.fs` — nested / recursive + composition via `Circuit.Nest` / + `Circuit.NestWithHandle` extension methods (NOT the + `H` operator; `H` = `distinct^Δ` per the operator- + algebra spec) +- `openspec/specs/operator-algebra/spec.md` — formal + spec of the composable operator substrate +- `openspec/specs/circuit-recursion/spec.md` — recursive + composition + +--- + +## Module-level discipline audit (bidirectional-alignment) + +- **AI → human**: does this module help the AI explain + operator composition clearly to a new contributor? + YES — LEGO anchor, operator-table + type-match rule, + F# pipeline example, alternative-comparison table, + self-check gate. +- **Human → AI**: does this module help a human + contributor understand what the AI treats as + operator composition (semantically + categorically)? + YES — arrows-in-a-category framing, DBSP identities, + H operator nested-composition surfaced, where- + composition-fails called out explicitly. + +**Module passes both directions.** diff --git a/docs/pr-preservation/203-drain-log.md b/docs/pr-preservation/203-drain-log.md new file mode 100644 index 00000000..2b319ab2 --- /dev/null +++ b/docs/pr-preservation/203-drain-log.md @@ -0,0 +1,453 @@ +# PR #203 drain log — third Craft module operator-composition + +PR: +Branch: `craft/third-module-operator-composition` +Drain session: 2026-04-24 (drain subagent per Otto-228 +three-axis drain + Otto-250 PR-preservation directive) +Thread count at drain start: 20 unresolved (Copilot + zero-empathy +reviewer roster) +Axes drained: review threads (20 unresolved). DIRTY-axis cleared +by rebase onto `origin/main`; CI was already SUCCESS at drain +start so no CI-axis work needed. + +Rebase: clean two-commit replay onto `origin/main`. No append-only +collisions, no skipped commits. + +Fix commit: `facebb09c25e3addcbd1df092cf99a5577fe2785` + +The 20 threads collapsed into seven content fixes plus a structural +delete. Many threads were duplicates of the same underlying defect +flagged by overlapping reviewers (P0/P1/P2 graded). The fix commit +addresses each defect at its source so multiple thread IDs share +the same outcome reference where the fix was structural rather +than line-local. + +--- + +## Thread 1 — `docs/craft/subjects/zeta/operator-composition/module.md:94` — F# pipeline example does not type-check + +- Thread ID: `PRRT_kwDOSF9kNM59O7eP` +- Severity: P2 + +### Outcome + +FIX in `facebb0`. The conceptual `|> filter ... |> count |> I` +snippet was rewritten to use the real +`[]` `Pipeline` API +(`Zeta.Core.Pipeline.filter c`, +`Zeta.Core.Pipeline.integrate c`, +`Zeta.Core.Pipeline.count c`). The new ordering puts +`Pipeline.integrate` before `Pipeline.count` so the Z-set +operators run on `Stream>` and the scalar collapse to +`Stream` happens last. + +--- + +## Thread 2 — `docs/craft/subjects/zeta/operator-composition/module.md:93` — pipeline example needs Circuit + qualified names + +- Thread ID: `PRRT_kwDOSF9kNM59O-p2` +- Severity: P1 (suggestion-comment) + +### Outcome + +FIX in `facebb0` (same fix as Thread 1). The reviewer's +suggested `Zeta.Core.Pipeline.filter c (...) |> +Zeta.Core.Pipeline.count c` shape is the shape the new +example uses, with the additional integrate step inserted to +satisfy Thread 3's correctness requirement. + +--- + +## Thread 3 — `docs/craft/subjects/zeta/operator-composition/module.md:105` — count then integrate is type-incorrect + +- Thread ID: `PRRT_kwDOSF9kNM59O-qH` +- Severity: P1 (correctness) + +### Outcome + +FIX in `facebb0`. The original example wrote `... |> count |> I` +which composes `Stream` into an integral that requires +`Stream>`. The fix integrates the Z-set first +(`Pipeline.integrate c`), then collapses to scalar with +`Pipeline.count c`. The surrounding prose now explicitly +explains why the ordering matters and that scalar-emitting +operators terminate the Z-set composition chain. + +--- + +## Thread 4 — `docs/craft/subjects/zeta/operator-composition/module.md:293` — Attribution section names contributors + +- Thread ID: `PRRT_kwDOSF9kNM59O-qg` +- Severity: P1 + +### Outcome + +FIX in `facebb0`. The Attribution section was deleted entirely +per `docs/AGENT-BEST-PRACTICES.md` BP "No name attribution in +code, docs, or skills". Authorship and review-plan notes +belong in commit messages, PR descriptions, persona memory, +or `docs/BACKLOG.md` rather than in the module body. + +--- + +## Thread 5 — `docs/craft/subjects/zeta/operator-composition/module.md:10` — missing prerequisite zset-basics + +- Thread ID: `PRRT_kwDOSF9kNM59PWiA` +- Severity: P2 + +### Outcome + +NARROW+BACKLOG in `facebb0`. The prerequisite line was +softened to "(forthcoming - Z-sets are what most operators +consume and produce; until that module lands, see +`docs/ARCHITECTURE.md` and +`openspec/specs/operator-algebra/spec.md` for the Z-set +definition)". Authoring the actual zset-basics module is a +separate larger Craft work-item beyond this PR's scope; this +narrow fix removes the broken-curriculum signal while pointing +readers at the canonical Z-set sources today. + +--- + +## Thread 6 — `docs/craft/subjects/zeta/operator-composition/module.md:150` — map-after-count self-check is impossible + +- Thread ID: `PRRT_kwDOSF9kNM59PWiB` +- Severity: P2 + +### Outcome + +FIX in `facebb0`. The self-check was rewritten to ask the +learner to *explain* why a `map` after `count` cannot +type-check given `Pipeline.count: Stream> -> +Stream` and `Pipeline.map: Stream> -> +Stream>`. The pedagogical intent (is the learner +reading types?) is preserved without asking them to produce +an unrepresentable pipeline. + +--- + +## Thread 7 — `docs/craft/subjects/zeta/operator-composition/module.md:74` — operator table starts with `||` + +- Thread ID: `PRRT_kwDOSF9kNM59PX8U` +- Severity: P2 + +### Outcome + +BACKLOG+RESOLVE. The current file at `HEAD` of this PR uses +single-pipe table syntax (`| Operator | ... |`) on every row +of both tables; `grep -n '^||'` returns no matches. The +double-pipe artifact the reviewer flagged appears to be from +an earlier draft rendering. No edit needed against the current +content. Resolving as a no-op outcome; the H-row content was +separately fixed by Threads 10/11/12. + +--- + +## Thread 8 — `docs/craft/subjects/zeta/operator-composition/module.md:113` — alternatives table starts with `||` + +- Thread ID: `PRRT_kwDOSF9kNM59PX8g` +- Severity: P2 + +### Outcome + +BACKLOG+RESOLVE. Same as Thread 7 - the alternatives table at +`HEAD` uses single-pipe syntax. No edit needed. + +--- + +## Thread 9 — `docs/craft/subjects/zeta/operator-composition/module.md:197` — `D o (Q1 o Q2) = (D o Q1) o Q2` is associativity, not delta-distribution + +- Thread ID: `PRRT_kwDOSF9kNM59PX8p` +- Severity: P2 (mathematical-correctness) + +### Outcome + +FIX in `facebb0`. Replaced the bullet with two correct laws: +`Q^Delta = D o Q o I` (the incrementalisation chain rule the +optimiser actually uses, citing `src/Core/Incremental.fs`) +and `D o Q = Q o D` for time-invariant linear `Q` (the +non-trivial commutation). Bare associativity of composition +is no longer dressed up as a distribution law. + +--- + +## Thread 10 — `docs/craft/subjects/zeta/operator-composition/module.md:75` — `H` defined as hierarchy conflicts with operator-algebra spec + +- Thread ID: `PRRT_kwDOSF9kNM59P5Rk` +- Severity: P0 + +### Outcome + +FIX in `facebb0`. The operator-table row for `H` now reads +"`H` (`distinct^Delta`) - Incremental-distinct +boundary-crossing operator (per +`openspec/specs/operator-algebra/spec.md`)" with input +ΔZ-set(t) and output ΔZ-set(t) (multiplicities clamped to +{0,1}). A note immediately below the table redirects nested / +recursive composition to `NestedCircuit.Nest` and the +`circuit-recursion` / `retraction-safe-recursion` specs. + +--- + +## Thread 11 — `docs/craft/subjects/zeta/operator-composition/module.md:230` — Hierarchical composition (`H`) section conflicts with H = distinct^Δ + +- Thread ID: `PRRT_kwDOSF9kNM59P5R3` +- Severity: P0 + +### Outcome + +FIX in `facebb0`. The "Hierarchical composition (`H`)" +heading was renamed to "Nested / recursive circuits", and the +section body now explicitly reserves `H` for `distinct^Delta` +per the operator-algebra spec while pointing nested-circuit +readers at `NestedCircuit.Nest` and the recursion specs. + +--- + +## Thread 12 — `docs/craft/subjects/zeta/operator-composition/module.md:75` — operator-table H definition will mis-teach contributors + +- Thread ID: `PRRT_kwDOSF9kNM59P5Zj` +- Severity: P1 + +### Outcome + +FIX in `facebb0`. Same fix as Thread 10 - the table row was +rewritten to reflect the spec's `H = distinct^Delta` +definition; the nested-composition story was relocated and +relabelled per Thread 11. + +--- + +## Thread 13 — `docs/craft/subjects/zeta/operator-composition/module.md:32` — every operator is Z-set to Z-set is overgeneralised + +- Thread ID: `PRRT_kwDOSF9kNM59P5Zk` +- Severity: P2 + +### Outcome + +FIX in `facebb0`. The LEGO-anchor paragraph was rewritten to +say "Many core operators transform `Stream>` to +`Stream>`, but composition is more general: one +operator can snap downstream of another whenever the upstream +operator's output type matches the downstream operator's +input type. Some operators (for example `count`, `sum`) emit +scalar streams (`Stream`) rather than Z-set streams; +these compose with operators that accept scalars." + +--- + +## Thread 14 — `docs/craft/subjects/zeta/operator-composition/module.md:196` — tautological delta-composition law (duplicate of Thread 9) + +- Thread ID: `PRRT_kwDOSF9kNM59P_xc` +- Severity: P2 + +### Outcome + +FIX in `facebb0` (same fix as Thread 9). Replaced with the +chain rule and time-invariant-linear commutation law. + +--- + +## Thread 15 — `docs/craft/subjects/zeta/operator-composition/module.md:293` — Attribution section duplicate of Thread 4 + +- Thread ID: `PRRT_kwDOSF9kNM59QFcO` +- Severity: P1 + +### Outcome + +FIX in `facebb0` (same fix as Thread 4). Section deleted in +full per BP "No name attribution in code, docs, or skills". + +--- + +## Thread 16 — `docs/craft/subjects/zeta/operator-composition/module.md:95` — F# example does not match Pipeline API (duplicate of Thread 1/2/3) + +- Thread ID: `PRRT_kwDOSF9kNM59QFcn` +- Severity: P1 + +### Outcome + +FIX in `facebb0` (same fix as Thread 1/2/3). Example uses +`Zeta.Core.Pipeline.*` with explicit `Circuit` argument and +the corrected integrate-before-count ordering. + +--- + +## Thread 17 — `docs/craft/subjects/zeta/operator-composition/module.md:130` — retract-safe marker does not exist + +- Thread ID: `PRRT_kwDOSF9kNM59Qik_` +- Severity: P2 + +### Outcome + +FIX in `facebb0`. The self-check was rewritten to point at +the `retraction-intuition` module and +`openspec/specs/operator-algebra/spec.md` instead of a +nonexistent `"retract-safe"` marker. The retraction-safety +question is now actionable against documented sources. + +--- + +## Thread 18 — `docs/craft/subjects/zeta/operator-composition/module.md:94` — Pipeline calls do not exist as written (duplicate of Thread 1/2/3/16) + +- Thread ID: `PRRT_kwDOSF9kNM59Q-Nm` +- Severity: P2 + +### Outcome + +FIX in `facebb0` (same fix as Thread 1/2/3/16). Example now +uses qualified `Zeta.Core.Pipeline.filter c` etc. + +--- + +## Thread 19 — `docs/craft/subjects/zeta/operator-composition/module.md:33` — every operator consumes / emits Z-sets is internally inconsistent (duplicate of Thread 13) + +- Thread ID: `PRRT_kwDOSF9kNM59Q-OO` +- Severity: P1 + +### Outcome + +FIX in `facebb0` (same fix as Thread 13). Paragraph was +rewritten to qualify the claim and acknowledge scalar-emitting +operators. + +--- + +## Thread 20 — `docs/craft/subjects/zeta/operator-composition/module.md:203` — tautological identity, replace with chain rule (duplicate of Thread 9/14) + +- Thread ID: `PRRT_kwDOSF9kNM59Q-OY` +- Severity: P1 + +### Outcome + +FIX in `facebb0` (same fix as Thread 9/14). Replaced with the +chain rule `Q^Delta = D o Q o I` and the time-invariant-linear +commutation `D o Q = Q o D`. + +--- + +## End-of-drain state + +- Unresolved threads: 0 (target) +- mergeStateStatus: target MERGEABLE +- Auto-merge: armed at drain start; preserved through push. + +--- + +# Drain pass: 2026-04-24 (round 2 — 7 threads) + +After round-1 push, a late Copilot re-review opened seven NEW +unresolved threads (CI was still SUCCESS, auto-merge still armed). +This appended section logs the round-2 drain. Per Otto-229 the +round-1 sections above are not edited; this section stands as a +correction-and-extension companion. + +Drain session: 2026-04-24 (drain-subagent round 2) +Thread count at drain start: 7 unresolved (Copilot late re-review) +Axes drained: review threads only. CI still SUCCESS at drain +start; rebase onto `origin/main` was clean (5-commit replay, no +append-only collisions). + +## Thread R2-1 — `docs/pr-preservation/203-drain-log.md:142` — round-1 grep claim disputed + +- Thread ID: `PRRT_kwDOSF9kNM59iLLJ` +- Severity: P1 + +### Outcome + +BACKLOG+RESOLVE — round-1 claim verified accurate against +current branch state. The round-1 narrative said `grep -n '^||'` +returns no matches and the file at HEAD uses single-pipe table +syntax. Re-running `grep -nE '^\|\|'` and `grep -nE '\|\|'` on +the rebased branch confirms zero matches; `od -c` on the operator +table at lines 78-86 shows single `|` separators. The Copilot +reviewer appears to have been looking at a stale render or +earlier draft. Per Otto-229 the round-1 section is append-only +and stays as written; this round-2 entry is the correction-row +record. + +## Thread R2-2 — `docs/craft/subjects/zeta/operator-composition/module.md:83` — operator-table `||` claim + +- Thread ID: `PRRT_kwDOSF9kNM59iLLT` +- Severity: P1 + +### Outcome + +BACKLOG+RESOLVE. Same finding as R2-1: the operator table at +line 78-86 uses single-pipe Markdown syntax already; `grep` +plus `od -c` verification on the rebased branch shows zero +double-pipe rows. No edit needed against current content. + +## Thread R2-3 — `docs/craft/subjects/zeta/operator-composition/module.md:145` — alternatives-table `||` claim + +- Thread ID: `PRRT_kwDOSF9kNM59iLLV` +- Severity: P1 + +### Outcome + +BACKLOG+RESOLVE. Same finding as R2-1/R2-2. The alternatives +table at lines 139-144 uses single-pipe Markdown syntax. No +edit needed. + +## Thread R2-4 — `docs/craft/subjects/zeta/operator-composition/module.md:93` — `NestedCircuit.Nest` API surface + +- Thread ID: `PRRT_kwDOSF9kNM59iLLa` +- Severity: P1 + +### Outcome + +FIX in this round-2 commit. The note below the operator table +now points readers at `Circuit.Nest` / `Circuit.NestWithHandle` +extension methods and explicitly cites the +`NestedCircuitExtensions` static class in +`src/Core/NestedCircuit.fs` as the implementation site. The +prior `NestedCircuit.Nest` wording is replaced; readers can now +locate the API. + +## Thread R2-5 — `docs/craft/subjects/zeta/operator-composition/module.md:312` — Composes-with mislabels NestedCircuit.fs + +- Thread ID: `PRRT_kwDOSF9kNM59iLLd` +- Severity: P1 + +### Outcome + +FIX in this round-2 commit. The "Composes with" bullet for +`src/Core/NestedCircuit.fs` no longer reads "hierarchical +composition (H operator)". It now reads "nested / recursive +composition via `Circuit.Nest` / `Circuit.NestWithHandle` +extension methods (NOT the `H` operator; `H` = `distinct^Δ` +per the operator-algebra spec)". The semantic mismatch with +the spec's `H = distinct^Δ` definition is removed. + +## Thread R2-6 — `docs/craft/subjects/zeta/operator-composition/module.md:233` — `D ∘ I = I ∘ D = id` precondition + +- Thread ID: `PRRT_kwDOSF9kNM59iLLf` +- Severity: P2 + +### Outcome + +FIX in this round-2 commit. The bullet now reads "For causal +streams with a declared zero at `t=0`, D ∘ I = I ∘ D = id" with +a pointer to `openspec/specs/operator-algebra/spec.md` for the +precondition. The unconditional reading is no longer offered. + +## Thread R2-7 — `docs/craft/subjects/zeta/operator-composition/module.md:311` — Composes-with H-mislabel duplicate of R2-5 + +- Thread ID: `PRRT_kwDOSF9kNM59iLLs` +- Severity: P1 + +### Outcome + +FIX in this round-2 commit (same fix as R2-5). The "Composes +with" entry that previously labeled `src/Core/NestedCircuit.fs` +as "hierarchical composition (H operator)" was rewritten to +reflect that nesting / recursion goes via +`Circuit.Nest` / `Circuit.NestWithHandle` extension methods and +that `H` is reserved for `distinct^Δ` per the operator-algebra +spec. + +## End-of-round-2 state + +- Unresolved threads: 0 (target) +- mergeStateStatus: target MERGEABLE +- Auto-merge: still armed.