Skip to content

Apply the Octoscript rename to l0-photo-weight and merge main - #13

Closed
ymote wants to merge 48 commits into
mainfrom
rename/l0-photo-weight
Closed

ymote wants to merge 48 commits into
mainfrom
rename/l0-photo-weight

Conversation

@ymote

@ymote ymote commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

This branch is l0-photo-weight (45 commits: the L0 native kits, beauty checks, design tokens, kit-host resources, ~224k lines of generated fragments under components/l0) with the same mechanical Splash -> Octoscript rename that main carries in fa91cae + a96f24b applied on top as one commit, then origin/main merged in (which brings the [workspace.dependencies] pinning from d49892a/6e2851e/844905f), plus one small port commit described below.

Rename tooling. The same rename.sh that produced main's rename, unchanged (263 paths git mv'd, 124 files rewritten), followed by fix-makepad-names.pl and fix-makepad-docs.pl over every tracked file to restore the identifiers upstream makepad owns (CxSplashVmExt, SplashVmId, MAIN_SPLASH_VM_ID, alloc_splash_vm_with_network, inject_splash_ui_handle, mark_splash_isolate_dead, handle_splash_network_responses, register_splash_isolate_mod, and the makepad Splash widget in live_design! and docs) — the same follow-up as a96f24b.

Merge. 19 conflicting files, all on lines the branch had changed that also carried the old name. Rust/DSL/docs (README.md, apps/flutter-samples/src/lib.rs, apps/kit-host/src/{l0,main,screens}.rs, components/l0/conformance.txt, crates/makepad-d3/README.md, crates/octoscript-makepad/src/lib.rs, crates/octoscript-makepad/tests/{l0_cards,l0_kit}.rs, crates/octoscript-render/src/{eval,lib}.rs) took the branch's renamed side. The seven Cargo manifests (apps/flutter-samples, apps/kit-host, crates/makepad-d3, crates/makepad-plot, crates/octoscript-makepad, crates/octoscript-render, crates/octoscript-widgets) took the branch's side and then had every makepad-widgets / makepad-script / octoscript-ui-l0 line converted from the branch's sibling-checkout paths (../../../makepad/..., ../../../octoscript/...) to { workspace = true }; the root [workspace.dependencies] block from main survived the merge untouched. No new manifest was added by the branch. After the merge, the only non-manifest differences from the rename commit are main's two ..Walk::default() lines.

octoscript-ui-l0 pointer. The root manifest points octoscript-ui-l0 at https://github.com/OctoSense-org/Octoscript.git, branch = "rename/fix-language-review-20260904" (OctoSense-org/OctoScript#20) instead of main, because the L0 node model this branch needs is only on that branch. Flip it back to main once that PR merges.

