Skip to content

Apply the Octoscript rename to fix/language-review-20260904 and merge main - #20

Merged
ymote merged 43 commits into
mainfrom
rename/fix-language-review-20260904
Sep 13, 2026
Merged

ymote merged 43 commits into
mainfrom
rename/fix-language-review-20260904

Conversation

@ymote

@ymote ymote commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

This branch is fix/language-review-20260904 (41 commits of language-review fixes and L0 work: approval flow, kit packs, value origins, the media host crate and their tests) with the same mechanical Splash -> Octoscript rename that main carries in 50267c3 applied on top as one commit, then origin/main merged in. After the merge the tree is content-identical to the renamed side branch: main's only other changes were the rename itself and a duplicate of the branch's own #19 fix.

Rename tooling. The same rename.sh that produced main's rename, unchanged: git mv of every path containing splash/Splash/SPLASH, then the same perl rules over every text file (protecting android_splash_screen / "Splash Screen", Unsplash, splashed/splashes, runsplash and makepad-splash). 158 paths renamed, 162 files rewritten. The merge conflicted in four files (Cargo.toml, Cargo.lock, crates/octoscript-ui-l0/Cargo.toml, crates/octoscript-ui-l0/src/lib.rs), all on lines the branch had changed that also carried the old name; each was resolved to the branch's renamed side. Cargo.lock was then regenerated by cargo (package reordering only).

Verification.

  • git grep -n -i splash (minus the protected spellings): nothing left; no path contains the old name.
  • cargo test --workspace: 860 passed, 2 failed, both in octoscript-ui-l0's profile test (the_source_catalog_matches_the_toml_spec: "sys.news_digest is in the Rust catalog but not in the TOML"; the_field_vocabularies_match_the_toml_spec: 29 vs 32). Both fail identically on the unrenamed side branch — the WIP added sys.news_digest to the catalog without documenting it in docs/ui-l0-constructors.toml — so they are the branch's state, not the rename's.
  • crates/octoscript-ui-l0/tests/live_news.rs does include_str! on ../../../../tools/octoscript-research/live-news.card, a file OUTSIDE this repository (it lives in Octoscript-AppCard under tools/splash-research/). The test binary only compiles when that sibling path exists; it was supplied for this verification. Also inherited from the side branch, and worth fixing as a follow-up (vendor the card as a fixture).

Downstream. The matching Octoscript-Makepad rename PR points its octoscript-ui-l0 workspace dependency at this branch (branch = "rename/fix-language-review-20260904") until this PR merges, after which that pointer goes back to main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw

ymote and others added 30 commits August 10, 2026 16:20
…oks like

L0 forbids a card naming a colour, so a request that asks for a look — "dark
weather tokyo", "毛玻璃天气" — had nowhere to go. octos-one answered it by
routing such requests to a hand-authored Splash-DSL template ahead of the L0
path, which meant the card was not L0 at all.

`theme <mood>` is the declaration that closes that gap without reopening §4.
At most one per card; the name must be in `catalog::THEMES` (dark, light,
glass, photo) and an uncatalogued one is refused at parse time with the known
set named — the same shape as an unknown source capability. A second `theme`
is refused naming the line the first was on.

It is not a presentation statement: a card saying `theme glass` has said
nothing about what glass IS. The kit answers the mood with a palette, so the
page, the panel fill, the hairline and the ink are still decided one layer
down, and a colour literal is refused exactly as before.

`card_theme()` reads it without realizing, like `state_initials()` — the host
has to pick a palette to assemble the source the kit is concatenated into,
which is earlier than there being a tree.

