diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 90a4675d..b60e4f03 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -43,9 +43,17 @@ Specialized knowledge for AI agents working on Video.js 10. | [docs](docs/SKILL.md) | Write Video.js 10 documentation (concepts, how-to, READMEs) | Yes | | [gh-issue](gh-issue/SKILL.md) | Analyze GitHub issues and create implementation plans | No | | [git](git/SKILL.md) | Git workflow — commit messages, PRs, branch naming, scope inference | No | +| [merge-behaviors](merge-behaviors/SKILL.md) | Merge two SPF behaviors with cleaned-shape-first discipline | No | | [refactor-behavior](refactor-behavior/SKILL.md) | Refactor an SPF behavior using purpose-first discipline | No | | [review-branch](review-branch/SKILL.md) | Review branch changes and suggest improvements | No | | [rfc](rfc/SKILL.md) | Write RFCs — proposals needing buy-in (public API, product, DX) | No | +| [spf-create-behavior](spf-create-behavior/SKILL.md) | Create a new SPF behavior with conventions-aligned shape (stub; grows with use) | No | +| [spf-document-feature](spf-document-feature/SKILL.md) | Produce/update SPF feature registry docs (triangulation, cluster heuristics, cross-cutting checks, cascade) | No | +| [spf-document-use-case](spf-document-use-case/SKILL.md) | Produce/update SPF use-case-composition registry docs (four-mechanism taxonomy, constituent-feature cascade) | No | +| [spf-implement-feature](spf-implement-feature/SKILL.md) | Implement an SPF feature from its registry doc (disambiguation, phase scoping, chunk decomposition, downstream-skill routing, doc-as-starting-point) | No | +| [spf-implement-use-case](spf-implement-use-case/SKILL.md) | Implement an SPF use-case composition from its registry doc (disambiguation/routing, constituent-feature readiness, variant assembly, doc-as-starting-point) | No | +| [spf-update-behavior](spf-update-behavior/SKILL.md) | Update an existing SPF behavior whose purpose is changing (stub; distinct from /refactor-behavior which preserves purpose) | No | +| [split-behavior](split-behavior/SKILL.md) | Split one SPF behavior into N with axis-declared, constraints-audited discipline | No | ## Review Workflows diff --git a/.claude/skills/spf-create-behavior/SKILL.md b/.claude/skills/spf-create-behavior/SKILL.md new file mode 100644 index 00000000..f14815c8 --- /dev/null +++ b/.claude/skills/spf-create-behavior/SKILL.md @@ -0,0 +1,185 @@ +--- +name: spf-create-behavior +description: >- + Create a new SPF behavior with conventions-aligned shape. Walks through + purpose articulation (carries /refactor-behavior's purpose-first discipline), + signal type choice, slot map design, composition placement, cleanup pattern + selection, test placement, and engine wiring. Distinct from + /refactor-behavior (which modifies an existing behavior preserving its + purpose) and /spf-update-behavior (which modifies an existing behavior whose + purpose is changing). Triggers: "create behavior", "new behavior", "create + SPF behavior", "add behavior", "scaffold behavior", "new SPF behavior". +--- + +# Create an SPF Behavior + +Scaffold a new SPF behavior in `packages/spf/src/playback/behaviors/` (or +appropriate sub-path for variant-specific behaviors) with conventions-aligned +shape. The canonical failure mode without this discipline is jumping from +"we need a new behavior" to `defineBehavior({...})` without articulating +purpose — producing a behavior whose slot map drifts, whose composition +placement is unclear, or whose cleanup contract doesn't match the project +convention. + +This skill is a **stub** scoped for use by `/spf-implement-feature`. Failure- +mode catalog grows from real use; the seeded entries below capture the +load-bearing patterns identified at skill-creation time. + +## Usage + +``` +/spf-create-behavior [] +``` + +The skill is typically invoked from `/spf-implement-feature`'s Step 6 when a +chunk requires a new behavior, but can be invoked directly when the user +knows the behavior shape upfront. + +## Reference docs + +Required reading before drafting: + +- `internal/design/spf/conventions/behaviors.md` — when to define a behavior, + one-vs-several decomposition, per-type specialization vs uniform-across- + tracks, file placement, source-reset handling, **cleanup convention** + (named-cleanup-collection + wrapper for multi-cleanup, not AbortController + for SPF behaviors). +- `internal/design/spf/conventions/signals.md` — when to use `Signal` vs + `ReadonlySignal`, multi-writer slot characterization, `peek` / + `equalsById` helpers, `initialState` / `initialContext` seeding. +- `internal/design/spf/conventions/reactors.md` — if the behavior is a + reactor (state-machine driven). +- `internal/design/spf/conventions/actors.md` — if the behavior creates or + consumes Actors. +- `internal/design/spf/conventions/config.md` — config surface conventions. +- `packages/spf/src/CLAUDE.md` — source layout + dependency rules. +- Existing similar behaviors as templates (e.g., `switchVideoQuality` as a + template for `switchAudioQuality`). + +## Failure-mode catalog (seeded; grows with use) + +- **Purpose-articulation skipped.** Same failure mode as `/refactor-behavior`: + jumping from "we need a behavior" to `defineBehavior` without naming what + the behavior *does* in business terms. Carries the purpose-first + discipline. The articulation should answer: what business rule does this + behavior implement? What signal does it react to or write? What lifecycle + does it own? + +- **Slot map over-sized.** Including more slots in `stateKeys` / + `contextKeys` than the behavior actually reads/writes locks the behavior + to a fixed composition. Audio-only / video-only / live variants can't + compose the behavior without wiring no-op slots. Per `behaviors.md` § + Inverse: behaviors that operate uniformly across tracks, compose against + the aggregating resource (e.g., `mediaSource.sourceBuffers`) when the + behavior's logic is uniform, not against per-type slot pairs. + +- **Cleanup pattern mismatch.** Per the project convention (recorded in + `feedback_spf_cleanup_pattern` memory and `conventions/behaviors.md`), + SPF behaviors use **named-cleanup-collection + wrapper** for multi-cleanup, + *not* AbortController (which the broader CLAUDE.md cleanup-pattern section + recommends for non-SPF code). New behaviors must match the SPF-specific + convention. + +- **Composition-variant logic in always-on behavior.** When a behavior is + variant-specific (lives only in a live variant, audio-only variant, etc.), + it must live as a separate behavior composed into the variant, *not* as a + runtime conditional inside an always-on behavior. Same failure mode as + the spf-document-feature catalog entry. + +- **Tests written after the fact.** TDD discipline: write the test in + `tests/.test.ts` first, run it failing, then implement. + Implementation-first produces tests that pass by construction. + +## Steps (do these in order) + +### Step 1 — Articulate purpose + +Carry `/refactor-behavior`'s purpose-first discipline forward to new +behaviors. Before any code: + +- **What business rule does this behavior implement?** Name it in plain + language. "Audio quality switching responds to bandwidth state by writing + selectedAudioTrackId to the next viable quality." +- **What signal does it react to or write?** Inputs (read slots) and + outputs (write slots). +- **What lifecycle does it own?** When does it activate, when does it + cleanup, what triggers source-identity reset? +- **What's the failure mode if the behavior weren't there?** Helps clarify + what's load-bearing. + +**Stop and report to user** with the purpose articulation. The user +confirms before proceeding to slot map design. + +### Step 2 — Choose signal types + slot map + +Per `conventions/signals.md`: + +- **`Signal` (writable) vs `ReadonlySignal`** — express read/write + intent at the use site. If the behavior writes a slot, it needs `Signal`. + If only reads, `ReadonlySignal`. +- **Multi-writer characterization** — if writing a slot another behavior + also writes, characterize coordination along the three axes from + `conventions/signals.md`: decision domain (config / DOM / intent / + derived), trigger (one-shot vs ongoing), cost (cheap vs side-effect-heavy). +- **`stateKeys` / `contextKeys` sizing** — include only what the behavior + actually reads/writes. Per the slot-map-over-sized failure mode, narrow + is better. +- **`initialState` / `initialContext` seeding** — if the behavior's first + read of a slot needs a meaningful default, seed via `initialState` rather + than relying on `undefined`-narrowing. + +### Step 3 — Choose composition placement + +- **Always-on or variant-specific?** Composed into `createSimpleHlsEngine` + (always-on), or into a variant factory (`createAudioOnlyHlsEngine`, + etc.)? +- **Position in the composition order?** Per `packages/spf/docs/hls-engine.md`, + the composition has a logical order (lead-in: presentation resolution; + middle: per-track-type setup, MSE, segment loading; tail-out: adapter + integration). Where does this behavior slot in? +- **Per-type or uniform?** Per `conventions/behaviors.md`'s per-type vs + uniform-across-tracks decision — if the behavior's logic varies per + track-type, follow the per-type pattern (sibling behaviors + shared + helper); if uniform, compose against the aggregating resource. + +### Step 4 — Implement (TDD) + +1. **Write the test first.** `packages/spf/src/playback/behaviors/tests/ + .test.ts` (or sub-path per behavior placement). +2. **Run the test failing.** +3. **Implement the behavior** in `packages/spf/src/playback/behaviors/ + .ts`. Apply conventions throughout. +4. **Run the test passing.** +5. **Wire into the engine composition** at the position chosen in Step 3. +6. **Run the composition tests** (`engine.test.ts` and related) to verify + no regression. + +### Step 5 — Final-shape audit + commit + +Per the parent skill (`/spf-implement-feature`), commits are typically +batched at the feature-implementation level, not per-behavior. If invoked +standalone, propose a per-behavior commit shape and ask the user to confirm. + +Audit checklist: +- **Conventions adherence** — behaviors.md, signals.md, cleanup pattern +- **No scope creep** — does the implementation match the purpose + articulation from Step 1? +- **Tests cover the purpose** — does the test assert the business rule, not + just incidental implementation details? +- **Composition wiring complete** — engine composition tests pass + +## When this is the wrong skill + +- **Refactoring an existing behavior, purpose preserved** → `/refactor-behavior` +- **Updating an existing behavior, purpose changing** → `/spf-update-behavior` +- **Splitting / merging existing behaviors** → `/split-behavior` / + `/merge-behaviors` (often routed through `/refactor-behavior`) +- **Creating a media-layer or network-layer helper (not a behavior)** → + manual for now; future media-layer / network-layer skills will own this + +## How the failure-mode catalog grows + +Same pattern as other SPF skills: when a new failure mode surfaces during +use, add an entry to the catalog above with a worked-example citation. +This skill is a stub; the catalog is expected to grow significantly as +real use surfaces patterns. diff --git a/.claude/skills/spf-document-use-case/SKILL.md b/.claude/skills/spf-document-use-case/SKILL.md new file mode 100644 index 00000000..f2e552b7 --- /dev/null +++ b/.claude/skills/spf-document-use-case/SKILL.md @@ -0,0 +1,637 @@ +--- +name: spf-document-use-case +description: >- + Produce or update an entry in the SPF use-case-composition registry at + internal/design/spf/use-cases/. Triangulates context from multiple sources + (Notion, GitHub, pasted writeups, existing use-case docs, constituent + feature docs, codebase), grounds the use case in the four composition + mechanisms (subtract / add / alternative-impl / alternative-default-config), + applies use-case-specific cross-cutting concern checks, drafts the doc at + the appropriate definition depth, and cascades narrow updates to + constituent feature docs and sibling use cases. Triggers: "document + use case", "register use case", "use case doc", "update use case doc", + "deepen use-case stub", "new SPF use case", "use case composition", + "draft use-case registry entry", "new use case composition". +--- + +# Document an SPF Use-Case Composition + +Produce or update a use-case-composition registry doc at +`internal/design/spf/use-cases/.md`. The canonical failure mode without +this discipline is jumping from invocation to drafting — producing a doc that +mistakes a middle-pattern feature for a composition variant, conflates +delivery-mode choice (Case-2) with source-shape correctness (Case-1), defaults +to subtractive-only composition framing when other mechanisms also apply, +promotes use-case-specific glue behaviors to standalone features +unnecessarily, or fails to cascade cross-links to constituent feature docs. + +Steps 1–2 are the load-bearing ones. Skipping them produces drafts that look +superficially correct but anchor on the wrong scope, wrong doc-type, or wrong +constituent-feature mix. Steps 3–7 only make sense once the use case, sources, +and intent are named. Step 8 (cross-doc cascade) is where the registry stays +internally consistent rather than drifting. + +## Usage + +``` +/spf-document-use-case [] +``` + +The arg is optional. The skill is also invoked after the user pastes context, +links a Notion doc or GitHub issue describing a delivery scenario, or +describes a use case in conversation. + +## Reference docs + +Read these before drafting: + +- `internal/design/spf/use-cases/README.md` — **source of truth** for the + doc-type. Template, decomposition rubric, composition-mechanism taxonomy, + cross-link discipline, complexity-phase framing all live here. The skill + consults this at every step. +- `internal/design/spf/features/clusters.md` — `§ Composition vs Policy vs + middle pattern` is the discriminator for what genuinely qualifies as + composition vs the middle-pattern shape most candidates fail to. Cluster + signals also help identify constituent features at Step 3. +- `internal/design/spf/conventions/behaviors.md` — `§ One behavior or + several` + `§ Inverse: behaviors that operate uniformly across tracks`. + Composition-variant discipline; the `updateMediaSourceDuration` worked + example for how existing behaviors compose unchanged across variants. +- `internal/design/spf/use-cases/.md` if a doc already exists for the + use case — required reading for any deepen / update use case. +- `internal/design/spf/features/.md` — constituent feature + docs supplying the engine capabilities the use case rests on. Step 3 + grounding maps the variant's composition against these. +- `.claude/skills/spf-document-feature/SKILL.md` — parallel skill for + feature docs; consult for the analogous discipline shape and the + failure-mode catalog patterns that apply symmetrically. +- `internal/design/spf/evaluation-axes.md` — for Phase 3 (optimizations) + candidates that surface the Path-A (update existing behavior) vs Path-B + (create new behavior) judgment call. The use-cases/README.md + *Implementation note* section forward-refs this for the principle. +- `internal/decisions/*.md` — for past tactical decisions that may + constrain a use case's shape. +- `packages/spf/docs/hls-engine.md` — current HLS engine composition + walkthrough; useful for grounding the "what behaviors does the default + composition include" baseline that variants subtract from / add to. + +## Failure-mode catalog (grows with use) + +Inline checks embedded in the steps below. Each entry cites the principle +source or risk pattern; the catalog grows as new failure modes surface from +actual use. + +- **Composition-vs-middle-pattern misclassification** — invocation says + "composition" but the implementation shape is middle pattern (new + state-producing behavior + targeted edits to consumers, no engine-time + composition change). Per `clusters.md` § Composition vs Policy vs middle + pattern: *"Most 'feels like composition' items actually fit the middle + pattern."* Run this check explicitly. Signals of true composition: engine + factory composes a different behavior list; behaviors are subtracted / + added / swapped at composition time. Signals of middle pattern: behaviors + read a new state slot at runtime; all behaviors compose uniformly. When + middle pattern fires, route to `/spf-document-feature`. + +- **Feature-vs-use-case framing confusion** — the invocation describes an + engine capability (Case-1: "the engine handles this source-shape + correctly") but is framed as a use-case composition (Case-2: "the engine + is composed for this delivery scenario"), or vice versa. Same vocabulary + appears on both sides — *audio-only*, *video-only*, *live* can mean a + source-shape concern *or* a delivery-mode choice. Resolve by asking + explicitly: is this *source-shape correctness* (route to feature doc) or + *delivery-mode choice* (continue as use case)? + +- **Subtractive-only thinking** — defaulting to "which behaviors do we + leave out?" when the use case might also add behaviors, swap alternative + implementations, or change defaults. Run all four composition mechanisms + (subtract / add / alternative-impl / alternative-default-config) at + scoping time, even if only one ends up populated. The `clusters.md` row's + legacy *"ideally accomplished by subtraction only"* language pre-dates + the four-mechanism view in `use-cases/README.md` — the broader taxonomy + is the source of truth. + +- **Use-case-specific behavior promoted to feature unnecessarily** — a + variant-decision-glue behavior, composition-wiring behavior, or + single-scenario tuning gets a feature doc when it should live in the + use-case doc's *Composition specifics → Behaviors added* section. Apply + the same "earns its place" rubric `/spf-document-feature` uses: + substantial independent implementation footprint, independent + priority/timeline, or a primitive other engine consumers would draw on. + Failing all three → behavior stays in the use-case doc; no separate + feature registry entry. See `use-cases/README.md` § Cross-link discipline + → "When the constituent-features framing doesn't apply." + +- **Forgetting the Case-1 feature sibling** — when a use case is the + Case-2 axis of an existing Case-1 feature *and the two ship distinct + engine factories*, the cross-link must go both ways. The feature doc's + *Out of scope (separate concerns)* must reference the use case; the + use case's *Related features* / *See also* must reference the feature. + Step 8 cascade enforces; Step 7 audit catches misses. **Caveat:** when + the Case-1 and Case-2 framings ship the *same* engine factory (the + audio-only and video-only family pattern — see + `audio-only-mode-override.md` for the canonical example), they + consolidate into a single use-case doc with a *Variant-decision signal + source* section covering both paths. The Case-1 feature doc does not + exist separately in those cases. Not every use case has a Case-1 + sibling — `background-looping-video` has constituent features but no + single Case-1 axis-counterpart. + +- **Conflating sibling use cases** — e.g., `video-only-mode-override` and + `background-looping-video` both touch the video-only delivery + composition but address different delivery scenarios (video-without-audio + delivery vs Mux background-video product). Check at Step 5: distinct + customer story? distinct composition specifics? If yes, separate docs + with a *Related use cases* cross-link between them — not a merged doc. + Shared constituent features ≠ same use case. + +- **Composition-variant logic in always-on behaviors** — applies + symmetrically to use-case work; cross-ref the existing entry in + `/spf-document-feature`'s catalog. When a use case wants to bias an + always-on behavior's runtime, the answer is a per-variant alternative + implementation (composed in place of the default) or alternative default + configuration — not a runtime conditional branch in the always-on body. + +- **Constituent features vs vocabulary-sharing features** — + `audio-playback` is a *constituent feature* of + `audio-only-mode-override` (the use case composes the feature's + rendition selection + media playlist resolution + segment loading), + not just a vocabulary sibling. The constituent relationship is + "this use case composes the feature's behaviors." Vocabulary-sharing + alone is not constituent: `audio-abr` shares vocabulary with + `audio-only-mode-override` but is constituent only if the use case + composes audio-abr's behaviors into the variant. + +- **Cluster heuristic application via constituent features** — for any + use case, identify the constituent features first, then run the cluster + signals on those features (per `clusters.md` § Clusters → each cluster's + Signals list). This surfaces cross-cluster touchpoints transitively: a + use case's constituent feature's cluster pattern still applies to the + composition variant. + +- **Pre-deciding things the user wants left open** — open questions in + the doc are markers to think about, not prompts to resolve via the + edit. If the user says "this is required" or "this is not in scope," + update the doc to reflect the decision. If they ask for clarification, + don't resolve via the edit. + +## Steps (do these in order; do not skip) + +### Step 1 — Identify the use case and gather source materials + +The load-bearing setup step. Triangulate the use case from every available +source: + +- **The user's invocation message.** Use-case name? Description? Delivery + scenario? Customer story? Link(s)? +- **Linked Notion docs.** Fetch via the Notion MCP tool. Case-2 epics in + the SPF Epics Working Document are the primary source. +- **Linked GitHub issues.** Fetch via `gh issue view <#>`. Mux product + scenarios (e.g., `mux-background-video`) often anchor consumer + context. +- **Pasted writeups in the conversation.** Read carefully — run the + scope-writeup vintage check (current or historical?). +- **Existing use-case doc.** Check + `internal/design/spf/use-cases/.md` and obvious aliases. +- **Closely related use-case docs.** Pull in any documented use cases — + shared customer context, shared constituent features, inverse-axis + siblings. +- **Constituent feature docs.** Consult `features/clusters.md` to identify + which clusters this use case likely touches; pull in the documented + features from those clusters as candidate constituents. + +**Constituent-feature identification.** Most use cases have multiple +constituent features. Identify them at Step 1 — they drive Steps 3, 4, 5, 8. +Common shapes: + +- Delivery-mode-from-mixed-source use cases (audio-only-mode-override, + video-only-mode-override) → constituent features include the parallel + Case-1 source-shape feature plus the baseline playback / buffer / + selection features. +- Product-scenario use cases (background-looping-video, shorts-player) → + constituent features include multiple capability features the variant + assembles. + +**Decomposition check (load-bearing).** Run the 4-criterion rubric from +`use-cases/README.md` § Decomposition rubric: + +1. **Uses composition mechanisms** (subtract / add / alternative-impl / + alternative-default-config) — not runtime config on always-on behaviors. +2. **Names a delivery scenario** — recognizable consumer mode. +3. **Has constituent features** — at least one Case-1 feature. +4. **Names a customer/consumer scenario** — who consumes this and what + product story. + +**Counter-routes when criteria fail:** + +- Fails (1) → middle pattern or cluster-E policy → `/spf-document-feature`. +- Fails (2) or (4) → composition-variant *phase row* inside an existing + feature doc (composition-variant pattern from + [`../conventions/behaviors.md` § Inverse: behaviors that operate + uniformly across tracks](../conventions/behaviors.md#inverse-behaviors-that-operate-uniformly-across-tracks)), + not standalone. +- Fails (3) → either the features aren't documented yet (write them + first via `/spf-document-feature`) or this isn't actually a use-case + composition. + +**Weak-criterion surface check.** When the rubric fires only *weakly* on +one or more criteria, surface alternative framings proactively in the +Step 1 report — same discipline as `/spf-document-feature`'s +weak-criterion check. The user gets to see the judgment call rather than +having to surface it themselves. + +**Stop and report back to the user** with: + +1. The use-case name (your best read). +2. Sources consulted (with links). +3. Existing doc status (none / coarse / technical / sketched). +4. Likely constituent features identified (with confidence notes). +5. Rubric criteria firing strongly / weakly / failing. +6. **Recommended framing** — new standalone use-case doc / extend + `` / extend ``'s + composition-variant phase row / route to `/spf-document-feature` — + with rubric reasoning. Always have a recommendation; don't hedge. +7. Ambiguities still unresolved (going into Step 2's discussion). + +This is the load-bearing step. Getting it wrong invalidates everything +downstream — same failure shape as `/spf-document-feature`'s Step 1 +misdiagnosis and `/refactor-behavior`'s Step 1 purpose-articulation. + +### Step 2 — Discuss to resolve ambiguities + +An **explicit conversational stage** — not optional, not implicit. After +Step 1's report, drive toward answers for the questions that remain: + +- **Implementation status.** Implemented? Partially? Not at all? Engine + variant composed today vs proposed? +- **User's intent.** Register a new use case? Deepen an existing coarse + stub? Update an existing doc because something changed? Discuss only + (no draft)? +- **Definition depth target.** Coarse / technical / sketched? Default + heuristic same as feature docs: implemented and code-grounded → + sketched; proposed / under-discussion → coarse; scope articulated but + no implementation → technical. +- **Composition mechanism mix.** Which of the four mechanisms (subtract / + add / alternative-impl / alternative-default-config) is the use case + likely to use? Often more than one. Asking up front grounds Step 3. +- **Customer-policy surface.** What's the consumer-facing API surface + the variant exposes? (Loop flag, autoplay-muted, buffer targets, etc.) +- **Scope confirmation.** If any gathered material reads like scope + framing (Notion epics, kickoff docs, product specs), explicitly ask + whether it's current or historical context. +- **Concurrent considerations.** Are there related use cases the user + wants tackled in concert, or are they cross-refs only? +- **Anything else** the source materials didn't clearly resolve. + +**Use `AskUserQuestion`** when the choice is clear-cut and short-listable +(definition depth, implementation status, register-vs-update intent, +framing). Use free-form discussion when the question doesn't enumerate +cleanly (scope nuance, what's "related enough"). + +**Lead with Step 1's recommendation.** Per the system instructions on +`AskUserQuestion`, when you have a recommended option, it goes first and +is labeled `(Recommended)`. The Step 1 decomposition check produces this +recommendation — carry it through into Step 2's question rather than +presenting equivalent options without a recommendation. + +**Discuss-only mode.** If the user signals they want to think out loud +without producing a doc, stay in Step 2 indefinitely until they +explicitly ask to draft. + +### Step 3 — Ground the use case in the codebase + +Required for `sketched` and `technical` definition depths; abbreviated +for `coarse`. + +**For implemented variants (sketched depth).** Dispatch an `Explore` +agent or read code directly to map: + +- **Composition specifics.** Which engine factory composes the variant? + Which behaviors are subtracted, added, or swapped vs the default + composition? Compare against `createSimpleHlsEngine`'s behavior list + in `packages/spf/src/playback/engines/hls/engine.ts` as the baseline. +- **Constituent features grounded.** For each constituent feature + identified at Step 1, map the specific behaviors / actors / state + slots the variant composes. Per-feature relationship per + `use-cases/README.md` template: used as-is / alternative defaults / + alternative implementation of behavior X. +- **Customer-policy surface in code.** Config inputs the variant accepts. +- **Variant-decision signal in code.** Adapter-upfront vs detect-from- + parser — where does the variant get selected? +- **Tests covering the variant.** E.g., `engine.test.ts` "handles + audio-only stream" for the audio-only path. + +**For not-yet-implemented variants (coarse depth).** Identify the pieces +the variant would *touch*, not the implementation itself: + +- Which existing behaviors would be subtracted / added / swapped? +- Which constituent features supply the baseline? +- Which behaviors would need alternative implementations (Path B per + `use-cases/README.md` § Implementation note)? +- Which alternative defaults would the variant configure? +- Which variant-decision signal source would drive selection? + +This output feeds the doc's *Composition specifics*, *Constituent +features*, and *Likely cross-cutting impact* sections. + +### Step 4 — Apply cross-cutting concern checks + +Run the failure-mode catalog and the cross-cluster patterns from +`clusters.md` against everything gathered. Cluster patterns apply +*through constituent features* — a constituent feature's cluster +patterns transfer to the use case that composes it. + +**The use-case-specific failure-mode catalog** (this skill's catalog +above). Each check fires when its signals are present in the use case's +description, grounded code, or constituent features. + +**The cross-cluster pattern checks (per `clusters.md`).** Apply each via +the constituent features: + +- **Gating / prerequisite chains** — variant adds a gate on an existing + behavior or introduces a prerequisite signal? +- **Multi-writer state slots** — variant adds a writer to a slot the + default composition's behaviors already write? +- **Constraint + filter** — variant introduces a slot that narrows a + default-composition behavior's candidate set? +- **Per-type specialization** — variant interacts with per-type + behaviors (video/audio/text siblings)? +- **Sampling-baked-into-loading** — variant changes loading flow in a + way that affects sampling? + +**The composition-mechanism check.** For each of the four mechanisms, +which behaviors are affected? This drives the *Composition specifics* +section's per-bucket breakdown. + +**Output of this step.** A list of: which patterns / checks fired, what +they imply for the doc's *Likely cross-cutting impact* section, what +behaviors qualify as use-case-specific (vs constituent), what +cross-references they pull in. + +### Step 5 — Pick phase framing and identify relationships + +**Phase framing.** Default to the three-phase complexity framing from +`use-cases/README.md` § The three default complexity phases: + +- **Phase 1 — Basic functionality.** Minimum viable variant on existing + / generic behaviors. +- **Phase 2 — Features/functionality relevant to the use case.** + Constituent features composed in beyond the baseline. +- **Phase 3 — Optimizations.** Alternative implementations / default + configurations that improve the variant's quality of delivery. + +Other framings allowed when this doesn't fit (e.g., a use case with no +meaningful optimization phase). The skill picks the framing per-use-case. + +**Relationships.** Sort related items into the right buckets per +`use-cases/README.md` § Cross-link discipline: + +- **Constituent features (always)** → *Constituent features* section, + with per-feature relationship (used as-is / alternative defaults / + alternative implementation). +- **Use-case-specific behaviors (sometimes)** → *Composition specifics → + Behaviors added*. Apply the "earns its place" rubric to confirm they + shouldn't promote to feature docs. +- **Direct Case-1 sibling (sometimes)** → *Related features* or *See + also* with the sibling framing made explicit. +- **Sibling use cases (sometimes)** → *Related use cases* section. +- **Cross-refs to existing docs** → *See also*. +- **Forward refs to candidate use cases / features (no doc yet)** → + bracketed entries (per registry convention). +- **Open questions** → *Open questions* section. + +**"One use case or many?" decomposition check.** Before locking the +phases in, ask: is this really one use case, or is the variant actually +a decomposition into multiple use cases? Heuristic: a slice belongs in +its own doc if it has (a) a distinct customer story, (b) distinct +composition specifics, or (c) independent timeline. Worked example: +`video-only-mode-override` vs `background-looping-video` — both +exercise video-only delivery composition, distinct customer +stories, distinct composition specifics. + +### Step 6 — Draft (or update) the doc + +Write the file at `internal/design/spf/use-cases/.md` using the +template from `use-cases/README.md` § Template for individual use-case +docs. Section presence varies by definition depth (same table shape as +feature docs): + +| Section | coarse | technical | sketched | +|---|---|---|---| +| Frontmatter (`status`, `date`, `definition`) | ✓ | ✓ | ✓ | +| Opening paragraph | ✓ | ✓ | ✓ | +| Status | ✓ | ✓ | ✓ | +| Target delivery context | ✓ | ✓ | ✓ | +| Phases of complexity | ✓ | ✓ | ✓ | +| Composition specifics | partial | ✓ | ✓ | +| Constituent features | ✓ | ✓ | ✓ | +| Customer-policy surface | partial | ✓ | ✓ | +| Variant-decision signal source | partial | ✓ | ✓ | +| Likely cross-cutting impact | ✓ | partial | — | +| Open questions | ✓ | partial | partial | +| Related use cases | ✓ | ✓ | ✓ | +| See also | ✓ | ✓ | ✓ | + +**For updates to existing docs.** Preserve structure; make targeted +edits. Don't rewrite sections wholesale unless the user asks. Open +questions resolved through conversation update the section the answer +constrains; the open question itself gets removed. + +**Show the user before treating the draft as final.** Iteration is +expected — failure-mode catalog updates may surface during user review. + +### Step 7 — Final-shape audit + +A deliberate second pass against the file as written. Most misses come +from the diff itself, not the pre-draft analysis. Run through: + +- **Frontmatter** — `status`, `date`, `definition` match Step 2's + agreement? +- **Phase framing** — the choice from Step 5 reflected, not silently + drifted to a different shape? +- **Composition specifics** — all four mechanism buckets (subtract / + add / alternative-impl / alternative-default-config) considered, even + if some are empty? Empty buckets noted explicitly? +- **Constituent features** — each one listed with the per-feature + relationship (used as-is / alternative defaults / alternative + implementation of behavior X)? +- **Use-case-specific behaviors** — each one in *Composition specifics + → Behaviors added* passes the "earns its place" rubric for staying in + the use-case doc (not promoting to feature)? +- **Case-1 sibling check** — if the use case has a direct Case-1 + sibling, is the cross-link present in *Related features* / *See also*? +- **Cross-cutting concerns** — each pattern / check that fired in Step + 4 surfaced in the doc somewhere? +- **Cross-refs** — bracketed entries for not-yet-documented items? + Plain links for existing docs? `See also` links resolve? +- **Implementation claims grounded** — every concrete behavior / actor / + file-path reference came from Step 3's exploration, not invented? +- **Open questions appropriate** — at coarse depth, open questions are + a feature; at sketched, they should cross-reference where they're + being tracked. +- **Resolved questions not lingering** — anything resolved during Step + 2 / Step 6 landed in its constraining section and cleared from open + questions? + +### Step 8 — Cross-doc cascade + +After the use-case doc is final, **survey other docs for narrow updates** +this draft entails. The cascade for use cases is heavier than for feature +docs because of the bidirectional cross-link discipline (use cases compose +features; features track which use cases compose them). + +**Cascade candidates:** + +- **Each constituent feature doc.** Add a *Use cases that compose this + feature* entry (create the section if it doesn't exist yet). This is + the cascade's load-bearing step — feature docs without this + back-reference become stale. +- **Direct Case-1 sibling feature doc.** If the use case is the Case-2 + axis of an existing feature, update the feature doc's *Out of scope + (separate concerns)* to reference the use case by name (drop any + "yet-to-be-formalized" placeholder language). Cross-confirm the use + case's *Related features* / *See also* references the feature. +- **Sibling use case docs.** Add bidirectional *Related use cases* + cross-links. +- **`use-cases/README.md` Index.** Move the entry from bracketed + `[name]` to plain `name` and update its description if needed. +- **`features/clusters.md`.** If a new composition pattern surfaced + worth recording — e.g., the use case demonstrates a new composition + mechanism shape — note in `§ Composition vs Policy vs middle + pattern` or `§ Cross-cluster patterns`. Note also: the current + Composition row's *"ideally accomplished by subtraction only"* + language is broader than the four-mechanism view in + `use-cases/README.md`; consider whether the row's Definition cell + needs softening as part of this cascade. +- **`packages/spf/docs/hls-engine.md`.** If the use case introduces a + new engine-variant factory, update the engine composition walkthrough. + +**Discipline for cascade edits:** + +- **Narrow** — add references, note relationships; don't restructure + other docs. +- **Per-doc confirmation** — propose each candidate edit explicitly; + user accepts / declines / modifies per doc. +- **Bounded** — only docs this use case explicitly references plus docs + that reference this use case (i.e., its constituent features and any + Case-1 sibling). Don't go fishing for unrelated cross-refs. + +**Cascade may also trigger updates to `use-cases/README.md`** — a new +composition mechanism worth naming, a new failure mode for the catalog, +a template adjustment. These are part of the same cascade. + +### Step 9 — Commit (with user confirmation) + +After Step 7 audit is clean and Step 8 cascade edits are agreed: + +1. **Audit working-tree state.** `git status -s`. Surface any + pre-existing uncommitted work on files outside the doc scope; never + commit files the user didn't ask you to touch. +2. **Propose a commit structure.** Common shapes: + - **Single commit** — new use-case doc only, no cascade. Coarse stubs + for novel use cases (no constituent features documented yet) may + land here, though this is rare. + - **Doc + constituent-feature cascade** — new use-case doc plus + *Use cases that compose this feature* additions to constituent + feature docs. Most use-case docs land here. + - **Doc + Case-1 sibling cascade** — new use-case doc plus update to + the Case-1 sibling feature doc dropping placeholder language. + Often combined with constituent-feature cascade. + - **Doc + README index update** — new use-case doc plus moving from + bracketed forward-ref to plain entry in `use-cases/README.md`'s + Index. + - **All of the above** — large new use cases that earn updates across + multiple constituent features, sibling feature, README, and + possibly clusters.md. +3. **Ask the user to confirm via `AskUserQuestion`.** Options include + "Land all commits as proposed," "Bundle into one commit," and + "Skip — I'll handle commits." +4. **On confirmation, run the commits.** Stage per-commit by name (no + `-A` / `git add .`), use the repo's commit-message conventions + (`docs(spf)` scope per the project's `git` skill). +5. **On decline or skip, stop.** The user owns the commit boundary. + +## Output format + +Propose the doc in this order before writing the file. Use markdown +headers for each numbered section. Do not write the file until the user +confirms. + +1. **Use-case identification** (Step 1's report — name, sources, + existing doc status, constituent features, rubric criteria firing, + recommended framing) +2. **Ambiguities to resolve** (Step 2 — questions for the user) +3. **Grounding summary** (Step 3 — composition specifics / constituent + features grounded / customer-policy surface, or what-it-would-touch + for coarse) +4. **Cross-cutting concerns identified** (Step 4 — which checks fired, + composition mechanism mix, what they imply) +5. **Phase framing + relationships** (Step 5 — phase shape, what's in + each scope bucket) +6. **Proposed doc** (Step 6 — the file content, ready to write) +7. **Cross-doc cascade** (Step 8 — proposed updates to constituent + feature docs, Case-1 sibling, README, sibling use cases) + +After user confirmation, write the file, run Step 7 audit, propose +Step 9 commit structure. + +## Why this order + +The canonical failure: invocation → drafting, skipping the +triangulation and discussion that surface the actual scope, doc-type, +and constituent-feature mix. Steps 1–2 force the framing before +mechanical work. Step 3 grounds claims in code; Step 4 runs the +failure-mode catalog while context is fresh; Step 5 commits the +structural choices before drafting. Step 6 produces the artifact. Step +7 is the second-pass audit that catches diff-introduced misses. Step 8 +keeps the registry internally consistent (heavier than for feature docs +because of the bidirectional cross-link discipline). Step 9 hands the +commit boundary back to the user. + +## Why a discussion stage (not implicit) + +Source materials are often under-specified or ambiguous in ways the +user didn't notice until asked. The conversational stage is explicit +because making it implicit produces drafts that look right but anchor +on the wrong doc-type — the feature-vs-use-case framing confusion is +the canonical example. Step 2 short-circuits to drafting only when +ambiguities are genuinely resolved by Step 1's gathering. + +## When this is the wrong skill + +- **You want to document an engine capability (Case-1)** → + `/spf-document-feature`. Capability docs answer "what can the engine + do?"; use-case docs answer "how is the engine composed for this + delivery scenario?" +- **Your candidate's implementation shape is middle-pattern, not + composition** → `/spf-document-feature`. Per `clusters.md`: most + candidates fail this check. +- **You want to refactor an existing behavior** → `/refactor-behavior`. +- **You want to split or merge behaviors** → `/refactor-behavior`'s + Step 3 / Step 6a may route you to `/split-behavior` or + `/merge-behaviors`. +- **You want to write an architectural design doc** → `design` skill. + Architectural concerns live in `internal/design/spf/` directly, not + under `use-cases/`. +- **You want to write an RFC for a cross-team decision** → `rfc` skill. +- **You want to write user-facing documentation** → `docs` skill. + +## How the failure-mode catalog grows + +When a new failure mode surfaces during use (most likely during Step 6 +draft review or Step 7 audit): + +1. Add an entry to the *Failure-mode catalog* section above with the + risk pattern and a worked-example citation. +2. If the failure-mode is cluster-pattern-shaped, also update + `clusters.md` § Cross-cluster patterns. +3. If the failure mode is doc-type-shaped (template, rubric, cross-link + discipline), also update `use-cases/README.md` in the relevant + section. +4. Note in the commit that the skill itself grew — `docs(spf): …` + commit scope covers skill updates. + +The catalog is the load-bearing distinction between this skill and a +generic "write a use-case doc" prompt. Every entry exists because a real +risk was hit (or, for the seeded entries, identified at skill-creation +time from cross-skill failure-mode patterns); keeping the catalog +up-to-date is the mechanism that keeps the skill earning its keep. diff --git a/.claude/skills/spf-implement-feature/SKILL.md b/.claude/skills/spf-implement-feature/SKILL.md new file mode 100644 index 00000000..6ec67a7e --- /dev/null +++ b/.claude/skills/spf-implement-feature/SKILL.md @@ -0,0 +1,558 @@ +--- +name: spf-implement-feature +description: >- + Implement a feature documented in the SPF feature registry. Consumes a + feature doc at internal/design/spf/features/.md and produces the + engine-side code: new behaviors, updates to existing behaviors, media-layer + / network-layer primitives, and tests. The implementation analog of + /spf-document-feature (which produces the doc; this consumes it). Walks + through resolving the doc's open questions before coding, maps phases to + discrete chunks, applies the SPF conventions catalog, routes to downstream + skills (/spf-create-behavior, /spf-update-behavior, /refactor-behavior) per + chunk shape, and updates the feature doc's Status / Implementation surface + / Verification sections as code lands. Triggers: "implement feature", + "implement SPF feature", "build feature", "code feature", "scope feature + implementation", "implement ". +--- + +# Implement an SPF Feature + +Take a feature documented at `internal/design/spf/features/.md` and +produce the engine-side code that satisfies its Phase 1 (and optionally Phase +2 / Phase 3) scope. The canonical failure mode without this discipline is +jumping from "implement audio-abr" to writing code — producing implementation +that doesn't match the doc's grounding, drifts beyond the agreed phase scope, +misses the conventions catalog, or skips the doc-update cascade that keeps +the feature registry current as code lands. + +Steps 1–2 are the load-bearing setup. Skipping them produces implementations +that look superficially correct but anchor on the wrong scope, miss the doc's +open questions, or fail to coordinate with cross-cutting concerns. Steps 3–7 +only make sense once the feature, sources, and intent are named. Step 8 (doc +update as living artifact) is where the registry stays in sync with code. + +## Usage + +``` +/spf-implement-feature [] +``` + +The arg is the feature doc's filename (without extension), e.g. +`audio-abr`. The skill reads the feature doc as its source of truth. + +## Doc-as-starting-point principle + +The feature doc is the **starting point for planning, not a hardened +specification**. This principle is load-bearing throughout the skill — +explicit because both directions of failure are real: + +- **Silent override** (covered by `Feature-doc-grounding drift` failure + mode) — implementation diverges from the doc without surfacing the + divergence; the doc becomes stale silently. +- **Rigid following** (covered by `Treating feature doc as hardened spec` + failure mode) — implementation refuses to revise the doc when new + questions surface or drift is discovered; the doc becomes a misleading + constraint. + +Acknowledge the doc's `definition` depth explicitly when planning: + +- **`coarse`** — feature sketched, many open questions. Implementation + fills in significant detail; revisions to the doc are expected + throughout. Planning is substantial. +- **`technical`** — scope and constraints articulated; specifics still + open. Implementation maps to constraints; moderate planning + revision. +- **`sketched`** — implementation surface populated. Implementation + primarily verifies; revisions for drift only. + +Discipline: + +- **Every doc revision is explicit and surfaced** to the user during the + implementation pass. Never silent. +- **Step 8 consolidates** the cumulative doc update reflecting all + revisions made during implementation — it's not the *only* revision + point, just the cumulative one. +- **Open questions are markers**, not absent specs — the implementation + resolves them through experience or explicitly defers them. + +## Reference docs + +Primary: + +- `internal/design/spf/features/.md` — **source of truth** for what + to implement. Read end-to-end before scoping. +- `internal/design/spf/conventions/*.md` — when to reach for which primitive + (behaviors, signals, reactors, actors, config). Applied throughout + implementation. +- `internal/design/spf/evaluation-axes.md` — A–E axes the implementation is + scored against. Cleanup pass and feature work share the same axes. +- `packages/spf/docs/hls-engine.md` — current HLS engine composition + walkthrough; the baseline the implementation extends. +- `packages/spf/src/CLAUDE.md` — source layout + dependency rules. + +Secondary: + +- `internal/design/spf/use-cases/.md` — if implementing for a + use-case-specific path (variant composition). +- `internal/decisions/*.md` — past tactical decisions constraining shape. +- `internal/design/spf/architecture.md`, `primitives.md`, `signals.md` — + implementation-level "how it works." +- Existing similar behaviors / actors as templates (e.g., + `switchVideoQuality` as a template for `switchAudioQuality` when + implementing audio-abr). + +Downstream skills routed-to: + +- `.claude/skills/spf-create-behavior/SKILL.md` — new behaviors. +- `.claude/skills/spf-update-behavior/SKILL.md` — existing behaviors whose + purpose is changing. +- `.claude/skills/refactor-behavior/SKILL.md` — existing behaviors whose + purpose is preserved but implementation improves. +- `.claude/skills/split-behavior/SKILL.md`, `.claude/skills/merge-behaviors/SKILL.md` + — structural changes. +- *(future)* media-layer / network-layer skills for `packages/spf/src/media/` + and `packages/spf/src/network/` changes. + +## Failure-mode catalog (seeded; grows with use) + +- **Skipping disambiguation** — invoking with a name or description that + ambiguously maps to (i) an existing feature, (ii) a different feature, + (iii) a use case, (iv) cluster-E policy, (v) something not yet + documented. Step 1's disambiguation must resolve before gathering + sources or planning. Proceeding-with-assumption is the canonical + failure shape. Worked example: invocation "implement resolution + capping" — could be (i) the `rendition-selection-caps` feature (the + right route; cluster-E selection policy), (ii) a use-case composition + shape (incorrect — composition vs runtime config), or (iii) something + else entirely. The disambiguation discipline catches it. Another + worked example: invocation "implement audio-only" maps to the + `audio-only-mode-override` use case (which absorbed what was + previously framed as a separate `audio-only-composition` feature); + the disambiguation is between use case and cluster-E policy, not + between feature framings. + +- **Routing-out failure** — Step 1's disambiguation should route + confidently. The failure mode is "ambiguity discovered but not + resolved" — silently picking one interpretation when the user could + have meant another (especially: a request that's actually a use case + routed-here as a feature, or vice versa). Always surface and confirm. + +- **Treating feature doc as hardened spec** — refusing to revise the + doc when implementation reveals new questions, refines framing, or + surfaces drift. Inverse failure of silent-override (see + `Feature-doc-grounding drift`). The right discipline is explicit, + user-surfaced revision per the *Doc-as-starting-point principle* + section above. + +- **Skipping the open-questions discussion** — feature docs at coarse depth + have unresolved open questions that block implementation. Resolving them + silently via the edit loses the user's design intent. Always discuss + before coding. Worked example: audio-abr's "Audio caps" open question + needs resolution before Phase 1 — does the implementation honor a max- + audio-bitrate cap as a constraint+filter, or is it deferred to Phase 2? + +- **Implementing beyond the agreed phase scope** — feature docs have + multiple phases. The skill must explicitly scope to one or a subset, not + "implement the whole feature." Worked example: audio-abr Phase 1 is + "BandwidthState reuse + switchAudioQuality behavior parallel to + switchVideoQuality"; landing Phase 2 (multi-signal extensions) at the + same time is scope creep. + +- **Conventions catalog under-application** — SPF conventions (behaviors, + signals, reactors, actors, config) all apply during implementation. + Failing to consult them produces code that "works" but doesn't match + patterns, requiring later refactor. + +- **Feature-doc-grounding drift** — implementing something that doesn't + match what the feature doc said. Either the doc is wrong (update it) or + the implementation is wrong (fix it). Don't silently diverge — the doc + is the spec, and divergence either way invalidates it. + +- **Cross-cutting impact under-checked** — the doc's *Likely cross-cutting + impact* section flags decisions the implementation forces elsewhere. + Skip → cascading-impact misses. Worked example: implementing audio-abr + without verifying `bandwidthState` multi-writer characterization (audio + samples + video samples both writing) misses the EWMA-mixed-source + concern the doc flags. + +- **Multi-writer slot mishandling** — adding a writer to a slot another + behavior already writes requires multi-writer characterization (per + `conventions/signals.md`). Default-merge or default-overwrite without + coordination is a bug. + +- **Test-after-the-fact implementation** — TDD discipline per chunk: test + → implement → verify. Implementation-first produces tests that pass by + construction. + +- **Composing into wrong engine factory** — variant-specific behaviors + compose into the variant factory (per `use-cases/`); the default factory + stays clean. Cross-ref to use-cases/README composition discipline. If a + use case has a Case-2-only behavior, it doesn't belong in + `createSimpleHlsEngine`. + +- **Status / Implementation-surface update skipped** — feature doc + transitions from coarse → sketched as code lands. The doc must be + updated as part of the implementation; deferring means future agents + see stale status. Step 8 enforces. + +- **Downstream skill missing — silent inline implementation** — when a + chunk hits a "Yes" row in the downstream-skill-needed table (new + behavior, non-trivial behavior update), and the downstream skill + doesn't exist yet, the failure mode is to silently apply discipline + ad-hoc. Step 6 explicitly surfaces this: branch on (i) defer chunk + pending downstream skill, (ii) build the downstream skill inline now, + (iii) apply discipline ad-hoc with explicit "extract later" flag. + +## Steps (do these in order; do not skip) + +### Step 1 — Identify the feature + disambiguate the request + +The load-bearing setup step. **Disambiguation comes first** — before +gathering sources or planning, confirm what the user actually wants. + +**1a. Identify the candidate.** + +- If the user passed a name → verify + `internal/design/spf/features/.md` exists. +- If the user passed a description (no name) → parse for candidates, + match against existing feature docs. +- If multiple candidates match → surface options to the user. + +**1b. Verify the candidate is actually a feature.** + +Apply the discriminator from `features/clusters.md` and the use-cases +boundary: + +- **Source-shape correctness or engine capability?** If yes → feature + (Case-1), stay here. +- **Delivery-mode choice / variant assembly?** If yes → this is a + **use-case composition** (Case-2) → route to `/spf-implement-use-case`. +- **Runtime policy tuning without composition change?** If yes → still + a feature (cluster-E policy), but the implementation shape is + config/middle-pattern, not composition; stay here. +- **Ambiguous between Case-1 and Case-2?** Same vocabulary often appears + on both sides (audio-only, video-only, live). Surface to user; don't + pick silently. + +**1c. Route the request appropriately.** + +- **Stays here** — confirmed feature, doc exists. Proceed to 1d. +- **No doc exists for the candidate** → route to `/spf-document-feature` + to produce the doc; return here once doc lands. +- **It's actually a use case, doc exists** → route to + `/spf-implement-use-case`. +- **It's actually a use case, no doc exists** → route to + `/spf-document-use-case` first, then `/spf-implement-use-case`. +- **Ambiguous between options** → surface to user; do not pick silently. + +**1d. Gather sources.** (Once routing is confirmed and we're staying here.) + +Triangulate from every available source: + +- **The user's invocation message.** Feature name? Phase scope target? + Specific concerns? +- **The feature doc itself.** Read end-to-end. Pay attention to: Status + block (what's already there?), Phases of complexity (what scope is + available?), Open questions (what blocks implementation?), Likely + cross-cutting impact (what will the implementation force elsewhere?), + Implementation surface / What's not implemented (where in code?). + Note the doc's `definition` depth and `status` per the *Doc-as- + starting-point principle* above. +- **Constituent feature docs.** Per the use-cases/README.md framing, + features may compose into use cases. If implementing for a use case, + read the use-case doc to understand the composition target. +- **Similar implementations as templates.** Identify analog behaviors + in the codebase. Worked example: implementing `switchAudioQuality` + benefits massively from reading `switchVideoQuality` first — same + shape, audio-axis. +- **Conventions catalog.** Skim `conventions/*.md` for relevance signals. +- **Recent ADRs.** Check `internal/decisions/` for tactical decisions + that constrain the implementation shape. + +**Stop and report back to the user** with: + +1. The feature name and the agreed phase scope (Phase 1? subset of + Phase 1's rows? multiple phases?). +2. Sources consulted (with links). +3. Feature doc status — what's already there vs what needs implementing. +4. Likely template implementations (similar behaviors / actors / helpers). +5. Open questions blocking implementation (going into Step 2's + discussion). +6. **Recommended phase scope** for this implementation pass. + +### Step 2 — Discuss to resolve open questions + +An **explicit conversational stage** — not optional, not implicit. After +Step 1's report: + +- **Walk through the feature doc's Open questions section.** Which need + to resolve *before* coding? Which can stay open as known-unknowns? +- **Confirm phase scope.** Phase 1 only? Phase 1 + Phase 2 subset? + Specific phase rows? The implementation must scope to a concrete chunk + list. +- **Confirm composition mechanism per chunk** — (i) subtractive (no new + code), (ii) config-driven (existing behavior gains a knob), (iii) + new behavior (route to `/spf-create-behavior`), (iv) behavior update + with purpose change (route to `/spf-update-behavior`), (v) behavior + refactor with preserved purpose (route to `/refactor-behavior`), (vi) + media-layer / network-layer change (handle inline or defer per the + downstream-skill-missing branch). +- **Resolve open questions the implementation needs.** Per the + pre-deciding-things failure mode, only resolve what the implementation + forces; leave the rest as open questions in the doc. + +**Use `AskUserQuestion`** for clear-cut choices (phase scope, +composition mechanism per chunk, open-question resolutions). + +### Step 3 — Map phases to implementation chunks + +Per the agreed scope, decompose into discrete chunks. Each chunk is: + +- **Small enough to TDD individually** — one test (or small test set), one + implementation file change, one composition wiring tweak. +- **Categorized by composition mechanism** — drives Step 6's routing. +- **Sequenced for least-risk order** — primitives before behaviors; + behaviors before composition wiring; composition wiring before + integration tests. + +**Output of this step.** A chunk list per the table shape: + +| Chunk | Mechanism | Downstream skill | Test target | +|---|---|---|---| +| Add bandwidthState audio sampling | Update existing | `/spf-update-behavior` (setupAudioBufferActors) | `setup-buffer-actors.test.ts` audio sampling assertion | +| Create switchAudioQuality behavior | New behavior | `/spf-create-behavior` | `switch-audio-quality.test.ts` (new) | +| Wire switchAudioQuality into composition | Composition | None | `engine.test.ts` composition assertion | + +### Step 4 — Apply cross-cutting concern checks + +Run the failure-mode catalog and the feature doc's *Likely cross-cutting +impact* section against the chunk list. Each check fires when its signals +are present: + +- **Conventions catalog application** — per chunk, which conventions + apply? (behaviors.md for behavior creation/update; signals.md for slot + map design + multi-writer; reactors.md for FSM-driven behaviors; + actors.md for actor creation/consumption; config.md for config surface). +- **Multi-writer characterization** — for any slot the implementation + writes that another behavior already writes, three-axis check per + `conventions/signals.md`. +- **Per-type vs uniform-across-tracks** — per `conventions/behaviors.md`, + ensure per-type chunks follow the sibling-behaviors-plus-shared-helper + pattern; uniform chunks compose against the aggregating resource. +- **Composition-variant logic** — variant-specific chunks go in variant + factories, not the default factory. +- **MSE codec-change implications** — per `/spf-document-feature`'s + catalog, if the chunk touches buffer behavior and codecs change, + surface the `changeType()` vs `flushBuffer` question explicitly. + +### Step 5 — TDD plan + +For each chunk, name: + +- **The test** — file path, test name, what it asserts. +- **The implementation target** — file path, behavior/actor/helper name. +- **The composition wiring change** — if any. +- **Acceptance criterion** — what does "done" look like for this chunk? + +This output drives Step 6's per-chunk implementation loop. + +**The TDD plan is the seed of the feature doc's *Verification* +section.** Step 8 persists each chunk's test (file path + test name + +assertion summary) into the feature doc — the TDD plan does not live +only in chat. Name tests with assertion summaries suitable for the +doc from the start, so Step 8 is a transcription pass rather than a +re-articulation. + +### Step 6 — Implement (test-first per chunk; route to downstream skills) + +Iterate per chunk: + +1. **Write the test first.** Run it failing. +2. **Branch by mechanism:** + - **Subtractive / composition wiring** — handle inline. + - **Config-driven** — handle inline. + - **New behavior** — route to `/spf-create-behavior` for this chunk. + - **Behavior update (purpose changing)** — route to `/spf-update-behavior`. + - **Behavior refactor (purpose preserved)** — route to `/refactor-behavior`. + - **Structural (split/merge)** — route via `/refactor-behavior`'s + decomposition check. + - **Media-layer / network-layer** — handle inline for now; future + skills will own these. +3. **Run the test passing.** +4. **Run composition tests** to verify no regression. + +**Downstream skill missing — explicit handling.** When a chunk routes to +a downstream skill that doesn't exist yet (or is only a stub): + +- **Defer this chunk** pending the downstream skill's full development — + if the chunk isn't load-bearing for the implementation goal. +- **Build the downstream skill inline now** — pause the implementation, + invoke `/create-skill` to build the missing discipline, resume the + implementation after the new skill lands. +- **Apply discipline ad-hoc with "extract later" flag** — implement the + chunk with explicit awareness that the discipline isn't yet codified; + flag in commit message and surface for skill extraction later. + +The user makes the call. Don't apply ad-hoc discipline silently. + +### Step 7 — Final-shape audit (per chunk + cumulative) + +After each chunk: + +- **Test passes? Composition tests pass?** +- **Conventions adherence?** Per-chunk check against the relevant + conventions docs. +- **No scope creep?** Did the chunk stay within the agreed scope? + +Cumulative audit after all chunks: + +- **Feature-doc grounding** — does the implementation match what the + doc said? Document drift surfaces here. +- **Cross-cutting impacts honored?** — every entry in the doc's + *Likely cross-cutting impact* section either addressed or explicitly + deferred? +- **No silent ad-hoc downstream discipline?** — if any chunk went the + "apply discipline ad-hoc" path in Step 6, the extraction TODO is + flagged. + +### Step 8 — Update feature doc as living artifact + +The feature doc transitions from `coarse` → `sketched` (or `technical` → +`sketched`) as code lands. Required updates: + +- **Frontmatter `status`** — `implemented` once all phases land; + `partial` if any phase landed but others haven't. **Update + `definition` per the rule below.** +- **Status block** — reflect the implementation state, naming what + shipped and what remains. +- **Phases of complexity** — phase rows that are now implemented may + promote to *implemented*; partially-implemented rows note the partial + state. +- **What's not implemented** — shrink to reflect what's now done. +- **Implementation surface (required once any phase implementation + lands)** — populated with actual file paths, behavior names, state + slots, helpers. See `audio-playback.md` for the canonical shape. +- **Verification (required once any phase implementation lands)** — + **this is the persisted TDD artifact.** The Step 5 TDD plan lives + here in the doc, not just in chat. Each chunk's test gets a line: + test file path → test name → assertion summary. Add *Sandbox* + entries where demos exist; add *Out of scope / deferred* sub-list + for verification gaps (sandbox follow-ups, E2E coverage deferred + elsewhere). +- **Open questions** — resolved-through-implementation entries moved + to a *Resolved during Phase N implementation* sub-section (kept for + traceability); new open questions surfaced by implementation + experience added. +- **Related features** — if implementation revealed new cross-feature + dependencies, add cross-refs. + +**`definition` advancement rule.** Advance per the *highest* +implementation depth across all phases: + +- Any phase's *Implementation surface + Verification* sections + populated with concrete exports / file paths / test names → + `sketched`. +- Phases all still scope-and-constraints-only, no implementation → + leave at `technical`. +- Phases still broadly sketched, many open questions → leave at + `coarse`. + +A feature with Phase 1 implemented but other phases still broadly +sketched is `sketched` at the doc level — populated surface trumps +unimplemented phases (which surface in *Phases of complexity* as +not-yet-landed rows, not in the doc's overall depth). + +This is **not optional** — the doc-as-living-artifact discipline is +load-bearing for the registry staying current. Per the *Status update +skipped* failure-mode entry. + +### Step 9 — Commit (with user confirmation) + +After Step 7 audit is clean and Step 8 doc update lands: + +1. **Audit working-tree state.** `git status -s`. Surface any + pre-existing uncommitted work outside the implementation scope. +2. **Propose a commit structure.** Common shapes: + - **Per-chunk commits** — one commit per chunk + a final doc-update + commit. Highest atomicity; clearest review trail. + - **Per-phase commits** — chunks bundled by phase; one commit per + phase + doc update. Cleaner when chunks within a phase are tightly + coupled. + - **Single feature-implementation commit + doc-update commit** — + small features (audio-abr per its doc) may fit one commit. + - **All-in-one** — small enough that splitting adds no value. +3. **Ask the user to confirm via `AskUserQuestion`.** +4. **On confirmation, run the commits.** Use `feat(spf)` for behavior- + adding work; `refactor(spf)` for behavior-refactoring chunks; + `docs(spf)` for the feature-doc update; conventional-commit scopes + per the `git` skill. +5. **On decline or skip, stop.** The user owns the commit boundary. + +## Output format + +Propose Steps 1–5 outputs as a structured report before writing any code: + +1. **Feature identification** (Step 1's report — feature, scope target, + sources, template implementations) +2. **Ambiguities + open questions to resolve** (Step 2) +3. **Chunk decomposition** (Step 3 — chunk list with mechanism + downstream + skill routing) +4. **Cross-cutting concerns** (Step 4) +5. **TDD plan** (Step 5 — per-chunk test + implementation targets) + +After user confirmation, proceed to Step 6 per-chunk loop. Surface Steps +7–9 outputs after implementation. + +## Why this order + +Same shape as `/spf-document-feature` and `/spf-document-use-case`. Steps +1–2 force framing before mechanical work. Step 3 commits to a concrete +chunk list; Step 4 runs cross-cutting checks while context is fresh; Step +5 commits to TDD targets. Step 6 produces the artifact; Step 7 audits. +Step 8 keeps the registry current. Step 9 hands the commit boundary to +the user. + +The novel discipline compared to the document-* skills is the **chunk +decomposition + per-chunk downstream-skill routing** at Steps 3–6 — +implementation work is inherently more granular than documentation work, +and the chunk-level discipline is what keeps it from sprawling. + +## Why a discussion stage (not implicit) + +The open-questions-skipped failure mode is the canonical example: feature +docs at coarse depth have unresolved design questions that block +implementation. Resolving them silently via the edit loses the user's +design intent. Step 2's explicit conversational stage forces resolution +in the open, with the user making the call. + +## When this is the wrong skill + +- **You want to document a feature (not yet implemented)** → + `/spf-document-feature`. +- **You want to implement a use-case composition (not a single feature)** → + *(future)* `/spf-implement-use-case`. For now, this skill can be invoked + per-constituent-feature of a use case. +- **You want to refactor an existing behavior without feature scope** → + `/refactor-behavior`. +- **You want to split or merge behaviors** → `/refactor-behavior`'s + decomposition check. +- **You want to write an architectural design doc** → `design` skill. +- **You want to write an RFC** → `rfc` skill. + +## How the failure-mode catalog grows + +Same pattern as other SPF skills: when a new failure mode surfaces during +use (most likely during Step 6 per-chunk implementation or Step 7 audit): + +1. Add an entry to the *Failure-mode catalog* section above with the risk + pattern and a worked-example citation. +2. If the failure-mode is downstream-skill-shaped (a recurring need + surfaces in `spf-create-behavior` / `spf-update-behavior`), the + downstream skill's catalog grows too. +3. If the failure-mode is doc-shape-shaped (the feature doc template / + conventions don't capture something), `/spf-document-feature`'s + catalog or the conventions docs grow. + +This skill is **new**; the catalog is expected to grow significantly as +the first real implementations exercise it. The seeded entries capture +patterns identified at skill-creation time from cross-skill failure-mode +analysis — they're starting points, not endpoints. diff --git a/.claude/skills/spf-implement-use-case/SKILL.md b/.claude/skills/spf-implement-use-case/SKILL.md new file mode 100644 index 00000000..1a458aa2 --- /dev/null +++ b/.claude/skills/spf-implement-use-case/SKILL.md @@ -0,0 +1,753 @@ +--- +name: spf-implement-use-case +description: >- + Implement a use-case composition documented in the SPF use-case-composition + registry. Consumes a use-case doc at internal/design/spf/use-cases/.md + and produces the engine-side code: a variant engine factory, a parallel + adapter, composition wiring, use-case-specific behaviors (if any), and + tests. The implementation analog of /spf-document-use-case (which produces + the doc; this consumes it). Walks through disambiguation + routing (verify + the request is actually a use case, check constituent-feature implementation + status), resolves the doc's open questions with the user, maps phases to + chunks, routes to downstream skills (/spf-implement-feature for + unimplemented constituents, /spf-create-behavior, /spf-update-behavior, + /refactor-behavior), and updates both the use-case doc and constituent + feature docs as code lands. Treats the use-case doc as a starting point for + planning, not a hardened specification. Triggers: "implement use case", + "implement SPF use case", "implement use-case composition", "build use + case", "implement audio-only-mode-override", "implement ". +--- + +# Implement an SPF Use-Case Composition + +Take a use-case composition documented at +`internal/design/spf/use-cases/.md` and produce the engine-side code +that satisfies its Phase 1 (and optionally Phase 2 / Phase 3) scope — +typically a variant engine factory + parallel adapter, composition wiring +that subtracts/adds/swaps behaviors per the four-mechanism taxonomy, and any +use-case-specific behaviors that don't promote to standalone features. + +The canonical failure modes without this discipline are: + +- **Skipping disambiguation** — proceeding with an interpretation of "implement + X" when X could be a feature, a use case, or undocumented entirely. +- **Treating the use-case doc as a hardened spec** — refusing to revise the + doc when implementation reveals new questions or drift; *or* the inverse, + silently overriding the doc with implementation choices. +- **Assuming constituent features are implemented** — building a use case on + top of feature capabilities that don't actually exist in code yet. + +Steps 1–2 are the load-bearing setup. Step 1 (disambiguation + routing) is +the most novel discipline — Steps 2–9 mostly parallel `/spf-implement-feature` +with use-case-specific content. + +## Usage + +``` +/spf-implement-use-case [] +``` + +The arg is optional. The skill can be invoked with a use-case-doc name (e.g., +`audio-only-mode-override`) or a description of what the user wants. Step 1 +routes appropriately. + +## Reference docs + +Primary: + +- `internal/design/spf/use-cases/.md` — **starting point** for planning + (not a hardened spec; see *Doc-as-starting-point principle* below). Read + end-to-end before scoping. +- `internal/design/spf/use-cases/README.md` — doc-type spec; the four- + mechanism composition taxonomy + decomposition rubric + cross-link + discipline + Implementation note (Path-A vs Path-B for behavior + customization). +- `internal/design/spf/conventions/*.md` — when to reach for which primitive. +- `internal/design/spf/evaluation-axes.md` — axes the implementation is + scored against. +- `packages/spf/docs/hls-engine.md` — current HLS engine composition; the + baseline the variant subtracts from / adds to. +- `packages/spf/src/CLAUDE.md` — source layout + dependency rules. +- Constituent feature docs (the use case's *Constituent features* section + enumerates them). + +Secondary: + +- Existing engine factory + adapter as templates (`createSimpleHlsEngine` + and `SimpleHlsMediaElement` — the canonical pair the variant parallels). +- `internal/decisions/*.md` — past tactical decisions. + +Downstream skills routed-to: + +- `.claude/skills/spf-implement-feature/SKILL.md` — **new**: when a + constituent feature is not yet implemented and the use case needs it. +- `.claude/skills/spf-create-behavior/SKILL.md` — use-case-specific + behaviors that don't promote to features (per use-cases/README cross-link + discipline). +- `.claude/skills/spf-update-behavior/SKILL.md` — existing behaviors whose + purpose changes for the variant. +- `.claude/skills/refactor-behavior/SKILL.md` — existing behaviors with + preserved purpose, improved implementation. +- `.claude/skills/spf-document-use-case/SKILL.md` — invoked when Step 1 + routing concludes the candidate isn't yet documented as a use case. +- `.claude/skills/spf-document-feature/SKILL.md` — invoked when Step 1 + routing concludes the candidate is a feature, not a use case (and isn't + yet documented as such). +- *(future)* media-layer / network-layer skills. + +## Doc-as-starting-point principle + +The use-case doc is the **starting point for planning, not a hardened +specification**. This principle is load-bearing throughout the skill — +explicit because both directions of failure are real: + +- **Silent override** (covered by `Use-case-doc-grounding drift` failure + mode) — implementation diverges from the doc without surfacing the + divergence; the doc becomes stale silently. +- **Rigid following** (covered by `Treating use-case doc as hardened spec` + failure mode) — implementation refuses to revise the doc when new + questions surface or drift is discovered; the doc becomes a misleading + constraint. + +Acknowledge the doc's `definition` depth explicitly when planning: + +- **`coarse`** — variant shape sketched, many open questions. Implementation + will fill in significant detail; revisions to the doc are expected + throughout. Planning is substantial. +- **`technical`** — scope and constraints articulated; specifics still open. + Implementation maps to constraints; moderate planning + revision. +- **`sketched`** — implementation surface populated. Implementation + primarily verifies; revisions for drift only. + +Discipline: + +- **Every doc revision is explicit and surfaced** to the user during the + implementation pass. Never silent. +- **Step 8 consolidates** the cumulative doc update reflecting all + revisions made during implementation — it's not the *only* revision + point, just the cumulative one. +- **Open questions are markers**, not absent specs — the implementation + resolves them through experience or explicitly defers them. + +## Failure-mode catalog (seeded; grows with use) + +1. **Skipping disambiguation** — invoking with a name or description that + ambiguously maps to (i) an existing use case, (ii) a different use case, + (iii) a feature, (iv) something not yet documented. Step 1's + disambiguation must resolve before gathering sources or planning. + Proceeding-with-assumption is the canonical failure shape. Worked + examples: invocation "implement resolution capping" — routes to + `/spf-implement-feature` on `rendition-selection-caps` (cluster-E + policy), not here. Invocation "implement audio-only" maps to the + `audio-only-mode-override` use case (which absorbed what was + previously framed as a separate `audio-only-composition` feature); + the disambiguation between feature and use-case framings is + resolved by the consolidated doc. + +2. **Routing-out failure** — Step 1's disambiguation should route + confidently. The failure mode is "ambiguity discovered but not + resolved" — silently picking one interpretation when the user could + have meant another. Always surface and confirm. + +3. **Treating use-case doc as hardened spec** — refusing to revise the + doc when implementation reveals new questions, refines framing, or + surfaces drift. Inverse failure of silent-override. The right + discipline is explicit, user-surfaced revision. + +4. **Use-case-doc-grounding drift** (silent override direction) — + implementation diverges from the doc without surfacing the divergence. + Either the doc is wrong (revise it) or the implementation is wrong + (fix it). Don't silently diverge. + +5. **Constituent-feature implementation status unchecked** — assuming + feature X is implemented when it isn't; the use case ships against + assumed capabilities that don't exist. Step 1's gathering must + verify each constituent feature's implementation status (frontmatter + `status`, code-grounding via the feature doc's `Implementation + surface` section). + +6. **Shared-factory-with-Case-1-sibling miscoordination** — when a use + case has a Case-1 feature sibling and both want the engine variant + factory, building it twice or building it without coordinating + produces drift. **Resolved pattern (since 2026-05-21):** when both + cases ship the *same* engine factory, the Case-1 feature doc + consolidates into the use-case doc with a *Variant-decision signal + source* section covering both paths (`audio-only-mode-override` is + the canonical example). The skill must surface this when a Case-1 + sibling exists in the source material — recommend consolidation + when the factory is shared, separate docs only when the factories + differ. + +7. **Adapter shape proliferation un-flagged** — each use-case adapter + multiplies the surface. The use-cases/README cross-cutting note + flags this as a registry-level concern; implementations should + track it explicitly per pass. + +8. **Phase scope creep** — use-case docs have 3 phases (Basic / + Features-relevant / Optimizations). The implementation must scope + to one phase or a subset, not "implement the whole use case." + +9. **Composing variant-specific behaviors into default factory** — + variant-specific behaviors go in the variant factory, not the + default `createSimpleHlsEngine`. Same failure mode as + `/spf-implement-feature`'s catalog, but more pointed here: the + *whole point* of a use-case implementation is the variant assembly, + so misrouting at the factory level is the canonical first-pass bug. + +10. **Multi-writer slot mishandling** — adding a writer to a slot + another behavior writes requires multi-writer characterization per + `conventions/signals.md`. Same as `/spf-implement-feature`. + +11. **Conventions catalog under-application** — SPF conventions + (behaviors, signals, reactors, actors, config) all apply during + implementation. Failing to consult them produces code that "works" + but doesn't match patterns. + +12. **Use-case-specific behavior promoted to feature unnecessarily** — + a variant-decision-glue behavior, composition-wiring behavior, or + single-scenario tuning gets a feature doc when it should live in + the use-case doc's *Composition specifics → Behaviors added* + section. Apply the "earns its place" rubric per use-cases/README + cross-link discipline. + +13. **Test-after-the-fact implementation** — TDD discipline per chunk: + test → implement → verify. Implementation-first produces tests + that pass by construction. + +14. **Status update skipped — both doc-types** — use-case doc *and* + constituent feature docs need status updates as code lands. The + constituent feature docs' "Use cases that compose this feature" + entries may gain implementation-status notes (e.g., "Phase 1 + constituent — implementation in progress as part of + audio-only-mode-override"). Step 8 enforces. + +15. **Downstream skill missing — silent inline implementation** — when + a chunk hits a downstream-skill gap (especially: + `/spf-implement-feature` for an unimplemented constituent), the + failure mode is to silently apply discipline ad-hoc. Step 6 + explicitly surfaces this: branch on (i) defer chunk pending + downstream skill, (ii) implement the constituent feature first via + the downstream skill, (iii) bundle the constituent implementation + into this use-case pass with explicit user confirmation. + +16. **SPF adapter stranded at engine layer** — the implementation pass + lands the engine variant + SPF adapter and stops there. The variant + is reachable via `@videojs/spf/hls` but **not consumable through + the existing player surface** (the `packages/html` custom elements + + `packages/react` components + sandbox demos that real consumers + actually use). The canonical failure shape: ship a "Phase 1 + complete" SPF adapter that customers can't actually instantiate + via their normal `` / `` flow. Step + 2's *implementation-scope extensions* question makes the player- + package layers explicit opt-ins; the failure is forgetting to ask + or defaulting to "engine + adapter only" without surfacing the + gap. Step 8's *Out of scope / deferred* sub-list must record every + non-landed extension so the surface gap stays visible. Worked + example: `audio-only-mode-override` Phase 1 (2026-05-21) landed + only the SPF layer — the discovery of this failure mode was the + feedback that drove this entry. + +## Steps (do these in order; do not skip) + +### Step 1 — Identify the use case + disambiguate the request + +The load-bearing setup step. **Disambiguation comes first** — before +gathering sources or planning, confirm what the user actually wants. + +**1a. Identify the candidate.** + +- If the user passed a name → verify + `internal/design/spf/use-cases/.md` exists. +- If the user passed a description (no name) → parse for candidates, + match against existing use-case docs. +- If multiple candidates match → surface options to the user. + +**1b. Verify the candidate is actually a use case.** + +Apply the use-cases/README discriminator + 4-criterion rubric: + +- **Composition mechanisms?** If everything works as runtime config on + always-on behaviors (no behaviors subtracted/added/swapped/defaulted + at composition time) → it's a **cluster-E policy feature** → route + to `/spf-implement-feature` on the relevant feature doc. +- **Delivery scenario?** If the concern is source-shape correctness + (engine handles a kind of source) rather than delivery-mode choice + (compose differently for a consumer scenario) → it's a **Case-1 + feature** → route to `/spf-implement-feature` on the relevant + feature doc. +- **Constituent features?** If the candidate has no real composition + assembly — it's a single capability the engine gains — → it's + likely a **feature**, route to `/spf-implement-feature`. +- **Customer/consumer scenario?** If the request is "tune the engine + differently for X" without a delivery scenario → likely **cluster-E + policy** → `/spf-implement-feature`. + +**1c. Route the request appropriately.** + +- **Stays here** — confirmed use case, doc exists. Proceed to 1d. +- **No doc exists for the candidate** → route to `/spf-document-use-case` + to produce the doc; return here once doc lands. +- **It's actually a feature, no doc exists** → route to + `/spf-document-feature` first, then `/spf-implement-feature`. +- **It's actually a feature, doc exists** → route directly to + `/spf-implement-feature`. +- **Ambiguous between options** → surface to user; do not pick silently. + +**1d. Gather sources.** (Once routing is confirmed and we're staying here.) + +- The use-case doc itself — read end-to-end. Note `definition` depth + and `status` (see *Doc-as-starting-point principle* above). +- **Constituent features (load-bearing).** For each one listed in the + use-case doc's *Constituent features* section, **check + implementation status** by reading the feature doc's frontmatter + `status` and its `Implementation surface` section. Categorize each: + - **Implemented** — variant assembly can compose it as-is. + - **Partially implemented** — variant may need to compose what + exists and defer the rest. + - **Documented but unimplemented** — branches in Step 2 (defer use + case / implement constituent first / bundle into this pass). + - **Not documented** — route to `/spf-document-feature` first. +- Direct Case-1 sibling feature doc (if applicable) — check whether + shared engine factory work is in scope. +- Related use cases (the doc's *Related use cases* section) — note + shared constituent features for adapter-shape-proliferation + awareness. +- Conventions catalog skim for relevance signals. +- Existing engine factory + adapter as templates. +- Recent ADRs (`internal/decisions/`). + +**Stop and report back to the user** with: + +1. The use-case name and the doc's current `status` + `definition` + depth. +2. Sources consulted (with links). +3. **Constituent-feature implementation status** — categorized per + the four states above. +4. **Recommended scope for this implementation pass** — including + whether to defer due to unimplemented constituents, implement + constituents first, or bundle into this pass. +5. Open questions blocking implementation (going into Step 2's + discussion). + +### Step 2 — Discuss to resolve open questions + confirm scope strategy + +An **explicit conversational stage** — not optional, not implicit. +After Step 1's report: + +- **Walk through the use-case doc's Open questions section.** + Classify each: must-resolve-before-code vs can-stay-open. +- **Confirm phase scope.** Phase 1? Phase 1 + 2 subset? Specific + phase rows? +- **Confirm composition mechanism per chunk** — per the use-case doc's + *Composition specifics* breakdown: subtractive / additive / + alternative-impl / alternative-default-config. Often more than one + in combination. +- **Confirm constituent-feature readiness strategy.** Per Step 1's + categorization, decide for each unimplemented constituent: + - **Defer the use case** until constituent lands separately. + - **Implement the constituent first** via `/spf-implement-feature` + in a separate pass (this skill pauses, downstream skill runs, + this skill resumes). + - **Bundle constituent implementation** into this use-case pass + (the implementation work covers both the constituent feature + chunks and the variant assembly chunks; the doc updates cover + both docs). +- **Confirm shared-factory-with-Case-1-sibling coordination** if + applicable. Pattern: if the source material distinguishes a Case-1 + source-shape concern from the Case-2 delivery-mode concern but both + ship the *same* engine factory, recommend consolidating into one + use-case doc with a *Variant-decision signal source* section + covering both paths (`audio-only-mode-override` is the canonical + example — that consolidation landed 2026-05-21 absorbing what was + previously a separate `audio-only-composition` feature doc). +- **Confirm implementation-scope extensions.** The engine variant + + adapter pair in `packages/spf` is the *minimum* implementation + surface — but a variant adapter that stops at the SPF layer is + effectively unconsumable through the existing player surface. Ask + the user (multi-select `AskUserQuestion`) which downstream layers + to bundle into this pass: + - **Core media wrapper** — `packages/core/src/dom/media//` + (~5 LOC; applies the SPF mixin to `HTMLVideoElementHost`). The + minimum bridge between the SPF adapter and the player packages. + Worked example: `simple-hls/index.ts` → + `class SimpleHlsMedia extends SimpleHlsMediaMixin(HTMLVideoElementHost) {}`. + - **HTML custom element** — `packages/html/src/media/-video/` + (~5 LOC; wraps the core media in `CustomMediaElement` + + `MediaAttachMixin`) + `packages/html/src/define/media/-video.ts` + + `packages/html/src/cdn/media/-video.ts` for the CDN entry. + Worked example: `simple-hls-video/index.ts` → + `class SimpleHlsVideo extends MediaAttachMixin(CustomMediaElement('video', SimpleHlsMedia)) {}`. + - **React component** — `packages/react/src/media/-video/` + (~37 LOC; React adapter exposing props matching the HTML + surface). Pairs with the HTML custom element. + - **Sandbox demo(s)** — `apps/sandbox/templates/html--video/` + and/or `apps/sandbox/templates/react--video/` (~50–80 LOC + each). **Write to `templates/`, not `src/`.** Per the sandbox + README, `apps/sandbox/src/*` is gitignored — `pnpm dev:sandbox` + mirrors `templates/` into `src/` on startup, leaving local edits + in `src/` untouched. Useful for manual verification + developer + onboarding. Required prereqs: HTML or React component, depending + on which sandbox is included. + - **E2E tests** — `apps/e2e/apps/vite/src/pages/html--video-*.{html,ts}` + fixture pages + `apps/e2e/tests/...` Playwright spec. + **Lean: defer by default.** Use E2E for behaviors that have + documented reliability concerns ([[project-e2e-renderer-reliability]]) + or that exercise cross-browser invariants the unit tests + can't reach. For most Phase 1 use-case lands, the engine-level + integration tests + sandbox manual verification suffice; + E2E coverage follows once behavior stabilizes. + + Defaults: none. The user explicitly opts in to each layer per + pass. Each opt-in becomes its own chunk in Step 3. + +- **Surface expected doc revisions.** Walking through the Open + questions + phase scope often reveals "we'll need to update the + doc to reflect X" — flag these explicitly so they're not silent. +- **Resolve the open questions the implementation needs.** Per the + pre-deciding-things failure mode, only resolve what the + implementation forces; leave the rest as open questions in the + doc. + +**Use `AskUserQuestion`** for clear-cut choices (phase scope, +constituent-readiness strategy, composition mechanism per chunk, +implementation-scope extensions). + +### Step 3 — Map phases to implementation chunks + +Per the agreed scope, decompose into discrete chunks. Chunk shapes +typical for use-case implementations: + +**Core SPF layer (always present):** + +- **Engine variant factory creation** (new) — typically the first chunk; + parallels `createSimpleHlsEngine` shape with the composition mechanism + applied (subtract / add / swap / configure). +- **Adapter creation** (new) — parallels `SimpleHlsMediaElement` / + `SimpleHlsMediaMixin`; uses `shareSignals` unchanged. +- **Constituent feature implementation chunks** (if bundling per Step + 2) — route to `/spf-implement-feature`. +- **Use-case-specific behavior creation** (if any) — route to + `/spf-create-behavior`. +- **Existing behavior updates** (if any) — route to + `/spf-update-behavior` or `/refactor-behavior`. +- **Engine-level test scaffolding** — engine integration tests for + the variant; per-behavior tests for any new use-case-specific + behaviors. +- **Composition wiring** — ties the variant factory into the adapter + and exposes via `packages/spf/src/playback/engines/hls/index.ts`. + +**Implementation-scope-extension layers (opt-in per Step 2):** + +- **Core media wrapper** — `packages/core/src/dom/media//index.ts` + applying the SPF mixin to `HTMLVideoElementHost` (or audio host for + audio-only variants). Inline implementation; ~5 LOC. +- **HTML custom element + define entry + CDN entry** — + `packages/html/src/media/-video/index.ts`, + `packages/html/src/define/media/-video.ts`, + `packages/html/src/cdn/media/-video.ts`. Inline implementation; + ~5 LOC + boilerplate. +- **React component** — `packages/react/src/media/-video/index.tsx` + exposing the props surface; ~37 LOC. Inline implementation. +- **Sandbox demo(s)** — `apps/sandbox/templates/{html,react}--video/` + (each ~50–80 LOC). **Write to `templates/`, not `src/`** (which is + gitignored — see sandbox README). Inline implementation; one chunk + per surface (html / react / both). +- **E2E coverage** — fixture pages under + `apps/e2e/apps/vite/src/pages/` + Playwright spec under + `apps/e2e/tests/`. Inline implementation. Often the largest + opt-in chunk; defer by default per the Step 2 lean. + +**Output of this step.** A chunk list with mechanism + downstream-skill +routing per chunk + which layer it lands in. Same table shape as +`/spf-implement-feature`, with a *Layer* column added when opt-in +extensions are in scope. + +### Step 4 — Apply cross-cutting concern checks + +Run the failure-mode catalog and the use-case doc's *Likely cross-cutting +impact* section against the chunk list. Specific to use-case work: + +- **Shared-factory-with-Case-1-sibling coordination.** If a Case-1 sibling + exists, the engine variant factory work likely belongs to both — confirm + ownership and avoid double-implementation. +- **Adapter shape proliferation.** Flag per the use-cases/README cross- + cutting concern; track adapter-surface growth at the registry level. +- **Composition-mechanism mix verification.** Per the doc's *Composition + specifics*, ensure all four mechanism buckets are considered (even if + empty for this use case). +- **Constituent feature cluster patterns.** Cluster patterns apply + transitively through constituents — a constituent feature's cluster + patterns (gating, multi-writer, per-type, etc.) transfer to the use + case that composes it. +- **MSE invariants.** Specific Firefox `mozHasAudio` and similar + cross-type invariants flagged in `mse-mms-pipeline` — variant + implementations may exercise these in new ways (e.g., subtractive-audio + composition under Firefox). + +### Step 5 — TDD plan + +For each chunk, name: + +- The test — file path, test name, what it asserts. +- The implementation target — file path, factory/behavior/adapter name. +- The composition wiring change — if any. +- Acceptance criterion — what does "done" look like? + +For use-case implementations, integration tests at the engine level are +typically more load-bearing than for feature implementations — the +variant assembly is the primary product, not just the individual +behaviors. Plan for `engine.test.ts`-style coverage that exercises the +variant end-to-end against a representative source. + +**The TDD plan is the seed of the use-case doc's *Verification* +section.** Step 8 persists each chunk's test (file path + test name + +assertion summary) into the use-case doc — the TDD plan does not live +only in chat. Name tests with assertion summaries suitable for the doc +from the start, so Step 8 is a transcription pass rather than a +re-articulation. + +### Step 6 — Implement (test-first per chunk; route to downstream skills) + +Iterate per chunk: + +1. **Write the test first.** Run it failing. +2. **Branch by mechanism:** + - **Subtractive composition / wiring** — handle inline. + - **Config-driven** — handle inline. + - **Engine variant factory creation** — handle inline (typically; the + factory shape parallels `createSimpleHlsEngine`). + - **Adapter creation** — handle inline (typically; the adapter shape + parallels `SimpleHlsMediaElement` + `SimpleHlsMediaMixin`). + - **New use-case-specific behavior** → route to `/spf-create-behavior`. + - **Behavior update (purpose changing)** → route to + `/spf-update-behavior`. + - **Behavior refactor (purpose preserved)** → route to + `/refactor-behavior`. + - **Unimplemented constituent feature** → route to + `/spf-implement-feature` (per Step 2's readiness strategy). + - **Structural (split/merge)** → route via `/refactor-behavior`. + - **Media-layer / network-layer** — handle inline for now; future + skills will own these. +3. **Run the test passing.** +4. **Run composition tests** (`engine.test.ts` for the variant) to + verify no regression. +5. **Surface any doc revisions** discovered during the chunk — + propose to user, get confirmation, update doc. + +**Downstream skill missing — explicit handling.** Same as +`/spf-implement-feature`: defer / build downstream skill inline / +apply ad-hoc with extract-later flag. User makes the call. + +### Step 7 — Final-shape audit (per chunk + cumulative) + +Per chunk: test passes? Conventions adherence? No scope creep? + +Cumulative audit after all chunks: + +- **Use-case-doc grounding** — does the implementation match what the + doc said? Surface any final drift; resolve via explicit doc update + in Step 8. +- **Cross-cutting impacts honored?** +- **Constituent feature docs status reflects reality?** — if + constituent features were partially implemented as part of this + pass, their docs need updates too (Step 8). +- **Variant assembly verified end-to-end?** — integration test + exercises the variant against a representative source. + +### Step 8 — Update use-case doc + cascade to constituent feature docs + +Doc updates for **both** the use-case doc and its constituent feature +docs (cascade): + +**Use-case doc updates:** + +- Frontmatter `status` — `implemented` once all phases land; + `partial` if any phase landed but others haven't; `draft` only + if nothing has shipped. **Update `definition` per the rule below.** +- *Status* block — reflect implementation state, naming the + factory(ies) and adapter(s) that shipped and what remains. +- *Phases of complexity* — phase rows that landed get an + *(implemented)* marker; rows partially-implemented note partial + state with a pointer to which sub-row landed. +- *Composition specifics* — populated with actual factory/adapter + names, behavior subtraction/addition lists, configuration + changes (e.g., `initialState` seed dropped). **Subtract / add + lists must match the actual composed behavior list**, not the + pre-implementation prediction — fix any drift from + Step 1's report here. +- *Constituent features* — per-feature relationship notes get + concretized with actual file paths if relevant. +- *Customer-policy surface* — populated with actual adapter API. +- *Variant-decision signal source* — populated with actual + composition (typically: adapter-upfront, confirmed). +- *Open questions* — resolved entries moved to a new *Resolved + during Phase N implementation* sub-section (kept for + traceability); new entries surfaced by implementation added. +- **NEW section once any phase implementation lands: + *Implementation surface*** — required when implementation + surface is populated. Mirror feature-doc shape (see + `audio-playback.md` for the canonical example) and extend + per opt-in extensions landed in this pass: + - *Engine factory* table (Export / File / Purpose) — + always present. + - *Adapter* table (Export / File / Purpose) — always present. + - *Composed behaviors* paragraph — always present. + - *Core media wrapper* table — if the core wrapper layer + landed (`packages/core/...`). + - *HTML custom element* table — if the HTML element layer + landed (`packages/html/...`). + - *React component* table — if the React layer landed + (`packages/react/...`). + - Public re-export entry points for each layer that landed. + - Each opt-in extension that did *not* land in this pass + is noted in the *Out of scope / deferred* section so the + surface gap is visible. +- **NEW section once any phase implementation lands: + *Verification*** — required when implementation surface is + populated. **This is the persisted TDD artifact** — the Step 5 + TDD plan lives here in the doc, not just in chat. Mirror + feature-doc shape (see `audio-playback.md` for the canonical + example), with structure per opt-in extensions landed: + - *Unit tests* bullet list (one entry per test file → test + name → assertion summary) — engine + adapter coverage, + always present. + - *Component tests* sub-list — if HTML / React component + tests landed. + - *Sandbox* entry naming the sandbox app directory(ies) — if + sandbox demo(s) landed. + - *E2E tests* entry naming fixture pages + Playwright spec + paths — if E2E coverage landed. + - *Out of scope / deferred* sub-list for verification gaps + (sandbox follow-up, E2E coverage deferred elsewhere, etc.). + Each non-landed opt-in extension from Step 2 appears here + explicitly — the deferral is the artifact. +- *See also* — add test paths, sandbox demo paths if applicable. + +**`definition` advancement rule.** The depth scale per the +use-cases/README is `coarse → technical → sketched`. Advance per +the *highest* implementation depth across all phases: + +- Any phase's *Implementation surface + Verification* sections + populated with concrete exports/file paths/test names → + `sketched`. +- Phases all still scope-and-constraints-only, no implementation + → leave at `technical`. +- Phases still broadly sketched, many open questions → leave at + `coarse`. + +A use case with Phase 1 implemented but Phases 2 and 3 still +broadly sketched is `sketched` at the doc level — the populated +surface trumps the unimplemented phases (which surface in +*Phases of complexity* as not-yet-landed rows, not in the doc's +overall depth). + +**Constituent feature doc cascade:** + +- *Use cases that compose this feature* entries — update to reflect + implementation status. Worked example shape: `"audio-only-mode- + override (partial — Phase 1 landed)"` rather than the + pre-implementation `"audio-only-mode-override (coarse)"`. +- If a constituent feature was partially implemented as part of this + pass (per Step 2's bundling strategy), the feature doc gets its + own update too — same shape as if `/spf-implement-feature` had run. + +**Doc revisions are explicit.** Per the doc-as-starting-point +principle, every revision is proposed to the user before applying. + +### Step 9 — Commit (with user confirmation) + +After Step 7 audit is clean and Step 8 doc updates land: + +1. **Audit working-tree state.** `git status -s`. Surface any + pre-existing uncommitted work outside the implementation scope. +2. **Propose commit structure.** Common shapes for use-case + implementations: + - **Per-chunk commits + doc-update commit + cascade commit** — + highest atomicity; clearest review trail. + - **Variant-factory commit + adapter commit + doc-update commit + + cascade commit** — natural boundaries for a Phase 1 pass. + - **Bundled: feature-implementation commit + use-case- + implementation commit + cascade commit** — when this pass also + implemented constituent features per Step 2's bundling. + - **Single feature-implementation commit + doc-update commit** — + for small Phase 1 use cases. +3. **Ask the user to confirm via `AskUserQuestion`.** +4. **On confirmation, run the commits.** Use `feat(spf)` for + variant-factory + adapter creation; `refactor(spf)` for behavior + refactors; `docs(spf)` for the doc updates; conventional-commit + scopes per the `git` skill. +5. **On decline or skip, stop.** The user owns the commit boundary. + +## Output format + +Propose Steps 1–5 outputs as a structured report before writing any +code: + +1. **Use-case identification + disambiguation report** (Step 1 — use + case name, doc status/definition, sources, constituent-feature + readiness, routing decision) +2. **Ambiguities + open questions to resolve** (Step 2) +3. **Chunk decomposition** (Step 3 — chunk list with mechanism + + downstream skill routing) +4. **Cross-cutting concerns** (Step 4) +5. **TDD plan** (Step 5 — per-chunk test + implementation targets) + +After user confirmation, proceed to Step 6 per-chunk loop. Surface +Steps 7–9 outputs after implementation. + +## Why this order + +Step 1 (disambiguation + routing) is the novel discipline compared to +`/spf-implement-feature`. Implementation work on a use case can route +to a feature implementation, a doc creation, or stay here — getting +that routing right at the start prevents wrong-skill work. + +Step 2 (constituent-feature readiness strategy) is also novel — use +cases compose features, and the readiness state of constituent +features determines whether the use-case implementation can proceed +straightforwardly or needs to bundle constituent work. + +Steps 3–7 mostly parallel `/spf-implement-feature`'s chunk-decomposition ++ TDD + audit shape, with use-case-specific details. + +Step 8 (doc update + constituent cascade) is heavier than +`/spf-implement-feature`'s Step 8 because of the bidirectional +cross-link discipline: both the use-case doc and the constituent +feature docs need updates. + +## Why a discussion stage (not implicit) + +The open-questions + constituent-readiness + shared-factory-coordination +mix is the canonical decision space for use-case implementations. The +explicit conversational stage forces the right resolutions in the open, +with the user making the calls. Implicit decisions in this space produce +the worst failure modes (assuming constituent X is implemented when it +isn't; building a factory twice; silent doc drift). + +## When this is the wrong skill + +- **You want to implement a feature** → `/spf-implement-feature`. Use + cases compose features; if your invocation is really about a single + capability the engine gains, the feature implementation skill is the + right tool. +- **You want to document a use case (not yet documented)** → + `/spf-document-use-case`. Implementation requires a starting-point + doc. +- **You want to refactor an existing behavior without feature/use-case + scope** → `/refactor-behavior`. +- **You want to split or merge behaviors** → `/refactor-behavior`'s + decomposition check. +- **You want to write an architectural design doc** → `design` skill. +- **You want to write an RFC** → `rfc` skill. + +## How the failure-mode catalog grows + +Same pattern as other SPF skills: when a new failure mode surfaces during +use (most likely during Step 6 per-chunk implementation, Step 7 audit, +or Step 8 cascade), add an entry with a worked-example citation. + +This skill is **new**; the seeded entries capture patterns identified at +skill-creation time from cross-skill failure-mode analysis. Expect the +catalog to grow significantly as the first real use-case implementations +exercise it — particularly around the constituent-feature readiness +strategy and the shared-factory coordination cases. diff --git a/.claude/skills/spf-update-behavior/SKILL.md b/.claude/skills/spf-update-behavior/SKILL.md new file mode 100644 index 00000000..7613a1ea --- /dev/null +++ b/.claude/skills/spf-update-behavior/SKILL.md @@ -0,0 +1,190 @@ +--- +name: spf-update-behavior +description: >- + Update an existing SPF behavior whose purpose is changing or expanding. + Distinct from /refactor-behavior, which preserves purpose — this skill + handles cases where the behavior gains new responsibility (new state slot + to react to, new lifecycle phase, new constraint, new code path). Carries + /refactor-behavior's purpose-first discipline applied to the *purpose + change*. Triggers: "update behavior", "extend behavior", "modify behavior", + "change behavior purpose", "expand behavior responsibility". +--- + +# Update an SPF Behavior + +Modify an existing SPF behavior whose purpose is **changing or expanding**. +The canonical failure mode without this discipline is treating +purpose-changes as refactors — applying `/refactor-behavior`'s +preserve-purpose lens to a change that's actually adding responsibility. The +discipline distinction matters because: + +- **Refactor:** behavior X stays X, but improved (cleaner code, better + patterns, smaller surface). +- **Update:** behavior X gains new responsibility — reacts to a new slot, + owns a new lifecycle phase, applies a new constraint. The behavior's + *contract* changes. + +This skill is a **stub** scoped for use by `/spf-implement-feature`. Failure- +mode catalog grows from real use. + +## Usage + +``` +/spf-update-behavior +``` + +Typically invoked from `/spf-implement-feature`'s Step 6 when a feature +implementation requires extending an existing behavior. Can be invoked +directly when the user has identified the behavior to update. + +## Reference docs + +- The existing behavior file and its tests (required reading) +- `internal/design/spf/conventions/behaviors.md` — convention catalog the + update must continue to satisfy +- `internal/design/spf/conventions/signals.md` — multi-writer characterization + when adding writers to a slot another behavior writes +- `.claude/skills/refactor-behavior/SKILL.md` — the purpose-first discipline + shape this skill mirrors (applied to *purpose change* instead of + *preserved purpose*) +- The feature doc driving the update (if invoked from + `/spf-implement-feature`) — Step 1 grounds the update in the doc's phase + row or "What's not implemented" entry + +## Failure-mode catalog (seeded; grows with use) + +- **Purpose-change articulation skipped.** The most common failure mode: + the user invokes the skill saying "add bandwidth sampling to + setupAudioBufferActors" without naming what's actually changing about + the behavior's contract. Articulating the change forces clarity: + *"setupAudioBufferActors gains responsibility for bandwidth-sampling on + audio fetches, which it didn't have before — same composition position, + same lifecycle, but now writes `bandwidthState` from audio samples in + addition to creating buffer actors."* + +- **Slot map evolution without multi-writer characterization.** If the + update adds a writer to a slot another behavior already writes, the + multi-writer characterization from `conventions/signals.md` must be done + explicitly. Default-merge or silent-overwrite is a bug — typically + surfaces as race conditions or last-write-wins ordering bugs. + +- **Cleanup pattern preservation/migration mishandled.** Existing cleanup + contracts (what gets torn down, when, in what order) must be honored or + explicitly migrated. Adding a new resource without adding cleanup is the + canonical leak shape; reorganizing cleanup without preserving order is + the canonical lifecycle bug. + +- **Conflating with refactor-behavior territory.** If the purpose isn't + actually changing — the behavior's contract stays the same, just the + implementation improves — route to `/refactor-behavior`. The discipline + for purpose-preservation vs purpose-evolution differs; using the wrong + skill produces drift in either direction (refactor-as-update bloats the + behavior; update-as-refactor silently changes contracts). + +## Steps (do these in order) + +### Step 1 — Articulate the purpose change + +The load-bearing setup step. Before any code: + +- **What is the behavior's current purpose?** Read the existing behavior, + read `conventions/behaviors.md` for context. Articulate in plain + language. +- **What's changing?** New responsibility, new state slot to react to, new + lifecycle phase, new constraint? Name the specific change. +- **What's *not* changing?** Surface the parts of the contract that are + preserved — slot positions, composition placement, cleanup ordering, + observable interface. +- **Why is this an *update*, not a *refactor*?** If the answer is "the + behavior does the same thing, just differently," **stop and route to + `/refactor-behavior`**. + +**Stop and report to user** with the purpose-change articulation. The user +confirms before proceeding. + +### Step 2 — Identify slot map / interface changes + +- **New slots read?** Add to `stateKeys` / `contextKeys`. Per + `conventions/signals.md`, narrow is better. +- **New slots written?** Multi-writer characterization required. + Three-axis check: decision domain, trigger, cost. Document the + coordination strategy with the existing writer(s). +- **Slots removed?** If the update removes a read or write, verify no + downstream behavior depends on it. +- **Interface change?** If the behavior's external contract changes (e.g., + it now emits an event it didn't before), document the contract change + explicitly. + +### Step 3 — Apply conventions to the change + +- **Cleanup pattern.** Per project convention (named-cleanup-collection + + wrapper, not AbortController for SPF). If adding a new resource that + needs cleanup, slot into the existing cleanup pattern. +- **Per-type behavior?** If the update touches a per-type behavior, apply + per-type discipline (sibling behaviors + shared helper) per + `conventions/behaviors.md`. +- **Composition-variant logic.** If the new responsibility is variant- + specific (live-only, audio-only-only), the answer is *not* to add a + runtime conditional inside the always-on behavior. Either split the + behavior into per-variant siblings or compose a new behavior into the + variant factory. + +### Step 4 — Implement (TDD) + +1. **Update the test first.** Add an assertion for the new behavior, or + write a new test case for the new responsibility. +2. **Run the test failing.** +3. **Update the behavior** to satisfy the new test while preserving + existing tests' assertions. +4. **Run all tests passing** (the existing tests + the new one). +5. **Run composition tests** to verify no regression on the behavior's + downstream consumers. + +### Step 5 — Final-shape audit + commit + +Per parent skill (`/spf-implement-feature`), commits are typically batched +at the feature-implementation level. If invoked standalone, propose a +per-update commit shape. + +Audit checklist: +- **Purpose change reflected** — does the implementation match the Step 1 + articulation? +- **Multi-writer coordination clean** — if a new writer was added, is the + coordination documented and tested? +- **Cleanup preserved or migrated** — no leaks introduced; cleanup order + preserved or explicitly changed? +- **Existing tests still passing** — preserved-contract tests must still + hold +- **Conventions adherence** + +## When this is the wrong skill + +- **Behavior's purpose stays the same, just code improves** → `/refactor-behavior` +- **Creating a new behavior** → `/spf-create-behavior` +- **Major restructuring (split or merge)** → `/refactor-behavior` (which + may route to `/split-behavior` or `/merge-behaviors`) +- **Pure config-driven change with no behavior code change** → handle in + the feature implementation directly; no behavior-update needed + +## How the failure-mode catalog grows + +Same pattern as other SPF skills: when a new failure mode surfaces, add an +entry with a worked-example citation. This skill is a stub; the catalog +will likely expand significantly as the first real implementations exercise +it. + +## Open framing question + +The boundary between `/spf-update-behavior` and `/refactor-behavior`-with- +extension is genuinely open. Per `project_spf_implementation_skills_next` +memory: *"`spf-update-behavior` OR non-trivial updates to `spf-refactor- +behavior` — when an existing behavior needs a feature-implementation change +that isn't a pure refactor. Open which framing — extend refactor-behavior +or add a new skill."* + +This skill ships as a separate skill (rather than a refactor-behavior +extension) because the **purposes differ** — refactor preserves; update +changes. If usage reveals the discipline is mostly shared, the skills may +later merge. For now, the separation is intentional: route by +purpose-preserved vs purpose-changed, and let the failure-mode catalogs +diverge based on what each skill actually catches. diff --git a/apps/sandbox/app/constants.ts b/apps/sandbox/app/constants.ts index c5ff095d..4ce9d883 100644 --- a/apps/sandbox/app/constants.ts +++ b/apps/sandbox/app/constants.ts @@ -8,6 +8,7 @@ export const PRESETS = [ 'mux-video', 'mux-audio', 'simple-hls-video', + 'simple-hls-audio-only', 'dash-video', 'audio', 'background-video', diff --git a/apps/sandbox/app/shell/app.tsx b/apps/sandbox/app/shell/app.tsx index 5335902e..34f0826e 100644 --- a/apps/sandbox/app/shell/app.tsx +++ b/apps/sandbox/app/shell/app.tsx @@ -148,7 +148,7 @@ export function App() { onPreloadChange={setPreload} availableSources={availableSources} isBackgroundVideo={preset === 'background-video'} - isSimpleHlsVideo={preset === 'simple-hls-video'} + isSimpleHls={preset.startsWith('simple-hls-')} isMuxVideo={preset === 'mux-video'} isMuxAudio={preset === 'mux-audio'} platforms={PLATFORMS} diff --git a/apps/sandbox/app/shell/navbar.tsx b/apps/sandbox/app/shell/navbar.tsx index d540aa00..f8090f29 100644 --- a/apps/sandbox/app/shell/navbar.tsx +++ b/apps/sandbox/app/shell/navbar.tsx @@ -25,7 +25,7 @@ type NavbarProps = { onPreloadChange: (value: PreloadValue) => void; availableSources: readonly SourceId[]; isBackgroundVideo: boolean; - isSimpleHlsVideo: boolean; + isSimpleHls: boolean; isMuxVideo: boolean; isMuxAudio: boolean; platforms: readonly Platform[]; @@ -49,6 +49,7 @@ const PRESET_LABELS: Record = { 'mux-video': 'Mux Video', 'mux-audio': 'Mux Audio', 'simple-hls-video': 'Simple HLS Video', + 'simple-hls-audio-only': 'Simple HLS Audio-Only', 'dash-video': 'DASH Video', audio: 'Audio', 'background-video': 'Background Video', @@ -75,7 +76,7 @@ export function Navbar({ onPreloadChange, availableSources, isBackgroundVideo, - isSimpleHlsVideo, + isSimpleHls, isMuxVideo, isMuxAudio, platforms, @@ -131,7 +132,7 @@ export function Navbar({ onChange={onSourceChange} options={availableSources .filter((id) => { - if (isSimpleHlsVideo) return sources[id].subType === 'mp4'; + if (isSimpleHls) return sources[id].subType === 'mp4'; if (isMuxVideo || isMuxAudio) return sources[id].type !== 'dash'; return true; }) diff --git a/apps/sandbox/templates/cdn/main.ts b/apps/sandbox/templates/cdn/main.ts index e0d6a82b..dfe1056d 100644 --- a/apps/sandbox/templates/cdn/main.ts +++ b/apps/sandbox/templates/cdn/main.ts @@ -44,6 +44,7 @@ async function loadCdnPreset(preset: Preset, skin: Skin, live: boolean) { break; case 'audio': case 'mux-audio': + case 'simple-hls-audio-only': if (skin === 'minimal') await import('@videojs/html/cdn/audio-minimal'); else await import('@videojs/html/cdn/audio'); break; @@ -70,6 +71,9 @@ async function loadCdnMedia(preset: Preset) { case 'simple-hls-video': await import('@videojs/html/cdn/media/simple-hls-video'); break; + case 'simple-hls-audio-only': + await import('@videojs/html/cdn/media/simple-hls-audio-only'); + break; case 'dash-video': await import('@videojs/html/cdn/media/dash-video'); break; @@ -80,15 +84,19 @@ async function loadCdnMedia(preset: Preset) { // Rendering — produces the exact HTML markup the installation builder generates. // --------------------------------------------------------------------------- +function isAudioPreset(preset: Preset): boolean { + return preset === 'audio' || preset === 'mux-audio' || preset === 'simple-hls-audio-only'; +} + function getPlayerTag(preset: Preset, live: boolean): string { if (preset === 'background-video') return 'background-video-player'; - if (preset === 'audio' || preset === 'mux-audio') return live ? 'live-audio-player' : 'audio-player'; + if (isAudioPreset(preset)) return live ? 'live-audio-player' : 'audio-player'; return live ? 'live-video-player' : 'video-player'; } function getSkinTag(preset: Preset, skin: Skin, live: boolean): string { if (preset === 'background-video') return 'background-video-skin'; - if (preset === 'audio' || preset === 'mux-audio') return CSS_SKIN_TAGS[skin].audio; + if (isAudioPreset(preset)) return CSS_SKIN_TAGS[skin].audio; if (live) return LIVE_VIDEO_CSS_SKIN_TAGS[skin]; return CSS_SKIN_TAGS[skin].video; } @@ -100,6 +108,7 @@ function getMediaTag(preset: Preset): string { 'mux-audio': 'mux-audio', 'native-hls-video': 'native-hls-video', 'simple-hls-video': 'simple-hls-video', + 'simple-hls-audio-only': 'simple-hls-audio-only', 'dash-video': 'dash-video', audio: 'audio', 'background-video': 'background-video', @@ -109,7 +118,7 @@ function getMediaTag(preset: Preset): string { } function loadStylesheets(preset: Preset, skin: Skin) { - if (preset === 'audio' || preset === 'mux-audio') loadAudioStylesheets(skin); + if (isAudioPreset(preset)) loadAudioStylesheets(skin); else if (preset !== 'background-video') loadVideoStylesheets(skin); // Background CSS is loaded via dynamic import in loadCdnPreset. } @@ -169,7 +178,7 @@ async function render() { return; } - if (preset === 'audio' || preset === 'mux-audio') { + if (isAudioPreset(preset)) { root.innerHTML = html`
<${playerTag}> diff --git a/apps/sandbox/templates/html-simple-hls-audio-only/index.html b/apps/sandbox/templates/html-simple-hls-audio-only/index.html new file mode 100644 index 00000000..3b1dfbe3 --- /dev/null +++ b/apps/sandbox/templates/html-simple-hls-audio-only/index.html @@ -0,0 +1,14 @@ + + + + + + Sandbox — HTML Simple HLS Audio-Only + + + + +
+ + + diff --git a/apps/sandbox/templates/html-simple-hls-audio-only/main.ts b/apps/sandbox/templates/html-simple-hls-audio-only/main.ts new file mode 100644 index 00000000..5634989c --- /dev/null +++ b/apps/sandbox/templates/html-simple-hls-audio-only/main.ts @@ -0,0 +1,68 @@ +import '@app/styles.css'; +import '@videojs/html/audio/player'; +import '@videojs/html/media/simple-hls-audio-only'; +import { createHtmlSandboxState, createLatestLoader, renderMediaAttrs } from '@app/shared/html/sandbox-state'; +import { loadAudioSkinTag } from '@app/shared/html/skins'; +import { + onAutoplayChange, + onLoopChange, + onMutedChange, + onPreloadChange, + onSkinChange, + onSourceChange, +} from '@app/shared/sandbox-listener'; +import { SOURCES } from '@app/shared/sources'; + +const html = String.raw; + +const state = createHtmlSandboxState(); +const loadLatest = createLatestLoader(); + +async function render() { + const tag = await loadLatest(() => loadAudioSkinTag(state.skin, state.styling)); + if (!tag) return; + + const mediaAttrs = renderMediaAttrs(state); + + document.getElementById('root')!.innerHTML = html` +
+ + <${tag}> + + + +
+ `; +} + +render(); + +onSkinChange((skin) => { + state.skin = skin; + render(); +}); + +onSourceChange((source) => { + state.source = source; + render(); +}); + +onAutoplayChange((autoplay) => { + state.autoplay = autoplay; + render(); +}); + +onMutedChange((muted) => { + state.muted = muted; + render(); +}); + +onLoopChange((loop) => { + state.loop = loop; + render(); +}); + +onPreloadChange((preload) => { + state.preload = preload; + render(); +}); diff --git a/apps/sandbox/templates/react-simple-hls-audio-only/index.html b/apps/sandbox/templates/react-simple-hls-audio-only/index.html new file mode 100644 index 00000000..f703407f --- /dev/null +++ b/apps/sandbox/templates/react-simple-hls-audio-only/index.html @@ -0,0 +1,14 @@ + + + + + + Sandbox — React Simple HLS Audio-Only + + + + +
+ + + diff --git a/apps/sandbox/templates/react-simple-hls-audio-only/main.tsx b/apps/sandbox/templates/react-simple-hls-audio-only/main.tsx new file mode 100644 index 00000000..09042926 --- /dev/null +++ b/apps/sandbox/templates/react-simple-hls-audio-only/main.tsx @@ -0,0 +1,45 @@ +import '@app/styles.css'; +import { AudioProvider } from '@app/shared/react/providers'; +import { AudioSkinComponent } from '@app/shared/react/skins'; +import { useAutoplay } from '@app/shared/react/use-autoplay'; +import { useLoop } from '@app/shared/react/use-loop'; +import { useMuted } from '@app/shared/react/use-muted'; +import { usePreload } from '@app/shared/react/use-preload'; +import { useSkin } from '@app/shared/react/use-skin'; +import { useSource } from '@app/shared/react/use-source'; +import { SOURCES } from '@app/shared/sources'; +import type { Styling } from '@app/types'; +import { SimpleHlsAudioOnly } from '@videojs/react/media/simple-hls-audio-only'; +import { useMemo } from 'react'; +import { createRoot } from 'react-dom/client'; + +function readStyling(): Styling { + return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css'; +} + +function App() { + const skin = useSkin(); + const source = useSource(); + const styling = useMemo(readStyling, []); + const autoplay = useAutoplay(); + const muted = useMuted(); + const loop = useLoop(); + const preload = usePreload(); + + return ( + + + + + + ); +} + +createRoot(document.getElementById('root')!).render(); diff --git a/internal/design/spf/features/audio-abr.md b/internal/design/spf/features/audio-abr.md index 2349d48d..669f33e9 100644 --- a/internal/design/spf/features/audio-abr.md +++ b/internal/design/spf/features/audio-abr.md @@ -262,6 +262,13 @@ Things this feature probably forces decisions on, not just additions: Audio-ABR is the second consumer; promoting `BandwidthState` to its own feature doc may become worthwhile. +## Use cases that compose this feature + +- **[`audio-only-mode-override`](../use-cases/audio-only-mode-override.md)** + *(coarse)* — Phase 2 constituent. When audio-abr is + implemented, the audio-only delivery variant composes it for + multi-bitrate audio support. Used as-is. + ## See also - [video-abr.md](./video-abr.md) — structural template; the diff --git a/internal/design/spf/features/audio-only-composition.md b/internal/design/spf/features/audio-only-composition.md deleted file mode 100644 index 736c3975..00000000 --- a/internal/design/spf/features/audio-only-composition.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -status: draft -date: 2026-05-20 -definition: coarse ---- - -# Audio-only composition - -Engine support for HLS sources that contain only audio renditions -(no video tracks declared in the manifest). The Case-1 Media-src -feature per [clusters.md § Feature classification axes — Composition -cases per mode](./clusters.md#composition-vs-policy-vs-middle-pattern): -"Default composition handles a manifest that is genuinely audio-only -(no other tracks present). Required to claim general permutation -support." Sister to [video-only-composition](./video-only-composition.md) -(parallel sibling on the inverse axis). - -A **Media-src feature** per -[clusters.md § Feature classification axes](./clusters.md#media-src-vs-player-vs-borderline): -without it, audio-only HLS sources don't play correctly. (Today the -engine *tolerates* audio-only sources per the `engine.test.ts` -"handles audio-only stream" test, but isn't *optimized* for them — -work goes here.) - -## Status - -- **Composition:** partially supported. The HLS engine - (`createSimpleHlsEngine`) tolerates audio-only sources today; the - test suite includes `engine.test.ts` → "handles audio-only stream - (no video tracks)" exercising basic playback. Tolerance derives - from `setupVideoBufferActors` and `loadVideoSegments` no-op-ing - when `presentation.videoTracks` is empty (rather than asserting). -- **Definition depth:** coarse — scope sketched at the engine- - variant level. Implementation specifics (engine-variant composition - shape, audio-only-optimized buffer targets, etc.) tracked as open - questions. -- **Source material:** Notion epic #4a (Basic Audio-only, Cluster - C/E, Media-src composition case 1, S/S sizing). - -## Phases of complexity - -Three brief phases covering the Case-1 scope: recognition, engine -variant, and edge cases. - -| Phase | What | Notes | -|---|---|---| -| Audio-only manifest recognition | Parser surfaces `presentation.videoTracks` as empty when the multivariant playlist contains only `EXT-X-MEDIA:TYPE=AUDIO` renditions (no video). Engine state observably reflects "no video tracks." Tolerated today; the parser already produces empty `videoTracks` for these sources | Already works today per the engine test. Foundation for the rest of this feature's scope | -| Audio-only engine variant | Explicit engine composition variant where video-side behaviors (`setupVideoBufferActors`, `loadVideoSegments`, `switchVideoQuality`, `selectVideoTrack`) are subtractively-composed-out rather than running as no-ops. Saves the no-op overhead and makes the "no video" state explicit in the composition. The current implicit tolerance becomes explicit | Composition-variant work. Per the failure-mode catalog: live vs VoD is a composition-time distinction, and the same principle extends here — audio-only vs A+V is composition-time. Two shapes: (a) audio-only variant composes a subset of behaviors (cleanest); (b) keep uniform composition with video-side no-ops (current state). Lean: (a) — explicit composition matches the SPF discipline | -| Audio-only-optimized buffer / playback | Audio-only sources may benefit from different default tuning: shorter forward-buffer targets (audio has lower bandwidth; less ahead-buffering needed), no display-related work (no `requestAnimationFrame`, no PiP, no thermal pressure from decode), simpler `endOfStream` (single SourceBuffer to coordinate) | Tier 2-ish: optimization beyond minimum viability. Defer until usage signals from podcast / audio-only customers actually exist | - -## What's in scope vs out of scope - -**In scope:** -- All three phases above for HLS audio-only-manifest sources -- Engine-variant composition shape (subtractive composition of - video-side behaviors) -- Audio-only-specific buffer / playback optimizations -- Confirming + extending the existing audio-only test coverage in - `engine.test.ts` - -**Out of scope (separate concerns — the "use case composition" -doc-type):** -- **Audio-only mode override** *(Player feature, "use case - composition" type — not yet formalized)* — subtract-down - composition that produces audio-only delivery *even from mixed- - manifest sources* (sources with both audio and video). This is - the Case-2 Player feature per Notion's "Composition cases per - mode" framing. Different concern: this feature handles audio- - only-as-source-shape; the override case is audio-only-as- - delivery-choice. Falls under the yet-to-be-formalized "use case - composition" doc-type (parallel concepts include background-video - playback, audio-podcast mode, etc.). -- **Dynamic audio-only switching** *(Case 3, deprioritized per - Notion)* — same engine, config/state-driven dynamic switching - between Case 1 and Case 2. Notion epic #4c: "May not build." - -**Out of scope (different architectural layer):** -- Adapter-level audio-only UI (cover art rendering, audio-podcast - player chrome, etc.). Adapter / consumer territory. -- Audio-only customer-facing modes ("Listen on the go" toggles). - Adapter-level. - -## Likely cross-cutting impact - -Things this feature probably forces decisions on, not just additions: - -- **Engine composition shape for variants.** Composition-variant - pattern from the failure-mode catalog: live vs VoD is the - precedent. Audio-only-vs-A+V should follow the same shape — - variant-specific composition rather than runtime no-ops. The - current implicit-tolerance state is a transitional shape; this - feature makes it explicit. -- **Behavior composition subtraction.** Today's `createSimpleHlsEngine` - composes a fixed list. Subtracting video-side behaviors for the - audio-only variant means a different composition list — closer to - `createAudioOnlyHlsEngine` (or similar) at the engine-factory - level. Cross-cluster with [engine-adapter-integration](./engine-adapter-integration.md) - on how engine variants are selected. -- **Variant-decision signal source.** Same question as - [live-stream-support](./live-stream-support.md)'s variant-decision - open question: adapter-upfront opt-in vs detect-from-parser - (engine sees `presentation.videoTracks === []` and routes to - audio-only composition). Detect-and-route is more adaptive; - adapter-upfront is simpler. Cross-feature with how live + DVR + - LL-HLS variants get composed. -- **Audio-only `endOfStream` gate.** Today's `endOfStream` gate in - [mse-mms-pipeline](./mse-mms-pipeline.md) coordinates across - video and audio buffers (`isLastSegmentAppended` per type + - `mediaSource.readyState`). For audio-only, the gate naturally - simplifies (single buffer to coordinate); per the catalog's - composition-variant entry, the existing `endOfStream` behavior - should compose unchanged — it reads `mediaSource.sourceBuffers` - uniformly rather than per-type. Verify this empirically when the - feature lands. -- **Audio-only ABR semantics.** Audio renditions can be multi- - bitrate (multiple AAC bitrate variants). When [audio-abr](./audio-abr.md) - lands, audio-only composition + audio-abr is a natural pairing. - The audio-only variant composes audio-abr in place of (or in - addition to) `selectAudioTrack`. -- **DOM exposure semantics.** `