Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion scripts/test-skills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions skills/allium/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions skills/allium/references/actioning-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion skills/allium/references/driving-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
51 changes: 51 additions & 0 deletions skills/allium/references/integrating-slices.md
Original file line number Diff line number Diff line change
@@ -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 <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.
- **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.
- **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.
Loading