feat(fled): machine-enforce the video.color source-color contract - #508
Open
zackees wants to merge 2 commits into
Open
feat(fled): machine-enforce the video.color source-color contract#508zackees wants to merge 2 commits into
zackees wants to merge 2 commits into
Conversation
`video.color` was written by the recorder and described in the spec, but
nothing validated it on either side. A producer could emit a declaration the
spec forbids and a consumer could reinterpret one, with no way to notice: a
wrong color declaration is invisible at runtime and outlives the tool that
wrote it.
Turn the prose into enforced rules.
- docs/fled-format.md: new "Source color metadata" section - the four
independent fields, the {bt709, srgb, rgb, full} default tuple, per-format
color classes, seven validation rules, and the forward-compatibility rule
that keeps video.color advisory for rgb8 while mandatory semantics gate on a
new pixel_format. Names the implementation and both test suites, so a rule
stated without tests is a documented bug.
- render/fled-color.ts: validateFledColor / readVideoColor / buildVideoColor,
throwing FledColorError with a code and field. Rejects linear/pq/hlg on
display-encoded formats, limited range, non-rgb matrices, partial
declarations on formats with no default tuple, and malformed custom
primaries. An absent declaration is never an error - every pre-color
recording keeps playing.
- New pixel_format 0x05 rgb16_linear (6 B/LED), which requires
transfer: linear. Not a version bump; unknown values already reject.
- recording.ts: embedFps derives its color block from buildVideoColor(format)
instead of a literal, so the declaration cannot drift from the payload.
- inspect-fled.mjs reports the resolved tuple and exits 11 on an invalid
declaration.
- 32 tests covering every rule, plus a parity guard on the duplicated
src/ and packages/gfx/src/ copies, which nothing enforced before.
The FastLED consumer mirrors these rules in src/fl/fled/color.h with
tests/fl/fled/fled_color.cpp.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review of the previous commit found the spec and the implementations disagreeing in two places, and the diagnostic tool missing rules it advertised enforcing. - The color-classes table said gray8/rgbw8 may declare all four keys "or be absent", while all three implementations reject an absent declaration there. The strict reading is the correct one - an rgbw8 payload has no interpretable color without a declaration - so the document was wrong. It now also says that outcome is *unresolvable*, not *malformed*, since callers report it distinctly and may not care. - Spec now separates a declaration verdict from a consumer's file policy. The old text promised video.color was "advisory" while rule 1 made an unknown value a hard rejection, so a reader on today's rules would refuse a file that a pre-color reader plays fine - exactly what "advisory" exists to prevent. Producers and tooling stay strict; playback consumers may fall back to the default tuple on advisory formats, and must refuse on mandatory ones. - inspect-fled.mjs skipped primaries validation for non-string scalars and never implemented rule 7 at all, so it exited 0 on files the reference validator rejects - the tool that exists to catch this was missing it. - embedFps no longer lets a color failure swallow video.fps along with it. - api-report.json records all seven new exports, not three; FledChromaticity is nameable from the entry points; DEFAULT_COLOR_TUPLE is frozen. - Reserved YCbCr matrix names now read as "reserved" while a typo reads as "unrecognized", and an explicit JSON null is treated as absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes the
.fledcontainer'svideo.colorsource-color metadata machine-enforced. The field was already written by the recorder and described in prose, but nothing validated it on either side — a producer could emit a declaration the spec forbids and a consumer could reinterpret one, with no way to notice.That asymmetry matters more than a normal missing validation: a wrong color declaration is invisible at runtime (nothing crashes, the pixels are just quietly wrong) and the file outlives the tool that wrote it.
Changes
docs/fled-format.md— new "Source color metadata" section: the four independent fields, the{bt709, srgb, rgb, full}default tuple, per-format color classes, seven validation rules, and the forward-compatibility rule. It names the implementation and both test suites, so a rule stated without tests is a documented bug.render/fled-color.ts(both source trees) —validateFledColor/readVideoColor/buildVideoColor, throwingFledColorErrorwith acodeand, where field-scoped, afield. Rejectslinear/pq/hlgtransfers on display-encoded formats,limitedrange, non-rgbmatrices, partial declarations on formats with no default tuple, and malformed custom primaries.pixel_format0x05rgb16_linear(6 B/LED), which requirestransfer: "linear". Adding an enum value is not a version bump — unknown values already have defined rejection behavior.recording.ts—embedFpsderives its color block frombuildVideoColor(pixelFormat)instead of a literal, so the declaration cannot drift from the payload it describes.scripts/inspect-fled.mjs— reports the resolved tuple and exits11on an invalid declaration.src/andpackages/gfx/src/copies, which nothing in the repo enforced before.The invariant that shaped the design
An absent declaration is never an error. Every recording predating this field — including every
public/demo/*.fled, written with novideoblock at all — must keep playing, so a format with a default tuple resolves to it. Only a declaration that is present and invalid is rejected. Verified against a real demo file:Validation
npm testnpm run typechecknpm run lintcheck:gfx-policy/test:gfx:package/test:gfx:parity/test:gfx:fastled-adaptercheck-gfx-api.mjsinspect-fled.mjsend-to-endtransfer: linearon rgb8 → exit 11;range: limited→ exit 11Known follow-up (deliberately out of scope)
isSupportedFormat()is exported but called by nothing, so known-but-unrenderable pixel formats (gray8,rgba8,rgbw8,rgb565le— and nowrgb16_linear) parse through instead of being rejected cleanly at the player boundary. This PR widens that pre-existing hole by one format rather than creating it, and norgb16_linearfile exists anywhere yet. Fixing it properly means deciding rejection policy for four formats that predate this work and touching the Movie Player / demo UI paths, which is a player-behavior change, not a color-spec change. Filed as a follow-up rather than smuggled in here.Cross-repo
The FastLED consumer implements the same rules — companion PR: FastLED/FastLED#4045. The canonical spec in this PR cites
src/fl/fled/color.handtests/fl/fled/fled_color.cpp, which land there. The two test suites are halves of one contract: change a rule in one and the other must move in the same change.One boundary worth a reviewer's eye
The transfer constraint (rule 2) applies to the display-encoded RGB family, not to
gray8/rgbw8, so{pixel_format: rgbw8, transfer: "linear"}is currently accepted. I probed this deliberately.The argument for tightening: rule 2's rationale is precision — 8 bits of linear light crushes the dark end — and that applies to any 8-bit format. The argument against, which won:
gray8is documented as a brightness/effect mask, and a linear mask is a legitimate thing to declare. Blanket-tightening every 8-bit format would forbid it for no benefit. Formats with no default tuple must already declare all four keys explicitly, and neither implementation interprets their color today.Happy to tighten if a reviewer disagrees — it's a one-line change to the format lists in
fled-color.ts, plus the same edit in FastLED'scolor.cpp.hpp.Review round (second commit)
An independent review of the first commit found three real gaps, all fixed:
gray8/rgbw8could declare all four keys "or be absent", while all three implementations reject an absent declaration there. The strict reading is right; the document was wrong, and now also distinguishes unresolvable from malformed.inspect-fled.mjsmissed rules it advertised (HIGH) — it skippedprimariesvalidation for non-string scalars and never implemented rule 7 at all, exiting 0 on files the reference validator rejects. The tool that exists to catch this was the one missing it. All five inputs the reviewer probed now match the validator.embedFpsswallowedvideo.fps(MEDIUM) — a color failure landed in the catch that returns the raw screenmap, silently dropping the playback rate too. It now writes the fps and warns.Also addressed: the spec's "advisory" promise contradicted rule 1's hard rejection of unknown values (a newer reader would refuse a file an older one plays — exactly what advisory prevents), so the spec now separates a declaration verdict from consumer file policy;
api-report.jsonrecords all seven new exports rather than three;DEFAULT_COLOR_TUPLEis frozen; reserved YCbCr matrix names read as "reserved" while a typo reads as "unrecognized".Scope note
The validator is exported but not yet called from any decode path, here or in FastLED. That is the intended phase-1 landing: producers and tooling are enforced now, consumers opt in as the color pipeline lands. The spec's consumer-policy section defines what they must do when they do.