§4.1 documents it, including the direction it is NOT: the profile sketched
`source env.theme sys.theme()` — a card ASKING the ambient theme — and that
was never catalogued, so it is a sketch and a card naming it is refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lexer treated `=`, `!`, `<` and `>` under one rule: only the two-character
form is a comparator. That is right for `=` and `!` (without it `!x` lexes as a
comparison and the level classifier's punctuation check never fires) and wrong
for the angle brackets, which have no other meaning in this grammar — no
generics, no arrows, no tags.

So `<` and `>` lexed as punctuation and no guard could use them, while
`framework/l0.md` documents `== != < <= > >=` and tells an agent to write
`when a > b { … }` instead of `if a > b`, and `compare` has implemented `<` and
`>` all along and could never be reached with them. A card following the
reference was refused with `expected "{", found "<"`.

Found writing the first card that needed a threshold both ways: a decision tree
of complementary guards needs `>= 40` AND `< 40`, and only one of them parsed.

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

The section said a theme was "a palette concatenated in front of the kit body",
which was true for one commit. It is now `base + delta + _derive + kit`: dials
and colours, then the mood's delta, then every size computed from the dials, then
the roles. Written down because both orderings that matter fail silently —
deriving before the delta leaves a mood unable to move a dial, and omitting the
derivation leaves every size undefined, which is 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A host choosing a palette needs to know whether the page is a PHOTOGRAPH, and
that is a fact about the source rather than about a realized tree — so it is read
the same way `card_theme` and `state_initials` are, by parsing and looking.

The pairing it exists for: a mood with dark ink is unreadable over an arbitrary
image, so a kit may legitimately answer `light` + `Photo` with its photo palette
instead. That decision belongs one layer down, and this is the fact it needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`trim_num` is a DISPLAY rule — one decimal, because a temperature reads as 21.4
and not 21.437 — and `expr_shape` was using it to emit arithmetic OPERANDS.
Rounding an operand does not round a display, it changes the computation.

Measured on the 6T with the new convert card: `state factor { initial: 0.621371 }`
lowered to `l0_hero((42 * 0.6), 22)` and drew 42 km as **25.2** miles instead of
26.1. A second card rounded 2.20462 to 2.2 the same way, which is what made it
obvious this was not the model rounding — I had blamed the generation first. The
card was correct, the checker accepted it, the realized tree and the built widget
tree were both structurally perfect, and the only wrong thing was the number a
person reads.

Operands now emit through `{}` on the f64, which is the shortest representation
that round-trips: 0.621371 survives and 42.0 still emits `42`.

`an_l1_coefficient_is_not_rounded_on_its_way_to_the_backend` in Splash-Makepad's
`l0_cards.rs` pins it, asserting on the LOWERED source — the only place the
defect was visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Row-shaped like `sys.news` and for the same reason: a feed is a list, and
`offset` is what lets a card show a lead event above the rest without repeating
it. Answers `id`, `mag`, `place`, `depth`, `ago`, `lat`, `lon`.

The backend helper already existed in aichat (`quake_field`/`quake_num` over the
USGS 2.5_day GeoJSON) — only L0 could not name it. `id` has no backend field: the
row index IS the identity, which is all a `for` key needs, so the lowering emits
it as a literal rather than a call.

Both catalog mirrors updated, which the TOML-spec tests require — they caught the
Rust side being one entry short before this compiled.

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

Guards are evaluated at realize against injected data, and a host injects list
rows and little else, so `when now.precip >= 40` compares against nothing and
BOTH complementary branches are false. Measured on the 6T: a weather-activity
card drew a correct header, correct rain and AQI tiles, and no verdict at all —
the §1.1 failure, from a card the checker accepts.

Closing that needs the guarded fields resolved BEFORE realize, and exactly those:
resolving every field of every source would fire a request per field per render,
which `fetched_scalars` refuses to do for good reason. This is the part that is
cheap and knowable — walk the views, collect `<declared source>.<field>` from
every `when`, dedupe, skip `$state` and state paths.

It is NOT sufficient on its own, and I found that by trying to use it. A guarded
field is reached by a call built from its source's ARGUMENTS, and those are
frequently paths into another source: weather-activity guards `now.precip`, `now`
takes `lat: place.lat`, and `place` is a `sys.geocode` scalar that is not in the
blob either. So the host change needs scalar sources resolved in DEPENDENCY ORDER
first — which is most of what realize already does — and I backed the half-built
version out of the render path rather than leave it there. The doc comment
records this so the next attempt starts from the real shape of the problem.

`weather-activity` stays unregistered.

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

Guards are decided during realize, against the injected blob, and a fetched
scalar was never in it — the kit resolves one lazily at draw time. So
`when now.precip >= 40` compared against nothing, and so did its complement
`when now.precip < 40`. Both false, in both directions, with no diagnostic.

`guard_bindings` is what a host resolves BEFORE realize: for each `source.field`
a `when` reads, the binding the kit would have emitted for that same field. The
branch is then taken on the number the tile beside it displays, and the two
cannot drift. Dependencies come out already resolved — a source argument
lowers to the callee's own live call, so `now` carrying `lat: place.lat`
arrives as one nested expression and nothing has to be fetched in order.

The card's own `when`s are the fetch policy. A card with no value guards asks
for nothing, which is why this can be unconditional where resolving every
scalar of every source could not.

Two defects found on the way, both of the same shape as the L1 operand that
lowered 0.621371 as 0.6:

  - a source ARGUMENT was emitted through `trim_num`, a one-decimal DISPLAY
    rule, so `sys.weather(lat: 37.7749)` asked about 37.8 — a different city,
    and a perfectly plausible forecast for it;
  - the source-argument grammar had no case for a negative literal, so
    `lon: -122.4194` failed at the `-` with "expected a name". No card could
    name a coordinate in the western hemisphere, or a temperature below zero,
    without going through `sys.geocode`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A string slot is not safe by construction after all. `{:?}` quotes whatever it
is handed, and a source argument that names ANOTHER SOURCE resolves to that
source's own live call — so `sys.photo(query: place.name)` lowered to
`sys.photo("sys.geocode(\"kyoto\", \"name\")")` and the wallpaper was generated
from the TEXT of the call.

Found on the wire, not on the screen. The device fetched
`image.pollinations.ai/prompt/sys.geocode%28%22kyoto%22%2C%20%22name%22%29` and
drew a photograph of Kyoto anyway: the service is a generative image model and
the place name is inside the string it was given. Every weather card has been
doing this. The request was wrong for as long as the arm has existed and there
was nothing to see.

`SourceBinding::nested` carries which arguments the lowering built as calls, and
the new `text()` helper unquotes exactly those — the text-slot counterpart to
`num()`. It is decided by what the lowering built, NOT by whether the string
starts with `sys.`: a text argument can carry what a person typed, so a youtube
card's search box would otherwise make a typed query executable.

Verified on the 6T. Before: `prompt/sys.geocode%28%22kyoto%22…`. After:
`prompt/Kyoto`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`$state` was observed by walking the realized tree for bindings, which makes it
a property of what RENDERED rather than of the fetch. A card that gates its rows
on the state it is waiting for is then a closed loop:

    when parks.$state == .pending { TextBody(text: copy.loading) }
    when parks.$state == .ready   { Panel { for p, i in parks … } }

no data -> pending -> the rows do not realize -> nothing binds `parks` -> no
status is written -> pending. Forever.

That is the shipped `activity` EXEMPLAR's shape, so every generated activity
card inherited it. Measured on the 6T with "what i can do in beijing tomorrow":
"Finding places nearby…" and nothing else, with `L0 $state:` logging empty on
every realize. The two apps that escape do so by accident — `weather-activity`
puts its `for` beside the pending guard rather than behind a ready one, and
`quake`'s gated feed shares a helper with an ungated lead.

`guarded_state_bindings` reports the sources a `when` reads the lifecycle of, so
a host can probe those directly. Only the gated ones: a source that is merely
displayed has its bindings in the tree already. Same fetch policy as
`guard_bindings` — the card's own `when`s say what must be answered before it
can decide what to draw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cond` has two consumers that need different readings of the same field. A
`TextRow` wants "Rain"; `WeatherIcon` wants the WMO code, and the kit says so in
as many words — "`cond` is a NUMBER — the WMO code the forecast returns". Both
lowered to `sys.weatherword`, so the icon was handed a string, coerced it to 0,
and drew the SUN.

Every icon, in every weather card, in every card that composes one. Measured on
the 6T over a Beijing forecast reading 92 % rain and AQI 146: a sun. It survived
because a wrong icon looks exactly like a right one, and because the test beside
it pinned `sys.weatherword` — the test agreed with the emitter and neither was
looking at a screen.

`vm_call` cannot tell the consumers apart, since a `SourceBinding` carries no
consumer, but the ROLE can and the kit lowering knows it. `icon_call` reads the
same forecast path `weatherword` does, deliberately: a card may show the word
beside the icon, and two readings of one sky that disagree is worse than either
being wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sys.photo` is a GENERATIVE image model, so `sys.photo(query: place.name)` asked
it to invent a Beijing — and it invented a tower that reads as Taipei 101, under
a title saying Beijing. A fabricated depiction of a real place, rendering as a
plausible photograph, with nothing on screen saying it was made up. Every weather
card has been doing this.

`query` is documented as a SCENE now, and `cond` is new: pass the forecast's
condition and the host folds it into the prompt, so a page reading 92 % rain is
not lit like a clear afternoon. The scene is the card's words — prose asserts
nothing — and the weather is the host's fact. Neither writes the other's half.

That exposed a second defect. `cond: now.cond` needs `now`, whose own `lat:`
names `place`, and `nested_source_call` resolved ONE level: it hit a path it
would not follow, fell to a scope lookup a live card cannot answer, and returned
None. That None propagated outward, so the whole photo binding failed and the
surface lowered to the realized em dash — the page drew NO wallpaper, and a card
with no image looks exactly like a card whose image has not loaded. It now
follows the chain, source-first like `source_binding`, capped at four so a cycle
in arguments cannot recurse away the stack.

Verified on the 6T. Before: `prompt/Beijing`. After:
`prompt/old%20city%20rooftops%20and%20grey-brick%20lanes...%2C%20drizzle%20weather`
— the model's scene, the host's live condition, and no place name anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`guarded_state_bindings` illustrated the deadlock with two indented lines of L0,
and rustdoc compiles an indented block as Rust — so `cargo test` gained a failing
doc-test the moment the doc was written. It hid because the suite is normally run
as `--test profile`, where doc-tests do not appear at all.

Fenced as `text`. Found by an agent auditing the slide deck against the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seen on the 6T: a weather-activity card whose eyebrow said TOMORROW over
`current.temperature_2m`. Nothing in the lowering could read daily.1, so only
the LABEL could claim tomorrow — and it did. The screen asserted the wrong day
and every stage was green: the same accepted-card class as the sun over 92 %
rain, except this time the card said the wrong thing in words.

`day:` is the capability that makes the label honest. Daily fields shift by the
named day, a forecast row's own index rides on top (day: 1 + row 2 = daily.3),
and the icon's weathercond call shifts identically — one sky, described once.

The refusal is the other half. A current-conditions field (temp, feels,
humidity, wind, pressure) under day > 0 gets NO translation: there is no
"current" tomorrow, and a visible absence beats today's reading served beneath
a label that says otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validated on the 6T with a simulated drive: 'directions to stanford university'
routed with from= empty, the card routed from the walking fix, the captured
origin froze — and the FROM row showed a BLANK. An empty origin is not unset;
it IS the trip's meaning, and the row now says so with complementary guards
(origin == "" → copy.cur_loc), the way every wayfinding surface does.

Fixture and exemplar changed together — the drift test fired on the first
half-edit, exactly as built to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nav card declares five sys.route trips, and exactly one — `trip`, whose
origin is the empty string on a from-here journey — is unanswerable BY DESIGN.
The host observed lifecycles per HELPER, worst state wins, stamped onto every
source naming it: so trip's eternal pending was written onto trip_here, which
had a real origin, a real destination and a 200 on the wire. "Finding a
route…" forever, Go never shown, zero failed fetches to point at. Measured on
a 6T mid-simulated-drive, and found only by logging what each probe ASKED:
the probe's own text read sys.searchnum("") — the wrong trip's question.

`source_state_bindings` resolves one probe binding per DECLARED source with
the same argument resolution the lowering uses. Two trips, two probes, never
one merged state.

With it validated on the 6T end to end: "give me directions to stanford
university" → from= left empty by the AMA → FROM row shows Current location →
trip_here resolves (4 min / 1.8 km) → Go → the drive screen counts down with
the simulated fix (1.7 → 1.6 → 1.4 km over 80 s of track). And the projection
behaves honestly off-route: a track that diverges from the OSRM line stalls
the instruction at the divergence corner while the camera keeps following the
real fix — rerouting is absent, not faked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add splash-media-host: an example host that registers image.generate
(OpenAI) and media.transcode (ffmpeg) as reviewed, policy-bound mod.tool
capabilities and drives them from an agent-authored workflow draft under
per-step capability leases. Proven end to end: no-lease run denies the
effect before any API call; both-lease run generates + edits a real image
with a full audit trail; opaque handles chain generate->edit via dataflow
so the script never sees a path, key, or binary.

The host preflights every generated step through check_syntax_named before
planning — the "check" half of generate->check->fix. Demonstrated catching
a JS-idiom draft (ternary, one-line block) with line-numbered diagnostics
and passing the corrected one.

Add docs/authoring-for-llms.md: a measured cheat card for LLMs trained on
JS/Python. Every rule verified against `splash check`/eval on the workflow
profile (block newline-termination, no ternary/spread/=>, module-fns vs
methods, + as string concat, etc.) — not inferred from the grammar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ovie API host

- splash-ui-l0: sys.wiki as a scalar source (SOURCES arg list + ANSWERS field
  catalog: title/extract/description). Its field-access lowering arm emits a
  self-contained Splash expression — sys.fetch(URL + query).parse_json().<field> —
  so tapping a movie card fetches its live Wikipedia summary; the retrieval logic
  runs as Splash, only the socket (sys.fetch) is Rust.
- splash-media-host: register net.imdb as a reviewed HTTPS origin-policy capability
  (feature http-endpoint-catalog); a Splash workflow draft calls IMDb's keyless
  suggestion API through it, least-privilege leased + audited. examples/
  movie_boxoffice_{draft,input}.json is that data-script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ics} out

