name: split-behavior description: >- Split one SPF behavior into N with axis-declared, constraints-audited discipline. Forces explicit axis declaration (per-type horizontal vs. per-concern vertical) and a cross-boundary constraint audit before the split lands — avoids the failure mode where an apparent per-type-friendly behavior ships a split that quietly drops a cross-type ordering invariant the merged code was enforcing. Use after /refactor-behavior's decomposition check has concluded "split," or when you've already noticed a behavior wants splitting. Triggers: "split this behavior", "split into per-type", "extract per-type behaviors", "convert to per-type variants", "split behavior".
Split one SPF Behavior into N
A split has two failure modes:
- Mis-declared axis. Treating an apparent per-type split as per-concern (or vice versa) anchors the rest of the analysis on the wrong shape and produces a messy result.
- Dropped cross-boundary constraint. The merged code was enforcing an invariant (often implicit — a sync-block ordering, a guard that protected against a transient state, an atomic-publish gate) that the split silently breaks. Latent races, not typecheck failures.
This skill enforces axis declaration before per-side analysis (so the shape is right) and a cross-boundary constraint audit before code (so nothing implicit gets dropped on the floor).
Usage
/split-behavior <file>
Where <file> is the single behavior file you're splitting. The skill
asks which axis (per-type vs. per-concern) and which post-split units
you're targeting.
Reference docs
The canonical references — read them first:
internal/design/spf/conventions/behaviors.md— especially:- Per-type specialization — the destination shape for per-type splits (per-type exports + setup-shape helper). This skill operationalizes the refactor moment; that section codifies the end-state convention.
- Sniffs that say "split" — the pre-existing decomposition sniffs. The skill's Step 3 (per-side analysis) leans on these.
- Cleaned-shape sketch — the structured format each post-split unit uses.
- Refactoring an existing behavior → Step 1 — the purpose-verb diagnostic.
internal/design/spf/conventions/reactors.md— transition-driven vs state-driven, leaky-abstraction sniff (helpers with conditional branches around optional state-scoped work — the setup-shape helper for a per-type split must not carry per-variant conditionals)..claude/skills/refactor-behavior/SKILL.md— the per-side analysis (Steps 1–4) is borrowed from here.
When to use this skill vs. /refactor-behavior, /merge-behaviors
| Situation | Skill |
|---|---|
| Cleaning up a single behavior file (same shape, same boundaries) | /refactor-behavior |
/refactor-behavior's decomposition check concludes "this should split" |
/split-behavior |
| Splitting one behavior into per-type variants (video/audio/text) | /split-behavior (per-type axis) |
| Splitting one behavior into per-concern behaviors (two disjoint slot clusters) | /split-behavior (per-concern axis) |
| Merging two behaviors into one | /merge-behaviors |
Steps (do these in order; do not skip)
Step 1 — Identify input
State the file being split (absolute or workspace-relative path). Confirm with the user if it's not obvious.
Step 2 — Articulate the current behavior's purpose
In one sentence: what is this behavior for? Same as
refactor-behavior Step 1.
The purpose establishes the basis for evaluating the split. If the
purpose is one coherent thing that doesn't decompose into a per-type
or per-concern shape, that's a finding ("the split doesn't earn its
keep — keep as-is or apply /refactor-behavior instead").
Pull from the file-level JSDoc if present; if not, that's a doc gap to flag.
Step 3 — Declare the split axis
Two axes — declare which:
- Per-type (horizontal). Same concern applied across media types
(video / audio / text). Body iterates types, applies the same
operation each (with type-specific parameters). Destination shape:
per-type exports + shared setup-shape helper, per
behaviors.md→ "Per-type specialization." Worked sibling examples:select-tracks,resolve-track,load-segments. - Per-concern (vertical). Two disjoint concerns happen to be
wired together.
stateKeys/contextKeyscluster into two groups touched by disjoint code paths. Destination shape: two separate behaviors with their owndefineBehavior, each with narrow keys. The pre-existing decomposition sniffs inbehaviors.md→ "Sniffs that say 'split'" describe this axis.
Diagnostic — which axis applies:
- Body has a
for (const type of types)loop where the loop body is the same operation in each iteration → per-type. - Two slot clusters (
videoBuffer+videoBufferActorcluster vs. some other cluster) touched by disjoint code paths → per-concern. - Both apply (e.g., a per-type behavior also has a buffer-vs-actor concern split inside) → split per-type first, then re-evaluate. Per-type extraction often dissolves what looked like a per-concern split.
Pause for user confirmation on the axis. This is the load-bearing call. Mis-declaring it poisons the rest of the analysis; the user may have context the diagnostic doesn't capture.
Step 4 — Per-side cleaned-shape sketch
For each post-split unit, produce a cleaned-shape sketch using the cleaned-shape sketch template:
- States:
- entry work:
- effects: (continuous reactivity):
- State-exit cleanup:
- Source-reset concerns:
- Private temporal state:
For per-type splits, produce one sketch per variant (video / audio / text). The shape of each variant will be near-identical aside from the per-type parameters — that's the point; the shared shape is what the setup-shape helper captures.
For per-concern splits, produce one sketch per concern. The sketches should look genuinely different (different slot writes, different lifecycle, different cleanup); if they look near-identical, the axis may be wrong.
Don't refactor pre-emptively. Sketches describe what each
post-split unit would look like extracted from the current code, not
a projected refactored version. If a unit's sketch reveals real issues
(closure-mutable state, fight-the-shape sniffs), note them as
follow-up /refactor-behavior candidates for that variant after the
split lands — don't bundle the refactor into the split.
Step 5 — Cross-boundary constraint audit (load-bearing)
This is the discipline that distinguishes /split-behavior from the
pre-existing decomposition guidance. Skipping it is how splits ship
latent races.
Find candidate constraints. Scan the merged code for any of:
- Comments referencing ordering (
no await between X calls,sync block,before any Y,must complete before). - Explicit guards immediately before context writes (
if (already…) return;placed defensively). - Browser-bug workarounds with named ordering requirements (
Firefox mozHasAudio,Safari requires X before Y, etc.). - Atomicity gates (
set context once so subscribers see all). - "Once X has happened" preconditions that imply temporal ordering.
peekreads positioned to suppress re-fire — often implying a constraint about when re-fire is safe.
For each constraint surfaced, write three lines:
- State the constraint. One sentence, the rule. Example: "All
addSourceBuffercalls must complete before anyappendBuffercall (FirefoxmozHasAudiobug)." - Cite the merged-code enforcement. Where in the current code
does the constraint live, and what's the mechanism? Example:
"Single sync block in
entry: 'no await between addSourceBuffer calls — then set context once.'" - Walk through how the split preserves it. One of three valid
classifications:
- Structural — signal-batching, composition order, framework
semantics, or another existing mechanism already gives the
ordering. Cite the specific mechanism. Example: "Both per-type
reactors are downstream of the same
mediaSourcewrite; both re-evaluate on the same microtask flush; bothaddSourceBuffercalls fire before any further microtask.loadSegments→appendBufferis ≥ 3 microtask boundaries + network I/O away." - Shared signal / handoff — a coordinating slot or computed signal explicitly carries the ordering. Cite the slot.
- Invariant invalid post-refactor — the constraint is no longer applicable after the split for a concrete reason (e.g., the downstream consumer it was protecting was removed). Archive the reasoning.
- Structural — signal-batching, composition order, framework
semantics, or another existing mechanism already gives the
ordering. Cite the specific mechanism. Example: "Both per-type
reactors are downstream of the same
If none of the three classifications apply for any surfaced constraint, the split introduces a regression. Pause and reconsider the axis, the per-side shape, or whether the split should happen at all.
This discipline is plausibly extractable into its own
/audit-constraintsskill once it has additional call sites (greenfield behaviors, global audits, post-refactor verification). For now it lives inline here.
Step 6 — Destination shape / shared helper
Based on the axis:
Per-type. Identify the setup-shape helper signature (
({ state, context, config }) => cleanuporReactor<...>). What does each variant pass in viaconfig/ inline call-site? What stays in the helper? Perbehaviors.md→ "Setup-shape helper" + "Naming." Worked sibling:setupTrackResolutioninresolve-track.ts.Leaky-abstraction check — the helper must not be parameterized by "is this optional thing on or off?" Per
reactors.md: "Helpers with conditional branches around optional state-scoped work (const x = optionEnabled ? doX() : undefinedinside the helper'sentry)" — the variant should supply the work; the helper shouldn't carry the conditional. If the helper is starting to growif (typeIsAudio) … else …branches, the variants want the variant-specific work to live in the variants themselves, with the helper consuming a uniform input.Per-concern. Confirm each concern lands as its own
defineBehaviorexport with its own narrowstateKeys/contextKeys. No shared helper; the concerns are genuinely independent.
Step 7 — Convention checks
Same as /refactor-behavior Step 5 applied to each post-split unit
independently:
- File placement (DOM-free vs. DOM-bound) per unit.
- Naming per
behaviors.md→ "Naming":- Per-type variants:
<verb><Type><Noun>matching sibling convention (selectVideoTrack/selectAudioTrack,loadVideoSegments/loadAudioSegments). - Setup-shape helper:
setup<Concept>(no per-type qualifier — the helper is type-agnostic).
- Per-type variants:
- Per-unit
stateKeys/contextKeysnarrowed to what that unit actually reads/writes. Per-type variants typically narrow the per-type selected ids and per-type slot pairs to just the relevant type. - File-level JSDoc on each new file articulating the unit's purpose.
- Domain-prefix on the behavior name (if the bare verb could plausibly act on more than one similarly-shaped target).
Step 8 — Decomposition note + stop for confirmation
After the split, look at each post-split unit:
- Does any unit still have decomposition concerns (further splitting, latent merging, multi-writer arrangement on a slot)? Note as follow-up — don't act in the same change.
- Did the split introduce a new multi-writer arrangement (e.g., two per-type variants writing the same shared slot)? If so, that's a sniff that the split axis may be wrong.
Propose the changes; do not write code until the user confirms.
Output format
Propose changes in this order. Use markdown headers for each numbered section.
- Input (the file being split)
- Current purpose (1 sentence)
- Split axis declaration — per-type or per-concern, with the diagnostic evidence. Stop here for user confirmation.
- Per-side cleaned-shape sketches — one per post-split unit, in the template format
- Cross-boundary constraint audit — explicit list of constraints surfaced from the merged code, each with the three-line treatment (state / cite / classify). If any constraint can't be classified, stop and reconsider.
- Destination shape — for per-type: helper signature + variants;
for per-concern: two
defineBehaviordeclarations - Convention checks (per unit)
- Decomposition note (post-split, any residual concerns)
Why this order
Three failure modes the order prevents:
- Anchoring on the merged file's shape rather than the per-side cleaned shapes. Step 4 forces per-side sketches first, before any combination work happens.
- Mis-declaring the axis (per-type vs. per-concern) and inheriting the wrong shape into per-side analysis. Step 3 makes the axis explicit and pauses for confirmation — the user can override the diagnostic based on context.
- Dropping a cross-boundary constraint silently — the canonical failure mode for splits. Step 5's audit forces every implicit invariant in the merged code to be either preserved (with explicit classification) or declared invalid (with reasoning). If neither, the skill stops.
A side benefit: the per-side sketches double as the design for each
post-split unit's standalone refactor (if the user later opts to land
the split as separate /refactor-behavior runs on each variant).