Skip to content

feat(orbit64): V2 picture-model facelet bridge - #43

Merged
wstein merged 6 commits into
mainfrom
orbit64-v2-picture-facelets
Nov 1, 2026
Merged

wstein merged 6 commits into
mainfrom
orbit64-v2-picture-facelets

Conversation

@wstein

@wstein wstein commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Scope

Adds the Orbit64 V2 picture-model facelet bridge next to the merged color-model bridge, following FORMAT-V2.md ("Picture facelets") and the Flix reference src/Orbit64/V2/Facelets.flix of flix-orbit64 a046e34.

  • A picture string has one base64url symbol per facelet: the sticker's index within its class (corner 3p+j, midge and wing 2p+j, center orbit p, fixed center 4p+r). It is as long as a color string, six fields separated by one space.
  • toFacelets, fromFacelets, encodeFacelets and decodeFacelets now accept the picture model for n = 3..15, canonical and framed (all 26 picture registry entries). The color API is unchanged except that a picture type is no longer an error. The 2x2x2 has no picture entry and reports the reference's text.
  • Reading validates in the exact order of the format and gives the reference's reasons: symbols before the count; per class corner, midge, wings (outermost first), centers, fixed centers, range before repeat; then slot pieces; the pose; the laws (twist, flip, M = C, R = C, center orbits).
  • A fixed center's rotation follows whole-cube turns (rotatePicture/unrotatePicture), ported from the reference.

Hardening (also benefits the color model and the state codec):

  • Orbit64V2.snapshot reads each array length once, accepts only a non-negative integer length, copies at most 65 entries (no array of a cube has more than 42), and keeps only primitive numbers (everything else becomes undefined, so no caller valueOf runs and a Number object no longer passes). A hostile length previously took seconds or exhausted memory.
  • The facelet string is walked once with O(1) extra memory beyond 6 n^2 values (no Array.from of the whole string); the first bad character is still reported before the length.
  • No String() of caller values; only a single code point of the caller string is quoted, as in the color port and the reference (the reference quotes a UTF-16 unit; for a character outside the BMP this port quotes the whole code point, as the color port already does).

Fixture and generator:

  • scripts/orbit64V2FaceletFixture.jl (Julia stdlib only) now also writes test/cube/notation/orbit64V2FaceletPictures.json from test/TestV2FaceletPictureData.flix. It fails on dropped, duplicated and unparsed rows, on entries other than the two of a size, on characters outside base64url and on tokens of unequal length, and pins 174 rows and the move sequences per size (3: 41, 4: 4, 5: 5, 6: 4, 7: 5, 8: 3, 9: 4, 10: 3, 11: 4, 12: 3, 13: 4, 14: 3, 15: 4). Both JSON files regenerate byte for byte (checked twice); the color file changes only its source revision (d701008 -> a046e34, the color rows are identical).

Verification

  • npm run typecheck, npm run lint, npm run format:check, typos, npm run build: clean.
  • Full vitest run --testTimeout=60000: all 83 test files pass (after git lfs pull; without LFS objects fixtures.test.ts, gridAlignmentRealCrops.test.ts and liveFaceAppearance.test.ts fail on JPEG pointers, independent of this change).
  • Tests take their expected values from the Julia/Flix data: 174 rows encode, decode, round-trip and read in any orientation; exact reasons and ordering for every error class; 24 frames on every odd size; fixed-center symbols 4f + r from the definition; hostile cubes (flip-flopping Proxy arrays and getters, length tricks, valueOf/Number/bigint entries, huge lengths and strings) never yield an unreachable cube and never throw.
  • Differential run against the Flix reference (Orbit64.V2.Facelets.encode / decode) over about 37,000 cases: 174 reference rows mutated (single and compound: random symbol, in-class symbol, swaps within and across classes, whole-slot swaps, twists, fixed-center rotations, deleted/inserted characters, bad characters, other entry id) plus random valid and mutated tokens of all picture entries. 0 disagreements in reason or result. The only differences seen are the reference's non-ASCII output being printed as ? when the JVM runs without UTF-8 stdout; with UTF-8 there are none.
  • Hostile battery (about 28,000 calls: every hostile value in every argument position of every exported function, random flip-flopping cubes, random strings): 0 throws, and every Ok text of a hostile cube reads back as a state that writes the same text.
  • Hand mutation check of about 40 compilable mutants of the new code (symbol alphabet, class limits, per of fixed centers, order of checks, rotation direction, pose handling, snapshot guards): all killed by the new tests except two that change no behavior (the surrogate-pair step of the string walk, which stops at the first non-BMP character anyway, and an identity edit).