The standalone realize() gate the benchmark harnesses shell out to: reads a
card path, realizes it against empty data, prints JSON. Differential model
soaks and the phone-card corpus builder both drive it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hero_points measured through valued_seeded, whose live-binding branch
(added for emission) returns the wire form — so a three-character '23°'
was measured as the ~120 characters of sys.weather(sys.geocodenum(…))
and every live hero fell to the ramp's bottom step: 17pt where the
fixture draws 43. The canonical weather fixture shipped this invisibly;
nothing was missing from the screen, only large.

Split the seeded tail out as seeded_body and route valued_literal (the
measurement path) straight to it. Emission is untouched: valued_seeded
still goes live for bound values.

Found by the mockup→DSL translation agent's differential probe
(format .compact → 43, live → 17) while reproducing a design mockup's
oversized numeral hero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
The mockup study priced its absence (pipeline-v2/REPORT.md): a city
eyebrow reads as 'T O K Y O' in every premium reference and no role
could say it. The card supplies only the words; caps and tracking are
the theme's, so the kit marks the node and the backend transforms a
literal — a live value stays as it arrives.

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

`theme <mood>` could only ever be four looks. A corpus of 100 generated
designs asked for square AND soft geometry, restrained AND vivid
palettes, grotesk AND serif display; a measured ablation then showed one
global setting helping the restrained schools and actively hurting
Organic and De Stijl. One coordinate is not enough.

