fix(plugin-list,plugin-view): deliver an authored map style to the renderer as mapStyle - #9999
Conversation
…Style`
`ObjectMapConfigSchema` declares `style`, and both view flatteners dropped it
before the renderer saw it: a view authoring `map: { style: '<url>' }` parsed
green and the map painted MapLibre's public demo tiles (objectui#9950).
Each flattener's whitelist becomes a TOTAL spelling table,
`FLAT_MAP_CONFIG_SPELLING`, mapping every key the declaration carries to the
name the internal flat form uses. Identity everywhere except `style`, which
travels as `mapStyle` — the spelling `getMapConfig` reads first
(`schema.mapStyle || schema.map?.style`) and a declared member of
`ObjectMapSchema`. The top-level `style` namespace stays the base face's
inline CSS, so the collision objectui#5177 closed stays closed, and an
undeclared key in the block still never reaches the product.
Both anti-drift pins were green while the key was dropped: each compared its
hand list against `Object.keys(ObjectMapConfigSchema.shape).filter((key) =>
key !== 'style')`, a comparison set narrowed by the same subtraction the
defect was made of. They now assert the relation "every declared key is
delivered under its flat spelling" against the declaration read whole, with a
control in the same body that feeds the pre-fix whitelist to that assertion
and shows it rejected. The table's `satisfies Record<keyof ObjectMapConfig,
string>` makes the coverage a typecheck failure as well.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
| event read 2026-09-19T09:12Z | |
|---|---|
| queued | 2026-09-19T08:05:27Z |
| ejected | 2026-09-19T08:26:16Z — removed_from_merge_queue with ⛔ no merged beside it |
| cause | the batch branch gh-readonly-queue/main/pr-10000-12b466ae5… (this PR + objectui#10000) failed Test (shard 1/4), job 105866539312 |
| the queue then | re-formed with objectui#10000 alone on 1ed2e69fa, which passed and merged |
This branch has since been updated onto current main (76f1543ab) and re-run alone at head
8defe49cb: shards 2, 3 and 4 pass, shard 1 fails again — on a different file and a different
divergence.
| run | failing assertion, both upstream-port-parity-wiring.test.ts:280 |
|---|---|
batch, job 105866539312 |
no listing line for **worktree-rule-card-ref** under **.claude/hooks/guard-main-checkout-bash.sh** |
this PR alone, job 105871105635 |
no listing line for **incident-first-person** under **.claude/hooks/guard-tree-enum.selftest.sh** |
Why it is not this PR's
- This PR's whole diff is five files under
packages/plugin-listandpackages/plugin-viewplus
one changeset. It touches ⛔ neither.claude/hooks/**norscripts/**. mainCI run35432852669on76f1543abiscompleted success— this same test passes there.- objectui#10000 alone and objectui#10001 both passed the same test on the same base.
- ⭐ The two failures name different (file, divergence) pairs. A real pin-vs-tree mismatch names
the same pair every time; two different pairs is the reading being unstable, ⛔ not the tree.
Root cause, located — ⛔ filed, not called a flake
scripts/check-upstream-port-parity.mjs:903-:912 runs process.exit(list()). With stdout on a
pipe — which is exactly how the test captures it, execFileSync('node', [GATE, '--list'], …) at
upstream-port-parity-wiring.test.ts:244 — Node's buffered stdout writes are not flushed by
process.exit, so the listing is truncated at a point that depends on runner load. list() at
:556 is a plain deterministic loop, so the gate's DECISION is stable and only its DELIVERY is not.
Full evidence and a suggested direction: objectui#10006. ⛔ Not this lane's surface to fix.
What this PR does next
Spending the one permitted re-run on the failed job. Green ⇒ re-queue. Red again on the same test
⇒ ⛔ no second re-run and ⛔ no blind re-queue: it goes back to objectui#10006 as a blocker.
⛔ No test is skipped, disabled or quarantined here, and nothing in this PR changes to route around it.
Generated by Claude Code
Fixes #9950
ObjectMapConfigSchemadeclaresstyle— "MapLibre style URL/spec (overrides the public demo default)" — and on the list-view and object-view paths it was dropped before the renderer ever saw it. A view authoringmap: { style: 'https://tiles.example.com/style.json' }parsed green, nothing refused it, nothing warned, and the map painted MapLibre's public demo tiles.Readings re-taken on this branch's base (
1ed2e69fa)packages/types/src/zod/objectql.zod.tsstyle: z.string().optional().describe('MapLibre style URL/spec (overrides the public demo default)')packages/types/src/zod/objectql.zod.tsObjectMapSchema.mapStyle— samedescribetext, verbatim; the top-level spellingpackages/plugin-list/src/ListView.tsxFLAT_MAP_CONFIG_KEYS, hand-listed, typed by anOmitofObjectMapConfigby'style'packages/plugin-view/src/ObjectView.tsxpackages/plugin-map/src/ObjectMap.tsxFLAT_MAP_CONFIG_KEYS— DERIVED fromObjectMapConfigSchema.shape, then.filter(key !== 'style')All four reproduce. The card said "both hand-listed flatteners"; there are three flatteners and the third one derives.
⭐ Why both pins were GREEN while the key was dropped
The dispatch turns on this, so here it is first-hand. Each pin derived its comparison set like this —
ListView.mapFlatten.test.tsx:155andObjectView.mapFlatten.test.tsx:190, identical text in both:declaredis not the declaration. It is the declaration with the same key subtracted from it that the whitelist was missing. The typeOmitofObjectMapConfigby'style'is the compile-time half of that subtraction and.filter((key) => key !== 'style')is the runtime half; the card named the type, and the.filterin the test is what actually kept the assertion green. A pin narrowed to match the bug reports "the list is correct" to the next reader, which is exactly what the card observed happening.⇒ this is not "a key was forgotten". Fixing the flattener without fixing the pin would have gone green again for the same wrong reason, and the next dropped key would have repeated the card.
The fix
Each flattener's whitelist becomes a total spelling table:
The
satisfiesclause is written with SQUARE brackets in this body on purpose: GitHub's body sanitizer eats angle-bracket-shaped fragments, backticks and fences included, so a literal type-argument list here would render as if nothing had been declared. The real source carries the ordinary type-argument brackets.pickFlatMapConfigwalks that table's entries and writessource[declaredName]underflatName.Why
mapStyleand notstyle. The reason the whitelist exists, in the source file's own words (objectui#5177): "styleis ALSOBaseSchema.style(inline CSS, legal on every node), and spreading the rawmapblock collapsed the two namespaces onto one key." Passingstylethrough unrenamed would re-open precisely that collision.ObjectMap.getMapConfig(ObjectMap.tsx:377) readsand deliberately reads no top-level
style(objectui#5017 —warnOnTopLevelStyleUrlexists to say so out loud).mapStyleis a declared member ofObjectMapSchema, so the flat product stays inside the declaration at both ends. Honouring the declaration and keeping the collision shut were never in tension — the old flattener just did neither for this key.Why not the derived shape
ObjectMap.tsx:138uses (the dispatch asks me to say why if I don't): bothListView.tsxandObjectView.tsxare reachable fromexamples/console-starter's ownsrc/, so they are in the import graphexamples/console-starter/test/vite-alias-closure.test.tswalks. That walker resolves a bare@object-ui/*specifier with plainindex.EXTconventions and cannot find@object-ui/types/zod's actual barrel (zod/index.zod.ts, a non-standard name) — a real runtime import there reproducibly fails that gate (PR #5231, CI run 32160288416).ObjectMap.tsxgets away with it only because nothing in console-starter's graph reaches@object-ui/plugin-map. I re-read the walker (resolveModule, which tries the target, then extensions, thenindex.EXT) and the constraint still holds, so the table stays hand-written — but it is now total, which the old list was not.The pin, rebuilt to measure a relation
Both suites now assert, over
Object.keys(ObjectMapConfigSchema.shape)read whole: every declared key is delivered by the flatten under its flat spelling. There is no literal key list left to narrow. The probe config is built by asking the declaration itself which value shapes each member accepts (safeParseagainst three candidates), so a key added later with an unfamiliar shape fails loudly instead of being skipped, and the config is asserted to parse green first — the card's own shape.Four controls travel in the same body:
key !== 'style'subtraction) is fed to the same assertion, and the undelivered set comes back as exactly['style']; the assertion itself is shown to throw.stylestill delivers it. That is how a whitelist drops one key silently.mapblock contributes (a fully authored block's product minus an empty block's product), asserted equal to the image of the spelling table: nothing undeclared added, nothing declared missing, both directions in one assertion.totallyUndeclaredKey,style2andmapStyle-written-inside-the-block are all confirmed dropped — the whitelist is keyed on declared source names, so onlystyleproduces themapStyleoutput.Coverage note:
cleanup()between the two renders in that last test is load-bearing. Without it the spy collects from both mounted trees and the baseline read is the other config's product — measured: the contributed set came back empty and the assertion silently compared a product to itself.Ablation — red leg and restore
Run from the committed fix (
c20cbd03d), with the mutation planted and restored byscripts/ablation-replace.mjs, which verifies on disk rather than by exit code.Mutation: delete the single line
style: 'mapStyle',from both tables. Anchor hit exactly once per file, and the blob moved:RED leg 1 — vitest, exit 1, 12 failures across the two files, including:
RED leg 2 —
type-check, exit 2. The table's type is total, so the missing entry is a compile error, not just a test failure:(the TS1360 text is quoted with square brackets in place of the type-argument brackets, same sanitizer reason as above)
Restore — proven by blob hash and an empty diff, ⛔ not by an exit code:
Re-checked in a separate shell afterwards:
git status --porcelainempty,git diff HEAD --statempty, and the anchor back at 1 occurrence in each file.Stop conditions, answered on the record
stylealready reaches the renderer on either path" — rejected. On the base, neither flattener emitsstyleormapStyle; the pre-fix probe above showsschema.mapStylearriving asundefined. The card's own carve-out stands: a hand-writtenobject-mapnode with amapblock does delivermap.style, becausegetMapConfigreads that block directly. The flattened path is the broken one, and only it is changed here.stylerequires editing the schema or the map renderer" — it does not.mapStyleis already declared (objectql.zod.ts:1666) and already read (ObjectMap.tsx:377). Both files stay read-only in this PR; the diff is four files plus the changeset.Omitby'style'is load-bearing for a reason the card does not know" — it is load-bearing, and the card half-knew: it asked "by what route, since it is not a flat field like the others". The reason is theBaseSchema.stylenamespace collision, and the answer is the rename, not a widened whitelist. The exclusion survives in substance — the flatten still never writes a top-levelstyle, pinned in both suites — it just no longer means "discard".Tests run
pnpm exec vitest run packages/plugin-list/src/__tests__/ListView.mapFlatten.test.tsx packages/plugin-view/src/__tests__/ObjectView.mapFlatten.test.tsx— 24 passed (12 on the base, before the new coverage).pnpm --filter @object-ui/plugin-list --filter @object-ui/plugin-view run type-check— both Done (each runstsc --noEmitplustsc -p tsconfig.test.json), after building the dependency closure.pnpm --filter '@object-ui/plugin-list^...' --filter '@object-ui/plugin-view^...' run build— exit 0.node scripts/check-changeset-presence.mjs,check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:shell-escape-residue— all exit 0.Acceptance notes
Two things measured beside this card, reported rather than ridden on. Both live in
packages/plugin-map/src/ObjectMap.tsx, which this dispatch declares read-only, so neither is touched here.warnOnTopLevelStyleUrl's remedy text points at a spelling that is silently dropped. The warning ends: "On a view, note thatoptions.mapis FLATTENED into the top level, so itsstylelands here as this same top-level key — spell itmap: { mapStyle }there." ButmapStyleis not a member ofObjectMapConfigSchema, so it is not in either flattener's whitelist (before or after this PR) andgetMapConfigreadsschema.map?.style, neverschema.map?.mapStyle. An author who follows that sentence writes a key nothing reads. After this PR the correct advice for a view is the plain declared spelling,map: { style: 'URL' }, which now works.ObjectMap.tsx:133-136states thatObjectView/ListView"derive their own flatten whitelist from this exact schema (imported from@object-ui/types/zod)". They do not and deliberately must not — see the alias-closure constraint above. Probe: occurrences of@object-ui/types/zodin either file, on this branch, is 0; the two files' own docblocks say the opposite ofObjectMap.tsx's claim.Authored by the
domain:ui#2execution seat in sessionsession_018HrVaotisyhgmot9o2MLRq(written into the prose as well as the footer, because aPATCHof this body would downgrade the footer's session reference).Generated by Claude Code