Risks

  • snapshot reads more strictly than main. An object with a valueOf (or a Number object) as an entry or frame was already rejected on main, but main ran the caller's valueOf (through the global isFinite/Math.floor, several times per entry) before rejecting it; now no caller code runs and the errors are the same. A length that is not a primitive non-negative integer (an object with a valueOf, a Number object) was coerced and accepted on main (an array-like reporting such a length passed) and is now rejected with the usual shape reason; the same guard stops the hostile-length cost. A bigint or symbol entry moves from malformed state to a normal layout error. Arrays longer than 64 entries read as 65 entries; the reasons are unchanged since no valid layout is that long.
  • A quoted bad character outside the BMP is the whole code point, not a UTF-16 unit as in the reference.
  • Not covered: the reference's "no whole-cube rotation puts the DBL corner home" for pictures is unreachable after the slot checks, so it has no dedicated test (the differential run cannot reach it either).

https://claude.ai/code/session_01VB53JGakrcQ5QAU95yXdMz


Generated by Claude Code

wstein and others added 6 commits November 1, 2026 17:41
The snapshot of a cube read its array lengths as given, so an array with a
hostile length (a Proxy or array-like reporting 1e9 entries) cost seconds
or exhausted memory before validation could reject it. A length that was an
object with a valueOf, or a bigint or symbol, was coerced more than once or
threw. Entries and the frame were copied as they were, so an object with a
valueOf passed validation and then answered differently when it was written.

The length is now read once, only a non-negative integer counts, and at most
65 entries are copied: no array of a cube has more than 42 entries, so a
longer one is no state either way and the reason is unchanged. Only a
primitive number is kept as a number; anything else becomes undefined and
fails validation without any caller code running on it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The picture model writes one base64url symbol per facelet, the sticker's
index within its class (corner 3p+j, midge and wing 2p+j, center orbit p,
fixed center 4p+r), so a picture string is as long as a color string. The
bridge now accepts the picture model for n = 3..15, canonical and framed,
through the existing functions: toFacelets, fromFacelets, encodeFacelets and
decodeFacelets. The color API is unchanged except that a picture type is no
longer an error; the 2x2x2 has no picture entry and says so.

Reading validates in the order of FORMAT-V2.md, with the reference's reasons:
symbols before the count, then per class (corner, midge, wings outermost
first, centers in orbit order, fixed centers) a range error before a
repeat, then each slot's pieces, then the pose, then the laws. A fixed
center's rotation is carried through whole-cube turns by the same geometry
that turns the stickers.

The string is now walked once and only its first 6 n^2 values are kept, so a
huge string costs time but no memory, for both models; the first bad
character is still reported before the length.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rator

The generator now also copies the 174 picture rows of
TestV2FaceletPictureData.flix (generated in flix-orbit64 by
facelet_picture_vectors.jl) into orbit64V2FaceletPictures.json. It fails on
dropped, duplicated and unparsed rows, on a picture entry other than the
two of a size, on a character outside the base64url alphabet and on tokens
of unequal length, and pins the 174 rows and the move sequences per size.
Both files regenerate byte for byte; the color file changes only its source
revision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The reference rows encode and decode exactly, in both poses, and read back
as the same cube in any orientation. Invalid pictures give the reference's
exact reasons in the order of FORMAT-V2.md, including which failure wins
when several happen in one step. A hostile cube (index getters, Proxy
arrays that change between reads, hostile lengths, valueOf tricks) is
copied once and never yields a picture of an unreachable cube; huge
strings and lengths cost bounded work; nothing throws.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… on stray lines

The architecture note said huge strings cost bounded work; the walk is
linear in time and bounded in memory, and now says so. The picture test
named "returns an error for every mutated row" only checks that nothing
throws and that results are well formed, and is renamed accordingly. The
fixture generator now fails on any line of TestV2FaceletPictureData.flix
that is neither a row nor one of the file's known structural lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@wstein
wstein marked this pull request as ready for review November 1, 2026 18:42
@wstein
wstein merged commit f0b4600 into main Nov 1, 2026
2 checks passed
@wstein
wstein deleted the orbit64-v2-picture-facelets branch November 1, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant