Skip to content

Commit fe9073e

Browse files
Make the fan-out reduce step a real procedure (#72)
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.
1 parent b18580c commit fe9073e

5 files changed

Lines changed: 118 additions & 2 deletions

File tree

scripts/test-skills.mjs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* node scripts/test-skills.mjs structure # run one group
1111
* node scripts/test-skills.mjs portability links # run multiple groups
1212
*
13-
* Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, modes, handoffs, trace, discovery, parking, witnessing, timinghook, crosstalk
13+
* Groups: structure, codex, consistency, portability, links, routing, generation, loopdocs, hooks, modes, handoffs, trace, reduce, discovery, parking, witnessing, timinghook, crosstalk
1414
*
1515
* All groups except discovery, parking, witnessing and crosstalk are offline (free, fast);
1616
* those four require --live and make Claude API calls.
@@ -734,6 +734,52 @@ if (shouldRun("hooks")) {
734734
}
735735
}
736736

737+
// ---------------------------------------------------------------------------
738+
// Reduce — the fan-out reduce step (driving-the-loop §6 + integrating-slices).
739+
// Pins the constructs that make the reduce a real procedure, so a reword can't
740+
// silently drop them: the reference doc exists and is linked from §6, the
741+
// cross-check runs `allium analyse` over all slices, and the seam-signal
742+
// diagnostic is documented in actioning-findings. Offline and deterministic.
743+
// ---------------------------------------------------------------------------
744+
745+
if (shouldRun("reduce")) {
746+
console.log("\n── reduce: the fan-out integration (reduce) step ──\n");
747+
748+
const integ = path.join(ROOT, "skills", "allium", "references", "integrating-slices.md");
749+
const loop = path.join(ROOT, "skills", "allium", "references", "driving-the-loop.md");
750+
const findings = path.join(ROOT, "skills", "allium", "references", "actioning-findings.md");
751+
752+
if (existsSync(integ)) pass("integrating-slices.md exists");
753+
else fail("integrating-slices.md", "reduce-step reference doc missing");
754+
755+
const integSrc = existsSync(integ) ? readFileSync(integ, "utf-8") : "";
756+
// The reduce leans on the CLI cross-checking all slices at once.
757+
/allium analyse/.test(integSrc)
758+
? pass("integrating-slices: cross-checks with allium analyse")
759+
: fail("integrating-slices: analyse", "must run `allium analyse` over the slices");
760+
// Canonical owner for shared entities is the assembly rule.
761+
/canonical owner/i.test(integSrc)
762+
? pass("integrating-slices: canonical owner rule")
763+
: fail("integrating-slices: canonical owner", "must state the canonical-owner rule for shared entities");
764+
// No new agent: edits are delegated to existing phase agents.
765+
/\btend\b/.test(integSrc) && /\bwitness\b/.test(integSrc)
766+
? pass("integrating-slices: delegates to existing phase agents")
767+
: fail("integrating-slices: delegation", "must route edits through tend and witness the whole");
768+
769+
const loopSrc = readFileSync(loop, "utf-8");
770+
loopSrc.includes("integrating-slices.md")
771+
? pass("driving-the-loop §6 links the reduce reference")
772+
: fail("driving-the-loop §6 link", "§6 must link integrating-slices.md");
773+
/reduce step/i.test(loopSrc)
774+
? pass("driving-the-loop §6 names the reduce step")
775+
: fail("driving-the-loop §6", 'must name the "reduce step"');
776+
777+
const findingsSrc = readFileSync(findings, "utf-8");
778+
findingsSrc.includes("allium.reference.unknownName")
779+
? pass("actioning-findings documents the seam signal (reference.unknownName)")
780+
: fail("actioning-findings seam signal", "must document allium.reference.unknownName as a seam signal");
781+
}
782+
737783
// ---------------------------------------------------------------------------
738784
// Modes — the interaction-mode contract is stated everywhere it must be.
739785
// The skill is the single source of truth for each agent-backed capability;

skills/allium/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,5 @@ When the `allium` CLI is installed, a hook validates `.allium` files automatical
337337
- [Test generation](./references/test-generation.md) — generating tests from specifications
338338
- [Recommended loops](./references/recommended-loops.md) — the gather-context → take-action → verify → repeat loop, with spec-first and code-first walkthroughs
339339
- [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)
340+
- [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
340341
- [Patterns](./references/patterns.md) — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract

skills/allium/references/actioning-findings.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ A rule's `ensures` clause could produce a state that violates a declared invaria
5252

5353
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.
5454

55+
## Findings across a seam (the reduce step)
56+
57+
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.
58+
59+
- 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.
60+
- 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.
61+
62+
### `allium.reference.unknownName` (diagnostic, seam signal)
63+
64+
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.
65+
5566
## Choosing which finding to present
5667

5768
When `analyse` returns multiple findings, pick the most relevant one. Apply these criteria in order:

skills/allium/references/driving-the-loop.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ Rule: a question is *blocking* iff the next unit of work depends on its answer.
7070

7171
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.
7272

73-
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.
73+
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:
74+
75+
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.
76+
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.
77+
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).
78+
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.
79+
80+
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.
7481

7582
## 7. Delegate each phase to an isolated sub-agent (default)
7683

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Integrating slices (the reduce step)
2+
3+
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.
4+
5+
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.
6+
7+
## Why the CLI does the seeing
8+
9+
`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.
10+
11+
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.
12+
13+
## The procedure
14+
15+
### 1. Assemble and wire
16+
17+
Establish the shared vocabulary and connect the slices.
18+
19+
- **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 <slice>` (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.
20+
- **Wire the references.** Add `use "./owner.allium" as <alias>` 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.
21+
- **Order by the data-flow graph.** Wire producers before consumers, following the trigger-emission graph, so the assembled set reads in dependency order.
22+
23+
### 2. Cross-check with the CLI
24+
25+
Run `allium analyse` over **all the assembled slices at once** (pass the directory or every file). Read both arrays it returns:
26+
27+
- **`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.
28+
- **`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.
29+
30+
### 3. Route each seam problem
31+
32+
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:
33+
34+
- **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).
35+
- **`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`.
36+
- **`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.
37+
- **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.
38+
39+
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).
40+
41+
### 4. Cross-service tests
42+
43+
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.
44+
45+
### 5. Witness the whole
46+
47+
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.
48+
49+
## What stays out of the orchestrator
50+
51+
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.

0 commit comments

Comments
 (0)