So the theme line gains axes, parsed on the SAME LINE as the mood:

    theme light shape: .square accent: .amber density: .compact

Same-line is enforced against Token.line because the lexer discards
newlines; without it the next declaration's first identifier would be
eaten as an axis name. Every value is a token from a closed set —
accent, shape, density, emphasis, icons — so a card still states an
intent and never a colour or a length, exactly as `theme photo` always
has. An unknown axis or an unknown value is a diagnostic naming the
legal set, never a silent default.

card_theme_axes() joins card_theme() as the pre-realize reader a host
needs to choose a palette.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
The one axis a generator never learned to vary. Two corpus runs chose
.square on 89% then 76% of cards, against a prompt that said in as many
words not to unless the corners really are hard — while emphasis moved
from a 98% reflex to a genuine 50/48 split under the same rewrite.

The difference is vocabulary age. .square/.subtle/.soft/.pill are names
invented for this project; radius none/small/large/full is the scale
Tailwind and Radix both use, which a model has read a million times.
Borrow rather than invent, and let the measurement say whether fluency
was the constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98
* chore(media-host): rustfmt

`cargo fmt --check` is the first step of CI, so this one unformatted file has
been failing every job on the branch — including PRs that touch nothing near it.
Formatting only, no behaviour change.

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

* chore(ui-l0): rustfmt

Same reason as the previous commit: `cargo fmt --check` gates every other CI
step, and two crates were carrying formatting debt that failed jobs touching
neither. Formatting only.

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

* fix(l0): document sys.wiki, stop offering a field nothing answers, clear clippy

Three things standing between this branch and a green CI, none of them related
to each other beyond blocking every job.

`sys.wiki` was added to the Rust catalog without an entry in
`ui-l0-constructors.toml`, so the spec and the code disagreed about what sources
exist. Documented.

Its vocabulary offered `thumbnail`, and no lowering answered it: the page-summary
endpoint returns that as a nested object (`thumbnail.source`) while every
lowering here reads a single flat field. A card asking for `d.thumbnail` got an
em dash with nothing able to warn it — which is precisely the failure
`every_offered_field_has_a_translation` exists to catch. The field is withdrawn
rather than half-answered; adding it back needs a nested-path lowering first.

Two clippy warnings cleared, because CI runs it with `-D warnings` and fmt was
short-circuiting before it ever got there.

263 passed, 0 failed, 0 clippy warnings, 0 fmt diffs.

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

* chore: clear the clippy lints that ship with Rust 1.98

CI pins nothing — `dtolnay/rust-toolchain@stable` — so a new clippy release
fails the branch without a line of code changing. 1.98 added
`chunks_exact_to_as_chunks` and tightened `field_reassign_with_default`, and
`-D warnings` turns both into build failures.

