From 80160adc258390fcbda790e7256a807d3a21334f Mon Sep 17 00:00:00 2001 From: Yavor Panayotov Date: Sun, 23 Aug 2026 14:29:25 +0300 Subject: [PATCH] Make the fan-out reduce step a real procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fanning a large goal out into one distill (or one loop) per service is the map. The reduce -- assembling the slices and driving the seams between them to convergence -- was only a hand-wave in driving-the-loop §6: "run a whole-spec integration pass". Slices distilled in isolation don't agree at their seams: the same entity defined twice, a rule that depends on a trigger another slice was meant to emit, a contract demanded with nothing to fulfil it. Turn §6 into a concrete reduce: assemble and wire the slices (canonical owner for a shared entity, use imports), cross-check the whole set with a single `allium analyse` call (now cross-module aware, so it reasons across use seams and returns the seams that don't line up), route each finding through the existing taxonomy to tend/weed, then cross-service propagate and a final witness. The CLI does the seeing and the existing phase agents do the edits, so the orchestrator holds slice paths and JSON, never slice bodies -- and no new agent is added. - New reference integrating-slices.md: the seam detail (canonical entity ownership, use wiring, contract matching, which findings signal a broken seam). - driving-the-loop §6 rewritten as the four-step reduce, linking it. - actioning-findings gains the cross-seam framing and documents allium.reference.unknownName as the seam signal to read from diagnostics. - test-skills: a reduce group pinning the procedure's constructs and links. --- scripts/test-skills.mjs | 48 ++++++++++++++++- skills/allium/SKILL.md | 1 + .../allium/references/actioning-findings.md | 11 ++++ skills/allium/references/driving-the-loop.md | 9 +++- .../allium/references/integrating-slices.md | 51 +++++++++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 skills/allium/references/integrating-slices.md diff --git a/scripts/test-skills.mjs b/scripts/test-skills.mjs index c3a4e5d..97c0efc 100644 --- a/scripts/test-skills.mjs +++ b/scripts/test-skills.mjs @@ -10,7 +10,7 @@ * node scripts/test-skills.mjs structure # run one group * node scripts/test-skills.mjs portability links # run multiple groups * - * Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, modes, handoffs, trace, discovery, parking, witnessing, timinghook, crosstalk + * Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, modes, handoffs, trace, reduce, discovery, parking, witnessing, timinghook, crosstalk * * All groups except discovery, parking, witnessing and crosstalk are offline (free, fast); * those four require --live and make Claude API calls. @@ -734,6 +734,52 @@ if (shouldRun("hooks")) { } } +// --------------------------------------------------------------------------- +// Reduce — the fan-out reduce step (driving-the-loop §6 + integrating-slices). +// Pins the constructs that make the reduce a real procedure, so a reword can't +// silently drop them: the reference doc exists and is linked from §6, the +// cross-check runs `allium analyse` over all slices, and the seam-signal +// diagnostic is documented in actioning-findings. Offline and deterministic. +// --------------------------------------------------------------------------- + +if (shouldRun("reduce")) { + console.log("\n── reduce: the fan-out integration (reduce) step ──\n"); + + const integ = path.join(ROOT, "skills", "allium", "references", "integrating-slices.md"); + const loop = path.join(ROOT, "skills", "allium", "references", "driving-the-loop.md"); + const findings = path.join(ROOT, "skills", "allium", "references", "actioning-findings.md"); + + if (existsSync(integ)) pass("integrating-slices.md exists"); + else fail("integrating-slices.md", "reduce-step reference doc missing"); + + const integSrc = existsSync(integ) ? readFileSync(integ, "utf-8") : ""; + // The reduce leans on the CLI cross-checking all slices at once. + /allium analyse/.test(integSrc) + ? pass("integrating-slices: cross-checks with allium analyse") + : fail("integrating-slices: analyse", "must run `allium analyse` over the slices"); + // Canonical owner for shared entities is the assembly rule. + /canonical owner/i.test(integSrc) + ? pass("integrating-slices: canonical owner rule") + : fail("integrating-slices: canonical owner", "must state the canonical-owner rule for shared entities"); + // No new agent: edits are delegated to existing phase agents. + /\btend\b/.test(integSrc) && /\bwitness\b/.test(integSrc) + ? pass("integrating-slices: delegates to existing phase agents") + : fail("integrating-slices: delegation", "must route edits through tend and witness the whole"); + + const loopSrc = readFileSync(loop, "utf-8"); + loopSrc.includes("integrating-slices.md") + ? pass("driving-the-loop §6 links the reduce reference") + : fail("driving-the-loop §6 link", "§6 must link integrating-slices.md"); + /reduce step/i.test(loopSrc) + ? pass("driving-the-loop §6 names the reduce step") + : fail("driving-the-loop §6", 'must name the "reduce step"'); + + const findingsSrc = readFileSync(findings, "utf-8"); + findingsSrc.includes("allium.reference.unknownName") + ? pass("actioning-findings documents the seam signal (reference.unknownName)") + : fail("actioning-findings seam signal", "must document allium.reference.unknownName as a seam signal"); +} + // --------------------------------------------------------------------------- // Modes — the interaction-mode contract is stated everywhere it must be. // The skill is the single source of truth for each agent-backed capability; diff --git a/skills/allium/SKILL.md b/skills/allium/SKILL.md index 032f5da..64c211b 100644 --- a/skills/allium/SKILL.md +++ b/skills/allium/SKILL.md @@ -337,4 +337,5 @@ When the `allium` CLI is installed, a hook validates `.allium` files automatical - [Test generation](./references/test-generation.md) — generating tests from specifications - [Recommended loops](./references/recommended-loops.md) — the gather-context → take-action → verify → repeat loop, with spec-first and code-first walkthroughs - [Driving the loop](./references/driving-the-loop.md) — the procedure `/allium` follows to drive a goal to convergence (entry detection, the tick, stop conditions, the ledger) +- [Integrating slices](./references/integrating-slices.md) — the reduce step for fanned-out goals: assemble the slices, cross-check the seams with `allium analyse`, reconcile and witness the whole - [Patterns](./references/patterns.md) — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract diff --git a/skills/allium/references/actioning-findings.md b/skills/allium/references/actioning-findings.md index fbe7101..b583bb0 100644 --- a/skills/allium/references/actioning-findings.md +++ b/skills/allium/references/actioning-findings.md @@ -52,6 +52,17 @@ A rule's `ensures` clause could produce a state that violates a declared invaria The finding's evidence shows the mechanism — how the ensures clause is inconsistent with the invariant. Use this to suggest a specific fix rather than asking an open-ended question. +## Findings across a seam (the reduce step) + +When `analyse` runs over several assembled slices at once (the reduce step — see [integrating slices](./integrating-slices.md)), the same finding types apply, but a finding on one slice often points at a *seam* with another: the consumer depends on behaviour the producer doesn't supply, or the two slices disagree about a shared entity. Read the finding as a cross-slice question rather than a single-spec gap. + +- A **`missing_producer`** or **`unreachable_trigger`** on a consuming slice usually means the *producing* slice should emit the trigger or expose the surface — decide which slice owns the source and `tend` that one, rather than adding the producer to the consumer. +- A **`dead_transition`** / **`deadlock`** whose exit is witnessed in another slice is a *wiring* problem first: check the `use` import and qualified names line up before treating it as a genuine gap. + +### `allium.reference.unknownName` (diagnostic, seam signal) + +Not a finding — a diagnostic — but the clearest sign of a broken seam in the reduce step: a slice references a qualified name (`orders/OrderPaid`) that the owning slice doesn't declare or emit. It typically appears alongside a `deadlock` on the owning slice whose exit that reference was meant to witness. **Ask which slice owns the name.** Either the producer should provide it (`tend` the producer to emit the trigger / declare the entity), or the reference is mis-wired (fix the `use` alias or qualified name). Always read the `diagnostics` array, not just `findings`, when integrating slices — a seam often breaks here first. + ## Choosing which finding to present When `analyse` returns multiple findings, pick the most relevant one. Apply these criteria in order: diff --git a/skills/allium/references/driving-the-loop.md b/skills/allium/references/driving-the-loop.md index d21d6d9..39896dd 100644 --- a/skills/allium/references/driving-the-loop.md +++ b/skills/allium/references/driving-the-loop.md @@ -70,7 +70,14 @@ Rule: a question is *blocking* iff the next unit of work depends on its answer. If the goal spans more than one independent behavioural slice, decompose along the spec's seams — one sub-goal per **entity lifecycle**, **surface**, or **independent rule / data-flow chain**. Order sub-goals topologically by the data-flow / trigger graph (producers before consumers). Run each sub-goal as its own loop, and witness each slice at its own convergence gate (§11) before you count it converged, so a falsified slice cannot be assembled into the whole. -After the slices converge, run a **whole-spec integration pass** — cross-entity / data-flow / reachability tests plus a full `weed`, and a final `witness` over the assembled spec — so the seams *between* slices converge too. Run this autonomously without blocking to confirm the plan, and produce one consolidated summary at the end. +After the slices converge, run the **reduce step** — assemble them into one spec and drive the seams *between* slices to convergence. Fan-out is the map; this is the reduce, and it is a real procedure, not a hand-wave: + +1. **Assemble and wire** — pick a canonical owner for any shared entity (compare declarations with `allium model`, cheap JSON), add the `use` imports and qualified names so the slices form one connected graph. Un-wired, the checker sees them as separate islands. +2. **Cross-check with the CLI** — run `allium analyse` over **all the assembled slices at once**. It resolves references and traces data flow, reachability and witnessing *across* the `use` seams, returning a small JSON list of the seams that don't line up. The CLI does the seeing; you hold only the findings. +3. **Route each seam problem** — read both the `findings` and the `diagnostics` arrays (a broken seam often shows as a dangling `reference.unknownName` on the consumer plus a `deadlock` on the producer). Translate each via [actioning findings](./actioning-findings.md) — a cross-seam `missing_producer` / dangling reference → `tend` the slice that should provide it (or fix the wiring); a cross-slice `conflict` → escalate (§5). Delegate every edit to `tend` / `weed`; re-run `analyse` until the seams are clean, under the normal caps (§4). +4. **Cross-service tests, then witness** — `propagate` over the assembled set for the cross-slice tests the per-slice loops could not exercise, then a final `witness` (§11) over the whole so the integrated spec carries the same convergence guarantee each slice did. + +Run the reduce autonomously without blocking to confirm the plan, and produce one consolidated summary at the end. The seam detail — canonical entity ownership, `use` wiring, contract matching, which findings signal a broken seam — is in [integrating slices](./integrating-slices.md). Throughout, the orchestrator holds slice paths and CLI JSON, never slice bodies — the same isolation the map uses, applied to the reduce. ## 7. Delegate each phase to an isolated sub-agent (default) diff --git a/skills/allium/references/integrating-slices.md b/skills/allium/references/integrating-slices.md new file mode 100644 index 0000000..7574a83 --- /dev/null +++ b/skills/allium/references/integrating-slices.md @@ -0,0 +1,51 @@ +# Integrating slices (the reduce step) + +When a large goal is fanned out — one `distill` (or one loop) per service or domain — each slice converges on its own. But slices distilled in isolation don't automatically agree at their **seams**: the same entity may be defined twice, a rule in one slice may depend on a trigger another slice was supposed to emit, a contract may be demanded with nothing to fulfil it. Fan-out is the *map*; this is the *reduce* — assemble the slices into one spec and drive the seams *between* them to convergence. + +This reference is the seam detail for [driving the loop](./driving-the-loop.md) §6. The orchestrator drives it with the CLI doing the cross-checking and the existing phase agents doing the edits, so no slice's full text is ever read into the orchestrator's own context. + +## Why the CLI does the seeing + +`allium analyse` and `allium check` take **multiple files or a whole directory** and reason across them: they resolve qualified references and `use` imports, match `demands` against `fulfils`, and — as of the cross-module analysis release — trace data flow, reachability and witnessing *across* `use` boundaries. So the cross-check is one CLI call over the whole slice set that returns a small JSON findings list, not a model reading every slice. The orchestrator runs the call and routes on the result; the reading stays in the CLI. + +One prerequisite: the slices must be **wired** first. Un-wired, they are separate islands to the checker — `analyse` can only cross a seam it can see through a `use` edge and qualified names. Assembly (below) is what makes the set one connected graph for `analyse` to reason over. + +## The procedure + +### 1. Assemble and wire + +Establish the shared vocabulary and connect the slices. + +- **Shared entities: pick one canonical owner.** When two slices each distilled the same entity (`User`, `Order`, `Account`), one slice owns the declaration and the others `use`-import it. Choose the owner by where the entity's lifecycle lives — the slice that creates it and drives its status transitions. Use `allium model ` (JSON: entity shapes, fields, state machines) to compare the two declarations cheaply without reading the full specs. If they disagree on fields or states, that disagreement is itself a seam to reconcile (via `tend`), not a free merge. +- **Wire the references.** Add `use "./owner.allium" as ` to each consuming slice and rewrite its references to the shared entity as qualified names (`orders/Order`). Config that derives from another slice's config uses the qualified reference or an expression-form default. +- **Order by the data-flow graph.** Wire producers before consumers, following the trigger-emission graph, so the assembled set reads in dependency order. + +### 2. Cross-check with the CLI + +Run `allium analyse` over **all the assembled slices at once** (pass the directory or every file). Read both arrays it returns: + +- **`findings`** — the process-level seam problems (`missing_producer`, `dead_transition`, `deadlock`, `conflict`, `unreachable_trigger`, `invariant_risk`). Across a seam these mean one slice depends on behaviour another slice doesn't provide. +- **`diagnostics`** — structural seam problems. The clearest broken-seam signal is **`allium.reference.unknownName`** (a slice references a qualified name the owning slice doesn't declare or emit) and unresolved `use` paths. Do not skip the diagnostics array: a broken seam often shows up there first, as a dangling reference on the consumer *plus* a deadlock on the producer whose exit that consumer was meant to witness. + +### 3. Route each seam problem + +Translate each finding or seam diagnostic into an action, the same way [actioning findings](./actioning-findings.md) prescribes — the finding taxonomy applies unchanged across a seam: + +- **Dangling reference / `missing_producer` / `unreachable_trigger` across the seam** — the consumer needs something no slice provides. Decide which slice should provide it and `tend` that slice to emit the trigger or expose the surface; or, if the producer exists but wasn't wired, fix the wiring (step 1). +- **`dead_transition` / `deadlock` at the seam** — an entity's exit is witnessed only in another slice that isn't correctly wired. Usually a wiring fix, occasionally a genuine gap to `tend`. +- **`conflict` across slices** — two slices' rules can set the same field in the same state. This is a direction-changing question: escalate it (§5), don't silently pick an order. +- **Shared-entity disagreement** — the canonical and imported field/state sets differ. `tend` the non-canonical slices to the canonical shape, or escalate if the difference is a real domain disagreement. + +Delegate each edit to `tend` (or `weed` when it's a spec↔code reconciliation), never reconcile by reading the slices into the orchestrator. Re-run `analyse` after the edits; iterate until the seam findings are gone, under the loop's normal caps (§4). + +### 4. Cross-service tests + +Once the seams are clean, `propagate` over the assembled set. Its taxonomy already covers the cross-slice cases — cross-module trigger chains, cross-entity process tests, data-flow-chain tests from a surface in one slice through to a downstream `requires` in another. These are the tests that exercise the seams the per-slice loops could not. + +### 5. Witness the whole + +Run a final `witness` (§11) over the assembled spec, so the integrated whole carries the same convergence guarantee each slice did. Only then is the large goal converged. + +## What stays out of the orchestrator + +The orchestrator holds the slice paths, the CLI's JSON findings, and the ledger — never the slice bodies. Assembly decisions use `allium model` (JSON); the cross-check uses `allium analyse` (JSON); the edits are delegated to `tend`/`weed` in their own contexts. This is the same isolation the fan-out uses for the map, applied to the reduce.