Port commit (Port octoscript-render's evaluator to the workspace makepad pin). The branch tip does not build against the pinned makepad 8c23d57: octoscript-render/src/eval.rs constructs the VM with the pre-ScriptVmHost shape (std: &mut std_slot) and calls ScriptVm::eval_checked, which exists in no published OctoSense-org/makepad revision (it appears only on port/appcard-on-octoscript @ 5b6932cf, and that lineage has the new VM shape). The commit builds the VM the way main's own eval.rs does and adds octoscript_render::eval_checked (same contract, written with with_instruction_limit + the captured-error sink); apps/kit-host/src/beauty.rs calls it. It is a separate commit so it can be dropped or replaced if a makepad revision carrying eval_checked gets pinned instead.

Verification.

  • git grep -n -i splash minus the protected spellings: only apps/kit-host/src/beauty.rs:19 host := Splash { width: Fill height: Fill }, which is the makepad Splash widget mount (kept on purpose, same as main's Splash{ ... } mounts; the exclusion pattern just lacks the space). No path contains the old name.
  • cargo test -p octoscript-render -p octoscript-makepad -p octoscript-node: 70 passed, 0 failed — including the L0 card tests that pull the fixtures and octoscript-ui-l0 from the renamed Octoscript branch.
  • cargo check --workspace --all-targets does NOT pass: octoscript-widgets (18 errors: Widget::selected_value x7 and Widget::checked are not trait members, View has no field selected x10) and makepad-plot (3 errors: GradientStop has no field straight_rgb) use makepad APIs that exist in no published makepad revision (checked 8c23d57, port/appcard-on-octoscript, work, dev, main, octoscript-vm-hardening, octos-org octos-one-framework, upstream makepad dev). The branch tip was evidently built against an unpublished local makepad. Resolving that needs either that makepad work pushed and pinned, or those two crates ported — outside the scope of a rename PR, so it is left as is and called out here.
  • The kit-host and the makepad tests include_str! L0 fixtures from a sibling checkout ../octoscript/crates/octoscript-ui-l0/tests/fixtures/ (lower-case, as the branch had ../splash/; main spells it ../Octoscript/) — an out-of-repo dependency inherited from the branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw

ymote and others added 30 commits August 10, 2026 16:20
The kit has no `import` — it is concatenated as source — so a theme is just a
palette prefix. The 15 colours move out of `_kit.splash` into
`_palette_dark.splash` (byte-identical values, so the goldens stay green) and
three new moods join it: `_palette_light`, `_palette_glass`, `_palette_photo`.
Their values are lifted from the shipped weather style templates those moods
replace, so a card that used to ask for "minimal weather" and get a
hand-authored DSL template lands on the same colours through the kit.

The palette must come FIRST, not after: a function body resolves a name at its
declaration point, so a palette appended below would not reach the roles that
read it. The kit body now says so where the palette used to be, because
rendering it alone leaves every colour undefined — which coerces to 0, fully
transparent, and draws a card that looks complete.

`l0_palettes_agree` pins the four together: every palette must define exactly
the names dark does, and the kit must not read an `l0_*` colour no palette
defines. That failure is invisible by construction — a missing entry is not a
default, it is transparent — and four palettes make it four times as easy.
Verified differentially: commenting out one `let` in the light palette fails
the test, restoring it passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adopts the structure makepad's own themes use — a handful of dials, everything
else computed from them — without taking on makepad's theme module, which the
kit's VM cannot reach: `splash_render::build` makes a bare `ScriptVmBase` and is
documented as the only makepad-script dependency in the render path.

The kit stops hardcoding sizes. `space_factor`, `radius_factor`, `font_base` and
`font_step` are the dials; every padding, radius and font size follows from them,
and the roles read tokens (`pad_page_x`, `radius_panel`, `font_title`,
`weight_bold`) instead of literals. Two things this buys that the flat colour
palette could not: NON-COLOUR theming (a compact or larger-text mood is now
expressible) and one place to change a scale.

A mood is a DELTA now, not a copy. Assembly is `base + delta + _derive + kit`:
`_palette_dark` holds the dials and every colour, a mood restates only what
differs, and `_derive.splash` computes the sizes afterwards. `glass` is fourteen
colours and `radius_factor: 1.5` instead of a second full palette.

The ORDER is load-bearing and fails silently both ways. Derivation must come
after the delta — `let` evaluates at its own line, so deriving in the base would
leave a delta's `radius_factor` with nothing left to change and every mood would
quietly keep the base's corners. And an assembly that omits `_derive.splash`
leaves every size an undefined name, which is 0: the tree still builds, with no
padding and no type scale at all. `l0_cards.rs` was assembling exactly that way
after the split and its structural tests still passed, which is the point.

Three tests, because none of these failures are visible:
- `the_default_tokens_are_the_numbers_the_kit_used_to_hardcode` pins all 20
  derived values. This is how "the layer moved, the look did not" is asserted —
  screenshots cannot do it, since two runs of a card are two generations of it.
- `a_delta_can_move_a_knob` builds every mood and asserts glass's panel radius
  differs from dark's, so the derivation really does follow the dial.
- `l0_palettes_agree` now checks that a delta only names tokens the base or the
  derivation defines: `l0_dimm` would otherwise bind a name nothing reads.

`conformance.txt`'s exact node counts are unchanged, so the tree is identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… backend

Asserts on the lowered SOURCE, because that is the only artifact where the defect
this catches was visible: a card declaring `factor 0.621371` lowered to
`(42 * 0.6)` while its realized tree and its built widget tree were both
structurally perfect. See Splash's fix for the measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ary, by a generator

The experiment: can the material screens' semantic vocabulary be handed to an
LLM as a contract, and a NEW app orchestrated from it? WIDGETS.md is that
contract — written from what the renderers actually read, not from what they
could — and brew.splash was then authored ONLY from the doc, by the generator
the doc is for.

It works: segmented + filter chips + slider + switch list-rows + a derived
summary, a derived price, and a conditional confirmation card, all recomputing
from the store. Verified on desktop at two states — the default ($4.40) and a
seeded one (Large/oat/syrup/3 shots → $6.60, every derived value correct).

The doc caused both failures, which is the finding:

  - the module-result trap. `page(brew_body())` as the screen's final
    expression evaluates to NOTHING — a blank window, no error anywhere. The
    kit's own comment records the rule; the doc advertised page() without it.
    Every real screen ends in a literal root, and now the doc says why.
  - a helper leak. The first draft used floorn(), which is the flutter kit's,
    not this one's — caught before running only because the doc was the
    declared boundary and floorn was not in it.

Two harness hooks fell out, both for the unattended QA loop:

  - SPLASH_ROUTE: the route file has no desktop equivalent; the env var is it.
  - SPLASH_SEED=k=v;k=v — write the store before first mount, so a sweep can
    photograph a screen in a CHOSEN state with no input driver at all. The
    route file picks the screen; this picks the state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…land

The 6T rendered brew as boxes with no words: chips, sliders and switches drew
in real M3 and every text node was silently absent. Two causes, one root — the
body was handed as TEXT to the `Splash` isolate:

  · `crate_resource("self:…")` resolves against the module that evaluated the
    body. On the isolate that is makepad_widgets, which ships no Roboto — so
    the font was missing and every label dropped, with nothing anywhere saying
    so. The APK even CONTAINED the font (assets/makepad/flutter_samples/…);
    it was simply never asked for.
  · the emitted `on_click` calls the `NAV` global, which only the app VM
    registers — on the isolate every tap died with an unseen "variable NAV
    not found".

kit-host hit both and fixed both (its mount comment records the blanked
labels); this ports that fix: evaluate with an explicit ScriptMod carrying
THIS crate's identity, hand the built View to the host widget, queue taps
through NAV/take_tap. Roboto ships in resources/ now, which the packager
already picks up.

Verified on the 6T end to end: the brew screen (authored by a generator from
WIDGETS.md) renders with text, and three adb taps changed size, milk and a
switch — summary and price recomputed to "Large · 2 shot espresso · oat milk,
$5.70", which is the right sum.

Two observations for the QA loop, both device-measured tonight: a tap aimed
from screenshot-estimated coordinates missed by one row (the case for reading
rects from the tree, not the screen), and the scroll swipe started on the
decaf row and toggled it — the swipe-fires-taps class, on this backend too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k contract

The widget-demo criticism answered with an actual app: Siren Coffee, a
Starbucks-shaped card in the semantic vocabulary. Tabs (Home/Menu/Order), a
seven-drink menu in categories, per-drink customization (Tall/Grande/Venti,
extra shots, oat, whip, quantity), a rewards balance with progress toward the
next free drink, and an order that earns stars. All state derived: the drink
card's "2 in order", the home tab's View-order button, every price, the stars
this order earns, and the balance after placing.

Walked end to end on the 6T by adb taps: customize -> Venti +2 shots oat qty 2
-> Done -> View order (2) · $17.90 -> Place order -> "Order placed, 34 Stars
added · now at 154". Every derived number correct (895¢ × 2, 2 stars/$, 120+34).

What it took is the finding — the WRITE-BACK CONTRACT, now in WIDGETS.md:

  · a button with `key` alone is INERT; `tap:1` writes 1, `action:"v"` writes
    the literal v — so "back" buttons are `action:"0"`
  · a chip writes `!on`; a chip that closes something open must declare `on:1`,
    or every tap rewrites 1 into a slot already at 1. The Done chip fired on
    every tap for four rounds and changed nothing — the no-op form renders
    perfectly and is invisible to everything but a state diff
  · sliders on this host never commit (their text editor opens, typed values
    do not write through; kit-host's slider-sync was never ported) — segmented
    is the index-writer
  · listitem action:"switch" renders a switch that is not wired to `key`
  · tap targets hit-test in UNSCROLLED coordinates: back/close affordances
    belong at the top of the screen
  · array `+` is not concatenation and fails silently — push, always

Also: the slider handle is a circle now (SliderRound base) instead of the
full-height stick, per review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the order view

Home/Menu taps from the order view wrote sb_tab correctly and were then
overridden: body() consulted a second sb_cart flag first. Two slots both
claiming to own the screen is the shadow-state class — the tap fires, the
write lands, the screen ignores it.

The flag is gone. View-order buttons jump tabs directly (key sb_tab,
action:"2"), which the write-back contract already covers: action writes a
literal. Verified on the 6T — Home from the order view renders home with the
placed order's 154-star balance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified-by-state-diff, renders-unwired, and does-not-write are three
different things that look identical in a screenshot. The contract now says
which is which, dated and per-host, and how a widget gets promoted: a
synthetic tap that moves its slot, never a look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One screen where every control writes its own slot and a caption RENDERS that
slot — so a screenshot diff is a state diff, and a control that fires without
writing is visible for the first time. Two proven anchors (chip, button)
validate the harness; the first chip tap missing its 32dp target and reading
as a widget failure is exactly why they are there.

Measured on the 6T, and the ladder in WIDGETS.md is rewritten from it:

  ✅ writes: chip, button(tap:1/action), segmented, tabs, checkbox
  ⛔ LIES:   radio, toggle, listitem action:"switch" — they flip ON SCREEN,
             write nothing, and the next remount silently reverts them. A user
             sees their tap work and later finds it undone. This is the worst
             class the catalog has, and it is invisible to every screenshot
             scorer because the screen genuinely changed.
  ⛔ inert:  fab, dropdown (no key wiring in the renderer at all),
             slider (its inline editor opens; nothing ever commits)
  🎨 notes:  progress draws a ring even with value:; dropdown's caret is tofu

The base-example rule follows from the measurement: exemplars draw from the ✅
row only, and a widget is promoted by a wired-screen pass — a tap that moves
its slot — never by a look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six rounds of the on-device movie-box run produced six JS idioms the parser
refused — if(cond,a,b), spread, value-position if — because the contract
documented the widgets and never the expression dialect. Each rule here is a
measured parser failure from that run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an `image` widget that renders a real bundled photo instead of the
gradient placeholder: `{t:"image", src:"coffee/hero"}` lowers to a native
Image emitting `src: crate_resource("self:resources/<src>.png")` with
CropToFill, full-width by default. A real photo returns None from lower()
so it passes through untouched — a lowered Image that still carried `src`
re-entered the arm forever (same trap the Input-chrome guard avoids).

flutter-samples: self-drive driver (JNI touch injection + framebuffer PNG
capture) so the phone taps and photographs itself; bundle coffee/ resources
(phone-generated hero + cups). WIDGETS.md documents the image row + assets.

Used by the on-device dev-master to author an image-rich coffee app: it led
with the hero, invented a state-derived cup swap, and fixed its own
segmented options:[]->items: bug from a finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
l0_tap_fit wraps in a Fit column, but a wrapped text node defaulted to
width: Fill, and Fill inside a Fit parent resolves to 0 in makepad — so
every tap-wrapped live hero drew as a zero-width column of one-glyph
wrapped lines: a small smudge at 17pt, a half-screen invisible void at
43. Found the moment the hero-size fix made live heroes big enough to
see they were not there. Coerce the child to fitw so it hugs its glyphs
exactly like its wrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
Two items off the mockup-translation gap list (NOTES-weather-tokyo.md):

- The photo surface's scrim is two stops instead of one flat wash:
  l0_scrim_top over the photograph, the mood's l0_scrim under the
  content. The photo mood restores the shipped immersive exemplar's
  exact stops (#0a0f1a22 -> #05070cf2) — flattened to one colour when
  the style was ported into L0, and most of why a forecast panel never
  read as floating out of the dark.

- weight_hero: the hero gets its own weight axis. Base stays bold (700);
  the photo mood dials it to 100 — a hairline numeral over a photograph,
  the largest visual gap between the shipped hero and the design mockup
  after size itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
…ons, pane inset

Closing the measured gap between the HTML twin (8/10 vs the mockup) and
the Splash card (4/10 at the start of this round, 6/10 after):

- hero_factor scales only the hero ramp's TOP step, so a photo-surface
  numeral reaches mockup scale without a long text hero wrapping at 86pt
- l0_bar / l0_bar_rail: the mood hands TempBar one hue on a faint rail
- icon_mono: WeatherIcon recoloured to a single ink via l0_icon_ink —
  composed around the constructor like l0_tinted, because mutating a
  fresh dict's w/h evaluated to nil in this VM
- panel_inset: the floating inset card; l0_fill goes dark-translucent,
  hairlines quieter, weight_semi 500 in the photo mood

Remaining, measured and unreachable from a mood: letterspaced caps and
a real blur primitive (pipeline-v2/REPORT.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
The kit's answer to TextEyebrow (splash 'TextEyebrow' role): caption
scale, medium weight, soft ink, and a variant marker the backend uses
to space and uppercase a literal. Photo-mood value weight drops to 400
per the parity judge's 'rows bolder than the mockup'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
…s not land yet

row_lines: l0_row_text stamps `lines` from the palette, so whether a list
row truncates is the theme's call and no card names it — presentation
staying where profile 1.1 puts it. Base is 1: the round-2 corpus showed
titles wrapping to three lines and breaking mid-word ('Thund erstorm'),
which wrecks the row rhythm on every feed card.

panel_border/l0_stroke: l0_panel now carries border and bordercolor, and
the light mood asks for a hairline edge — a panel with no edge dissolves
into a light page, which is what the material and pastel schools were
marked down for. This does NOT currently reach the screen; see the note
in the app emitter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
`Attrs.elevation` has been declared on the node and read by the evaluator the
whole time, and written by nothing. `l0_panel` now carries it, and the base
palette gains `panel_lift` — 0 for dark, which separates by fill alone, and 6
for light, where a card on near-white needs a shadow to read as a surface.

Delivery-gated on device 2026-08-30: the light baseline now darkens the page by
44 luminance levels just below the card edge (199 against a 243 background).

Also fixes a scope error that fired on EVERY render: `l0_stroke` read
`l0_hairline` 27 lines before that name was declared. `let` evaluates at its own
line, so the name was unresolved, and an unresolved name coerces to 0 — a fully
transparent stroke ink. The declaration now sits after the colour it reads, and
the device log goes from a scope error per render to none.
The weather fixture writes `Col(gap: 36)` with a comment calling it "the
mockup's air — the half-screen of photograph". Over the photo mood that gap
frames a picture; over a flat fill it is simply a hole, and the same card
scored 6/10 in photo against 5/10 in the three flat moods, with every mood's
judge naming the void as its biggest gap.

`l0_col_gap` is the one place the kit can still speak about a card-authored
gap, and §1.1 says spacing is the kit's to choose. Gaps of 24 or more are
scaled by a new `air_factor` — 0.62 in the base, 1 in photo, which keeps the
picture's air whole. Anything under 24 is deliberate fine spacing between
related things and is untouched.

Measured on device: the mid-screen void between the hero and the forecast card
drops from 164px to 126px.

Written as an early return rather than reassigning a local inside the `if`. The
first version did `let s = g` then `s = g * air_factor` in the branch and moved
the rendered void by exactly nothing, so that mutation does not escape the
block — `l0_colw` already branches by returning, and this now matches it.
Two knobs already existed and only `photo` carried them. Scoring the four moods
turned that into a natural experiment twice over: photo was the only mood NOT
accused of "system emoji icons instead of custom iconography", and later the
only one NOT accused of "spectral gradient bars that break the restrained
palette". The three moods at the default were flagged both times.

`icon_mono` and `l0_bar`/`l0_bar_rail` now sit in the base. Light overrides the
rail, because the base's white-at-16% vanishes on a near-white page.

The rainbow ramp also encodes nothing the bar's own length does not already
carry, so the spectrum was decoration competing with the type.
A judge scoring these renders called the flat moods "generic flat surfaces"
with "no atmosphere", and scored the photo mood — the only one whose page is not
a flat colour — on its atmosphere rather than against it.

`l0_surface` now carries `bg2`, the same second stop the photo scrim already
uses, so this applies an existing and verified capability where it was missing
rather than adding anything new. Dark settles a few levels darker toward the
bottom, light lifts toward white, glass deepens into its navy. A mood that wants
a genuinely flat page sets `l0_base_2` equal to `l0_base`.
The first version lifted light's page to #ffffff -- exactly l0_fill, the card's
own colour -- so the card dissolved into the page toward the bottom of the
screen. Measured: page and card both at luminance 255, separation ZERO.

A judge scoring that build called it 'near-white' with 'flat low-contrast
surfaces', which is precisely that merge; I read it as a generic complaint
rather than as the regression I had just introduced.

Now deepens to #e8e8f0. Separation 0 -> 21. A gradient behind a surface has to
move away from that surface's fill.
Both stops sat within five levels of each other, so the fall rendered as 5
luminance levels on device and a judge called the background 'flat black' and
the gradient 'invisible' -- correctly. Glass, whose stops are 39 levels apart,
reads as intended.

The TOP stop lifts to #131a25 and the BOTTOM stop becomes the canonical
#0a0e14, so the screen still settles on the page colour where content ends.
Measured fall: 5 -> 11 levels.
One indigo, lifted from the photo mood into the base, cannot serve a navy page,
a white card and a near-black page at once. Measured against each mood's own
panel: glass 1.76:1 and light 1.92:1 -- the 'accent bars clash' and 'accent
colour feels generic' a judge named -- while dark sat at an acceptable 2.90:1.

Glass goes lighter (#9fd8ff, 2.76:1) because on navy the separation has to come
from luminance; light goes deeper (#3d48b8, 2.96:1) for the same reason against
white. Dark keeps the base indigo. Both landed on their predicted values on
device.

This is not accent VARIETY, which measured null earlier -- it is contrast
against the surface, which is a different quantity.
pad_page_top was 54 logical px with a comment saying it clears the status bar.
Measured on device, the system already insets the app below the bar -- the
page's own fill begins at y=102, under a black status area -- so all 54 sat
inside already-safe space and rendered as the screen's single largest void
(173px, 7.4% of the display).

36 rather than 0: a 24dp status bar still clears with margin if some window
configuration ever stops insetting. Measured: first ink moves from y=272 to
y=221, the top band falls 166 -> 116px, the trailing band disappears, and total
dead vertical space goes 487 -> 394px.
The previous commit made the fall visible by raising the page's TOP stop to
#131a25. It worked (fall 5 -> 11) and paid for it exactly where it hurts: the
hero's contrast against the page went 11.0:1 -> 8.0:1, and a judge immediately
called the text contrast low.

Deepening the BOTTOM to #000103 instead is strictly better on both axes:

    original flat   fall  5   hero 11.0:1
    lifted top      fall 11   hero  8.0:1
    deepened bottom fall 11   hero 12.5:1

The top is where the type sits; the bottom is where the content has already
ended. A page gradient should spend its range there.
This file's own comment says 'the scrim carries all of the legibility'. At 13%
opacity at the top it did not: the hero numeral measured 2.59:1 against the
photograph — under WCAG AA's 3:1 for large text — and 2.71:1 even before the
top-padding change moved the hero into a brighter part of the image. A paired
judge preferred an older build purely because its hero happened to sit over a
darker region, which is legibility by luck.

Top stop raised to 40%. Measured on device: 2.59 -> 3.37:1, AA pass, and better
than the build this regressed from.

40% rather than the ~65% that would reach 4.5:1: past that the photograph stops
reading as a photograph, and this mood exists to show one.
The hero measured 2.59:1 against the photograph, under WCAG AA's 3:1 for large
text, so the previous commit raised the top scrim to 40%. That reached 3.37:1
and flattened the upper half of the image into uniform grey; a paired judge
preferred the OLD build for exactly that reason -- 'keeps the photo's depth and
lantern glow alive' -- and it was right. This mood exists to show a photograph.

The scrim was the wrong lever. weight_hero was 100, the lightest weight there
is, over a busy image: the strokes were thin, not merely low-contrast, which is
why WCAG sets a lower threshold for bold text. 200 keeps the cinematic thin
hero and puts down 28% more ink than the original (5138 px vs 4011) -- MORE than
the 40% scrim managed (5066) -- while the scrim pulls back to 22%.

Paired judging prefers this over BOTH the original and the heavy scrim.

Honest limitation: the luminance ratio is 2.79:1, still under AA. Reaching 3:1
by luminance alone costs the photograph, and the two goals genuinely conflict
here. Recorded rather than hidden.
43 corpus cards declare one of these two and could not realize. The catalog
entry lands in Splash; this is the kit's answer to the names.

Both are deltas from dark and both keep the panel-lift ratio every shipped mood
converges on — measured at 1.12:1 in light and dark alike, and stated here as an
alpha so it self-scales to whatever ground the mood sets.

`vibrant` puts the colour in the PAGE rather than in an accent, which is the one
thing the accent axis cannot do: accent reaches a bar, a highlight and a button
and never a surface. Ink stays near-white — tinting the text as well is how a
vibrant mood becomes an unreadable one.

`minimal` withdraws colour entirely and lets a hairline do the separating. Its
brief is restraint, and one measurement this session says what restraint must
NOT mean: in an eight-way blind comparison the emptiest mockup lost all seven of
its pairs, with both judges calling its space "void", "empty, not a pause",
"unmotivated". So the budget goes into a wider ink range and a slightly airier
rhythm, not into removing things.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`light` is the only mood allowed to invert `l0_text`, because the host's
light-over-Photo substitution keys on exactly that being the single case. A
second mood setting a dark ink would render unreadable text over a photograph
with every automated check still passing — and `l0_card.rs` has a test that says
so, which caught this before the pointer moved.

Both moods wanted dark's white anyway, so nothing is given up. The reason is
recorded beside each, since the next mood added here will hit the same wall.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… library

_derive_color.splash computes the palette from nine seed scalars in-kit via
mod.math.round — the Ant-style seed architecture; gen_grounds emits 14 grounds
as seeds, not solved files. Packs: _palette_atro(+_light) carry a purchased
kit's system whole (palette via the E4 solver, measured type scale at the 0.62
font-unit calibration, navy glow depth, geometric family, card gradient).
Axis fragments: accent x mood cross-product (incl. packs), depth (flat/hard/
glow bound to solved tokens), type (serif/display), texture (grains+patterns).
Kit: l0_card splits from l0_panel for pack gradients; l0_avatar; texture/
family/tracking ride every text role. splash-node Attrs grows texture/shadow/
family/gradient_across fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ymote and others added 18 commits September 1, 2026 15:42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in layers

l0_surface goes fith->fillh (a design frame is owned, not hugged), l0_thumb
takes a square flag, and l0_surface_pin2/pin3 place layers by aligny because
deferred fills in the app fork are greedy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o ground

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… attr

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sixteen fragments, eight hues over the pack's two moods, minted the same way
the other packs' are. Without them a CaMo card naming an accent found nothing
to splice, and an axis the assembler cannot find contributes silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_derive_color` reads `l0_accent`'s previous binding, and nothing bound it
unless a card named an accent AXIS. So every base mood — dark, light, glass,
minimal, vibrant, photo — errored once and painted the accent fully
transparent, which is invisible because 0 is a legal colour and the VM's
complaint goes nowhere this side can read.

The value is not invented: `l0_bar` in this file IS the base's accent, and an
accent fragment says exactly that (`l0_bar = l0_accent`). A fragment spliced
later still overrides it.

Found by `lab/sketch/lint_theme.py`, which resolves the chain statically;
confirmed independently by running the VM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pack's card colour is part of its identity — CaMo's black slab, Atro's
indigo gradient — and it is stated once, in the pack's base palette. The
`_light` variant inherits that colour and flips `l0_text` dark with it, and
nothing re-solves the pair: CaMo light drew near-black headlines on a pure
black card at 1.14 contrast, unreadable, on the desktop rail and the phone
alike. That both rails agreed is what proved it was the theme.

The kit used to mark such a card `inkdark`. No renderer ever read that flag
and the evaluator never even parsed it, so it fixed nothing for as long as it
existed. `ink` carries the colour instead, and a backend applies it only where
a descendant's own colour fails contrast against the card — an accent that
already reads keeps what the theme gave it.

`l0_aligned` also records why the two rails still disagree about centring a
column: giving it `fillw` does not close it, because a makepad Label defaults
to Fill width while an ArkUI text node hugs, so every child spans the centred
column and still draws its glyphs at its own left edge. Measured, not assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Attrs.inkdark` was a boolean marking "this card's fill needs different text".
No backend read it, and `splash-render`'s evaluator never parsed it out of the
VM at all, so a card could set it for months and change nothing. A flag cannot
say WHICH ink; carrying the colour can, and lets a backend apply it only where
it is needed.

`splash-makepad`'s emitter resolves the planes as a pre-pass over the tree
rather than threading them through emission: it is a property of the tree, not
of the emission, and the two backends then apply one rule at the same point in
their pipelines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`dc_toward_ink` and `dc_toward_pole` were defined ABOVE the accent-as-seed
solve and read `seed_ink_*`. A function in this VM keeps the binding a name had
when the function was defined — the boot selftest on the ArkUI rail now logs
that answer rather than leaving it to argument — so both mixers used the
ORIGINAL seeds and never saw the tinted values the solve produces. Every ink
derived through them (l0_text, l0_soft, l0_dim) came out untinted, and the
whole accent-as-seed path was inert.

A call may precede a definition, so moving the two definitions past the rebinds
is the entire fix. Found by `lab/sketch/lint_theme.py`.

The light mood's `l0_dim` was 2.92 against its own page — below AA and below
even the large-text floor, so every caption in that mood was under-contrast.
Nothing measured it because the auditor checked the PRIMARY ink only, and
captions do not use the primary ink. Darkened along its own faint blue cast
until the page pair clears AA at 4.63.

Neither change reaches the corpus: none of its 122 cards names `theme light`
or declares an accent axis, and the 15 Atro screens re-render pixel-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rces

Work in progress carried by the Octoscript-AppCard l0-content-parity
branch: the native L0 kit packs (atro/camo/taskplan/ux, with the fonts
their tokens reference), beauty/kit-audit examples, design.rs and l0.rs
in splash-makepad, kit/design/progress widgets, chart_paint in
makepad-plot, l0_helpers in splash-render, units in splash-node, and
the native-l0-kits doc. Makepad studio state (.makepad/) is ignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	README.md
#	apps/flutter-samples/Cargo.toml
#	apps/flutter-samples/src/lib.rs
#	apps/kit-host/Cargo.toml
#	apps/kit-host/src/l0.rs
#	apps/kit-host/src/main.rs
#	apps/kit-host/src/screens.rs
#	components/l0/conformance.txt
#	crates/makepad-d3/Cargo.toml
#	crates/makepad-d3/README.md
#	crates/makepad-plot/Cargo.toml
#	crates/octoscript-makepad/Cargo.toml
#	crates/octoscript-makepad/src/lib.rs
#	crates/octoscript-makepad/tests/l0_cards.rs
#	crates/octoscript-makepad/tests/l0_kit.rs
#	crates/octoscript-render/Cargo.toml
#	crates/octoscript-render/src/eval.rs
#	crates/octoscript-render/src/lib.rs
#	crates/octoscript-widgets/Cargo.toml
The l0-photo-weight tip was built against a local makepad checkout that has
`ScriptVm::eval_checked` and the pre-ScriptVmHost VM shape; the workspace now
pins OctoSense-org/makepad @ 8c23d57, which has neither. Build the VM the way
main already does (`ScriptVmHost::new((), ())`) and express `eval_checked` in
octoscript-render with the pinned revision's `with_instruction_limit` and
captured-error sink, so octoscript-render, octoscript-makepad and octoscript-node
build and test against the pin. The kit-host beauty preview calls the same helper.
@ymote

ymote commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #18, which merged l0-photo-weight into main under the new crate names with the makepad pin at 703c00a (the fork now carries the four widget/svg APIs this branch needed).

@ymote ymote closed this Sep 14, 2026
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