Three `chunks_exact(N)` call sites become `as_chunks::<N>().0`, which is what
the compiler itself suggests and is semantically identical — both drop the
same remainder, and every one of these indexes into the chunk, which a
fixed-size array still supports. One `ExecutionLimits` built by assignment
becomes a struct literal with `..Default::default()`.

Verified against the toolchain CI actually uses, not the one that happened to
be installed locally — which is how this went unnoticed: 1.95 does not have
these lints, so the branch looked clean and CI did not.

  cargo +1.98 fmt --check                     0 diffs
  cargo +1.98 clippy --workspace -D warnings  clean
  cargo +1.98 test --workspace                1013 passed, 0 failed

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
An emoji is painted from the colour font with its colours baked in, so
`draw_text.color` never reaches it. A card that uses one as an icon renders
correctly under exactly the polarity its author had on screen, and is invisible
under the other — white plates on cream, in the case that surfaced it.

This cost real measurement time to find. Four separate judge verdicts said
"white icons vanish on cream" while an experiment assumed the palette was at
fault and spent two rounds tuning it; the icons were never icon widgets, they
were `TextRow(text: "🍽")`, and no palette can tint them.

`UiL0Report` gains a `lints` channel, separate from `diagnostics` on purpose: a
lint that could refuse a card would be a rule, and this is not a rule. The card
is valid and will realize. It is just not portable, and nothing else in the
pipeline can tell you so.

Swept over the 967-card corpus it flags 81 cards and 138 glyphs — mostly 🌳, ☕
and 🍽 standing in for icons — and refuses none of them. Latin, CJK, accents,
em dashes and ellipses are monochrome and carry no lint.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
43 corpus cards declare `theme vibrant` or `theme minimal` and none of them can
realize: the checker refuses an uncatalogued mood before the card reaches a
colour, and the vocabulary named four while the generator had been writing six.

That is a third of every card the L0 checker rejects — the single largest class,
found by sweeping the whole 967-card corpus rather than a sample of it. The
cards are not wrong; the catalog was behind them.

Palettes answering the two names land alongside this in splash-makepad.

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

Vendored copy kept byte-consistent with splash-lang/Splash for the catalog,
lower table, and constructor set (the app builds against this tree).

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>
ymote and others added 13 commits September 1, 2026 21:17
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… draws

`ui-l0-constructors.toml` is the declarative mirror of the Rust catalog, and
it had fallen behind it: `Card.tint`, `Space`, `Bubble`, `Fab`, `TabBar` and
`Tab` were drawable and un-declared. A card naming one passed realize() on the
strength of the Rust side alone, so the two could disagree without anything
failing — which is the one thing a mirror exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`valued()` prefers `value:` and only falls back to `text:`, so a node carrying
both dropped the label without saying so. The weather card states
`TextCaption(text: "最高", value: now.hi)` three times and rendered three bare
temperatures with nothing to tell them apart; the news card lost `points`,
`comments` and `by` the same way. On every rail, because the loss is in the
lowering.

A label beside a value is the value's leading decoration, in the same position
`glyph` occupies, so it goes through the decoration reader both the live and
seeded paths already share. Not on a HERO: prepended there, "card balance"
turned $235.00 into a two-line headline at 43pt and shrank the one figure the
screen exists to show.

Only a literal `text:`. Two live calls this side never resolves are not for
the lowering to join.

265 profile tests green; 122 corpus screens re-rendered, one moved and it
gained the labels its card had always stated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Work in progress carried by the Octoscript-AppCard l0-content-parity
branch: approval.rs, kit_pack.rs and value_origin.rs in splash-ui-l0,
the layers-review / live-news / nav-waypoint / origins-approval tests,
and the matching constructor catalog and L0 profile doc updates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eview-20260904

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/octoscript-ui-l0/Cargo.toml
#	crates/octoscript-ui-l0/src/lib.rs
@ymote
ymote merged commit 9cf5add into main Sep 13, 2026
4 of 8 checks passed
@ymote
ymote deleted the rename/fix-language-review-20260904 branch September 13, 2026 22:22
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