Files
v10/internal/design/spf/evaluation-axes.md
T

12 KiB

status, date
status date
draft 2026-05-07

SPF evaluation axes

Reference frame for evaluating SPF code — both cleanups of existing code and new feature work added to behaviors, composition, and the surrounding media/network layers. The axes name the dimensions a change is scored on, the decision criteria for each, and the tensions that show up when an axis is pushed in isolation.

Purpose

Establish a stable, reusable lens so that:

  • Cleanup pushes are evaluated against the same criteria across iterations and contributors (human or agent).
  • Feature additions are scored on the same axes a cleanup would be — preventing regressions that "ship a feature" but degrade the codebase against a stated axis.
  • Tradeoffs are made explicitly. Each axis has natural tensions with the others; pushing one without acknowledging tension on another is the normal failure mode.

This doc is the axes. Companion docs (conventions/) define when to reach for which SPF primitive and the patterns those choices entail. Implementation-level "how it works" lives in architecture.md, primitives.md, signals.md, and packages/spf/docs/hls-engine.md — so a Reactor implementation refactor doesn't force editing the "when to use a Reactor" guidance.

Scope

Applies to packages/spf/src/**. The axes are framed broadly enough to apply to VJSv10 outside SPF (especially E — Size, which is cross-cutting), but examples and tensions in this doc are SPF-specific.

The first concrete user is the cleanup pass tracked in .claude/plans/spf/behaviors-and-media-cleanup.md. Future feature work (DRM, audio-only, instant clips, multi-language audio, LL-HLS, codec capability checks, multi-CDN, …) should evaluate the same way.

The five axes

A — Reusability under feature pressure

Decision criterion: can this code accommodate any feature on the upcoming-features pressure list (see Pressure list) without architectural surgery?

"Reusable" is not "supports any imagined consumer" — that produces gold-plating. Reusable means doesn't bake in things we already know we'll need to vary. The pressure list is the concrete forcing function.

Healthy patterns:

  • Configuration-driven branching for things that vary per asset/session (DRM, codec, audio-only).
  • Behaviors that operate on signal contracts, not specific concrete sources.
  • Pre-append wait conditions exposed as composable hooks (DRM key handshake) rather than baked-in if checks.

Anti-patterns:

  • Hardcoded video+audio source buffer creation when audio-only is on the pressure list.
  • Implicit "HLS parser is always present" assumption baked into behavior bodies.
  • Pre-flight checks that can't be extended (e.g., a startup gate that doesn't compose with future DRM gates).

B — Robustness

Decision criterion: does this code make implicit ordering, lifecycle, or state assumptions that break under realistic concurrency, error, or teardown patterns?

The MS/SB lifecycle (MediaSource readiness, SourceBuffer updating state, append/remove sequencing) is the canonical place this axis surfaces in SPF. Brittleness here is rarely about "missing error handling" — it's about implicit protocol assumptions between actors.

Healthy patterns:

  • Sequencing made explicit through actor message ordering or signal-derived gates, not timing.
  • Teardown that runs under AbortController so partial state never leaks.
  • Failure modes that surface as state, not silent dead-ends.

Anti-patterns:

  • "It works because the previous step always finishes first."
  • Cleanup paths that assume happy-path ordering.
  • Locks/flags that protect a sequence the type system could have enforced.

C — Patternability

Decision criterion: does this code follow an established SPF pattern? If not, should one exist — and if so, the cleanup produces both the convention and the code that follows it.

Patternability is what makes code subjectively easier to reason about: a reader who has paid the learning cost for the pattern once can scan any conforming code at low cost. This is the axis where the conventions docs are produced — by mining for what is and isn't pattern-shaped.

Healthy patterns:

  • Behavior helpers shaped like behaviors (so wiring looks the same).
  • Read/write intent annotated uniformly via Signal<T> / ReadonlySignal<T> typing.
  • Repeated problems solved the same way at every layer (initial selection, error mapping, teardown).

Anti-patterns:

  • Three different ways to extract a helper from a behavior (factory, free function, closed-over signal access).
  • Bespoke flag-based state machines (abrDisabled) when the codebase has primitives for the same job.
  • One-off naming/shape conventions that don't match any nearby code.

D — Simplicity

Decision criterion: is this code more elaborate than the problem requires? Can existing primitives (Reactor, Task + Runner, Signal, Actor, shareSignals) replace ad-hoc machinery?

Simplicity is about objective complexity reduction — fewer moving parts for the same behavior. This is distinct from C (which is about conformance) and from E (which is about size in compiled output): code can be small but elaborate, or large but trivial.

Healthy patterns:

  • Replacing ad-hoc subscribe/cleanup with Reactor when the shape fits.
  • Collapsing "two flags and a callback" into a single signal-derived state.
  • Using defineBehavior consistently rather than hand-rolled behavior objects.

Anti-patterns:

  • Four-step state machines for what is really a derived signal.
  • Hand-rolled task scheduling alongside Tasks + Runners.
  • Coordination protocols between behaviors that should be one behavior.

E — Size

Decision criterion: does this code minimize compiled output size? Cross-cutting (applies broadly to VJSv10) and uniquely measurable among the axes — pnpm -F @videojs/spf size (public API) and size:all (all exports) provide direct numbers.

Size is its own axis because smaller and simpler are correlated but not identical. Terse code via higher-order abstractions, FP composition, or metaprogramming can shrink output while increasing line-by-line density. That tradeoff is acceptable when (a) the abstraction is a documented convention and (b) comments cover the non-obvious why. Size is also load-bearing for SPF specifically — it is shipped to browsers and the public API surface has measured size budgets.

Healthy patterns:

  • Shared helpers with narrow surfaces, exported once, reused broadly.
  • Conditional features behind tree-shakable subpath imports rather than runtime branches.
  • Small declarative configs over imperative wiring repeated per call site.

Anti-patterns:

  • Per-behavior copies of the same wiring scaffold.
  • Defensive runtime guards for invariants the type system already enforces.
  • Wide enums/discriminants that prevent dead-code elimination.

Tensions

Pushing one axis tends to apply pressure to another. These are the recurring ones:

Tension What it looks like
A vs D Pushing assumptions to config can inflate code with branches/options. The right call is per-case: if the assumption is on the pressure list, A wins; if speculative, D wins.
A vs E More configurability = more code paths in the bundle. Mitigate via subpath splitting (/dom, /playback-engine) so each consumer pays only for what they import.
A vs C A new feature may need a new convention. Stretching an existing pattern beyond fit is worse than introducing a new one — but introducing one without writing it down is worse still.
B vs D Defensive code reads as bigger and more elaborate. The real fix is usually to repair the protocol so the guard becomes unnecessary, not to add the guard.
B vs E Same as above, in compiled output terms.
C vs D Conventions can mandate elaborate-feeling shapes (always wrap in X). Sometimes inline is simpler in isolation but harder to scan in aggregate — the convention earns its keep across many call sites, not at any one.
C vs E A convention can either be the size win (shared helpers) or cost size (mandatory wrappers). Conventions doc should note which when it matters.
D vs E Terse-via-HOF/metaprogramming can shrink size while reading denser. Acceptable when the abstraction is a convention with a documented when-to-use.

Pressure list (axis A target)

The concrete forcing function for A — Reusability. From the Mux Video Content Permutations doc, filtered to items in or imminent for SPF (MPEG-TS explicitly out of near-term scope):

  • DRM — Widevine, PlayReady, FairPlay (incl. legacy com.apple.fps.1_0 fallback and AirPlay path); pre-append key handshake gating.
  • DRM security level configurations — per-asset/session policy enforcement.
  • Audio-only streams — no video source buffer; UI/feature suppression.
  • Multi-language audioAudioTrackList wiring, BCP-47-aware default selection.
  • Multi-bitrate audio (audio ABR) — quality selection across audio renditions, not just video.
  • Instant clips / non-zero PTS — timeline normalization equivalent to hls.js initPTS / timestampOffset.
  • LL-HLS — blocking reload, partial segments, delta playlists, preload hints.
  • Live / DVR sliding window — manifest re-polling, sliding/growing playlist handling.
  • Multi-CDN failover — alternate-URI rotation on persistent rendition failure.
  • Codec capability detection — HEVC, 5.1 surround, etc.; isTypeSupported-based filtering.
  • Mux platform behaviors — pseudo-ended detection, edit-list compensation, VRLT pacing audit, 1080p+ cap, stream termination (#EXT-X-ENDLIST), playback token expiry, buffer stall recovery.

When evaluating axis A on a piece of code, the question is "which of these would force me to rewrite this?" — not "could I imagine any future change?"

Mapping back to the original framing

The five axes consolidate the original six goals while preserving their distinctions:

Original goal Lands on axis
1. Cleanliness / complexity reduction (with subjective ease of reasoning) C (subjective half) + D (objective half)
2. Implementation detail changes (abrDisabled, smarter initial track selection) Backlog items, scored on multiple axes — not an axis itself
3. Brittleness B
4. Reduce assumptions A
5. Code consistency (helpers conform to behavior shape, establish patterns) C
6. Code size E

Goal 2 is the meaningful exception: it's a category of work, not a cleanup principle. abrDisabled rework scores on C + D + A; smarter initial track selection is feature-leaning and tagged as such on the backlog so it isn't conflated with axes-driven cleanup.

  • internal/design/spf/conventions/ (to be drafted) — when to reach for which SPF primitive, paired patterns. Cross-references this doc for axes; cross-references architecture.md / primitives.md for implementation.
  • internal/design/spf/architecture.md — current implementation snapshot.
  • internal/design/spf/primitives.md — Tasks, Actors, Reactors, State.
  • internal/design/spf/signals.md — signals decision and tradeoffs.
  • packages/spf/docs/hls-engine.md — current HLS engine composition walkthrough.
  • .claude/plans/spf/behaviors-and-media-cleanup.md — the cleanup pass driving the first concrete use of these axes.
  • .claude/plans/spf/behaviors-and-media-assessment.md (to be drafted) — current-state mapping of in-scope code against these axes.