feat(bindings): the translation contract — every target accounts for the full vocabulary - #57
Merged
Merged
Conversation
…the vocabulary The bindings package now exports the contract types: HandlerKey/AttrKey (derived from EventBindings/AttrBindings) and HandlerTargets/AttrTargets — Records over the closed vocabulary where every key names the host prop that carries it, or null: a declared drop for a binding the substrate cannot express. Every target's normalize declares its maps against the contract, so a new vocabulary key is a compile error in every target until it decides — mapped or dropped, never silently leaked through the unknown-key passthrough (which remains, but only for keys outside the vocabulary). No behavior change: the ad-hoc HANDLER_DROP/ATTR_DROP sets fold into null entries, native's accessibilityState fold derives its key set from the ledger, and the two implicit passthroughs (native role, opentui disabled) become explicit renames. A conformance test per target walks its ledger and asserts every binding lands on its declared target — or, for a null, nowhere at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…Targets The string-indexable view each normalize loop reads through was declared inline in every target; the pair now lives in bindings next to the ledger types it widens, and the consts carry the ANY_ prefix so they can't be mistaken for the ledgers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exported map is both things at once: annotated AnyHandlerTargets / AnyAttrTargets (string-indexable, so the loop reads it directly) and `satisfies` the contract (so the literal stays exhaustive and excess-checked). The separate widened view consts disappear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d base A target that can't express most of the vocabulary spreads the base and overrides what it does carry, instead of writing a wall of nulls. The drop-by-default decision for future vocabulary keys moves to the base, declared once next to the vocabulary; `satisfies` keeps the overrides typo-checked. Native and opentui shed ~70 null lines; react stays fully explicit. Bindings becomes a runtime dependency of the spreading targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… form Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ontract types HandlerTargets/AttrTargets are now the mapped vocabulary keys intersected with a string index, so a ledger needs a single annotation: exhaustiveness from the mapped keys, loop lookup from the index. The Any* types and the satisfies clauses disappear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he loop read HandlerTargets/AttrTargets drop the string index: ledger literals get full excess-property checking (a typo'd key is a compile error again), and each normalize loop widens at its read site with a localized `as Record<string, string | null | undefined>` cast — a miss is `undefined`, outside the vocabulary, passes through. Along the way: react's loop gains the null-drop guards its siblings already had (unreachable today — react maps the full vocabulary — but the contract's null half now works the day react declares one), and native derives A11Y_VALUE_KEYS from the ledger like A11Y_STATE_KEYS so the fold can't drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ture The identical describe block triplicated across the targets moves to tests/fixtures/ in the bindings package (the contract's home); each target invokes it with its own normalize + ledgers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mechanism lives on HandlerTargets/AttrTargets in the bindings package and each file's header; the per-ledger restatements were noise. OpenTUI's onFocus/onBlur drop rationale moves into its header — the one fact that lived only in the deleted block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every comment this branch added gets cut to its load-bearing facts; the mechanics restated from the contract types are gone. A11Y_VALUE_KEYS goes back to the plain literal — the ledger-derived prefix-strip was clever but unreadable; the sync constraint is now one comment line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The vocabulary (
EventBindings+AttrBindings, 60 keys) is a closed set, but each target's normalize restates it as untyped data —Record<string, string>maps plus ad-hocHANDLER_DROP/ATTR_DROPsets. Nothing checks coverage: add a vocabulary key and every target still compiles, with the new binding silently falling through the unknown-key passthrough onto the host (onWheellanding verbatim on a RNView). The native normalizer even states the rule in a comment ("every vocabulary key must be accounted for — an unlisted key would leak") — but only discipline enforced it, and #54 shows this failure class is real.The contract
shared/bindingsowns the vocabulary, so it now also owns the translation shape — types only, nothing added at runtime:Every target's maps are annotated against it.
Record, notPartial: every key is required, excess keys are rejected, andnullis a declared drop — the substrate says "I cannot express this" in a reviewable diff instead of a hand-maintained set:A new vocabulary key now breaks all three targets' typecheck until each decides — mapped or dropped, never leaked. The passthrough survives, but only for keys outside the vocabulary (
data-state, consumer extras).No behavior change
HANDLER_DROP/ATTR_DROPsets fold intonullentries (native, opentui).accessibilityStatefold now derives its key set from the ledger, so routing can't drift from the declaration; theaccessibilityValuesub-key table stays.role, opentuidisabled— become explicit renames with the same output.devDependencies+import typeonly: verified erased from every target'sdist(js and d.ts).Verification
null, produces nothing at all (the anti-leak assertion, with the offending key in the failure message).Follow-ups
DOM_EVENT/DOM_ATTRSauthority + shared payload adapters, with react/vue keeping only their listener-spelling layer.🤖 Generated with Claude Code