feat: build-time selection compiler (experimental)#69
Open
matej21 wants to merge 34 commits into
Open
Conversation
.mock() supplies deterministic values for scalar / .use() props used only during static selection analysis, never at runtime render. It fixes cases the generic tolerant stand-in mishandles: indexing a real object with a mocked key (crash + partial selection), branching on a scalar value (nondeterministic branch), and .map() over a scalar array (callback never runs, so entity fields accessed inside it are silently missed). The custom mock wins over the interfaces-mode branch in the collection propsProxy so a mocked scalar is not mistaken for an interface entity prop. Entity props cannot be mocked (compile-time error). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Deliverable A of the selection compiler: consume a compiler-emitted static selection instead of the runtime proxy collection pass. - staticSelectionToMeta(map) in packages/bindx/src/selection/ converts a StaticFieldMap into SelectionMeta by driving a SelectionScope, so output is indistinguishable from SelectionScope.toSelectionMeta() (alias/path/id-seeding). - .render(fn, staticSelection?) gains a compiler-facing 2nd arg, threaded into buildComponent. When present, ensureImplicitCollected() builds selectionsMap entries via the converter + createFragment and skips the proxy pass entirely (all-or-nothing for v1). - setStaticSelectionValidation(enabled) module-level flag: when on and a static selection exists, also run the proxy pass and deep-diff per prop, emitting one console.warn with the display name and a readable field-level diff on mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…imental) New private workspace package implementing deliverable B of the selection compiler plan: a pure analyzer core (analyzeSource) that proves a createComponent chain's implicit selection or bails with a machine-readable reason (default deny), plus a Babel plugin that injects the emitted StaticSelection as the 2nd argument of .render(). - Analyzer mirrors the runtime collector (SelectionScope + collector proxy): usage-derived relation-ness, auto-id on relations, skip-list ($/id/__), $fields/$entity transparency, full-body union over all branches + nested fns. - Recognized components: Field/Attribute/Show/HasOne/HasMany/If + cond DSL + .map(); HasMany literal params emitted (dropped by the oracle, so equivalence compares field trees only). - Bail taxonomy: interfaces, non-inline render/if fn, dynamic entity name, entity escaping to call/component, spread, computed member, non-literal HasMany param, entity reassignment, unclassified. - Equivalence harness validates every fixture against the runtime oracle (runtime = truth); ternary fixture asserts runtime ⊆ compiler. 39 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Rendering a raw ref as a JSX child (`{article.title}`) let React's
isValidElement probe `$$typeof` on the collector proxy, which delegated to
field access and upgraded the scalar to a bogus relation `{ id, $$typeof }`
(invalid GraphQL). Skip `$$typeof` and the `@@iterator` string fallback in
both collector proxy wrappers so probes return undefined instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Static-selection validate mode previously warned on any per-prop mismatch, including the two legitimate compiler-vs-runtime divergences: branch unions (compiler emits the superset) and has-many params/many-ness (the runtime never records them in implicit collection). Diff now reports only fields the runtime proxy selection requests that the static selection omits — the sole under-fetch bug class — keyed by field name so a params-driven alias is not read as missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…-rate measure Deliverable C of the selection compiler plan. - End-to-end test: transform a createComponent source with the Babel plugin, load the emitted module, and prove the static path — render fn skipped during collection, field renders under <Entity> via MockAdapter, validate mode silent. - Playground: wire the plugin into @vitejs/plugin-react's babel.plugins behind BINDX_COMPILER=1; enable validate mode in the dev entry when compiled. Off by default — build verified both ways. - measure script + `measure` package script: bail-rate over a .tsx tree (default packages/example). Reports per-chain results and a summary. - Docs: 'Compiled selections (experimental)' section in selection-collection.md. - Fold in typecheck refinements to the task-1/3 tests (HasMany over .map(), collector-level probe reproduction). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…lution
Reshape the compiled `.render(fn, compiled?)` argument into
`CompiledSelection { props, holes? }` (breaking change within the
experiment) and resolve nested-component holes at collection time.
- bindx: `driveSelectionScope(scope, map)` drives a caller-provided open
SelectionScope; `staticSelectionToMeta` becomes a thin wrapper over it.
- bindx-react: new `compiledSelection.ts` holds the `CompiledSelection`/
`CompiledHole` contract and `applyCompiledSelection` — seeds one live
scope per entity prop, then resolves each hole through the target's
runtime selection surface (getSelection, else staticRender + tolerant
scalar-prop fallback), replaying member paths via the source prop's
collector proxy. Per-hole error containment; validate-mode warn names
plain-React targets as selection blind spots. Host render never runs.
- Shared `createFragment`/`createScalarPropMock` extracted to
`collectionHelpers.ts`; `resolveComponentName` exported from analyzer.
- Export `CompiledSelection`/`CompiledHole` from the jsx layer + package
index for the compiler to code against.
- Migrate phase-1 tests to `{ props: {...} }` and add hole coverage
(createComponent + withCollector targets, entity-derived path, TDZ
thunk, multiple entityProps, plain-target blind spot, error
containment, render-never-runs, validate-mode no-warn).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…t holes
Phase 2 of the selection compiler. The 2nd argument of .render() changes to the
CompiledSelection shape { props, holes? }: `props` is the phase-1 per-entity-prop
StaticFieldMap, `holes` are statically-emitted references to nested components that
received entity-derived values (the Relay-fragment-spread equivalent), resolved at
collection time through the target's runtime selection surface.
ENTITY_ESCAPES_TO_COMPONENT no longer bails when the escape is a prop on a
component-typed element with a module-scope identifier tag. Multiple entity props on
one element merge into one hole; entity props become { source, path } (absolute from
the host entity prop, preserved through HasOne/HasMany/map callback roots via new
RootRef source/absPath tracking); statically-literal extra props are kept, non-literal
ones dropped; children stay statically analyzed. The emit is no longer pure JSON — each
hole's `component` is an arrow thunk referencing the tag (TDZ-safe, lazy).
Still bails: entity in a non-JSX call arg (ENTITY_ESCAPES_TO_CALL), entity in a
non-path expression prop (new ENTITY_IN_EXPRESSION_PROP), spread onto an element
(ENTITY_SPREAD), member/namespaced component tags (new MEMBER_COMPONENT_TAG), and
tag identifiers that don't resolve to a module binding (ENTITY_ESCAPES_TO_COMPONENT).
Same-element recognized bindx components are never holes (existing handling wins).
Measured on the reference app: 251/257 chains compile (98%, up from 84%), 99 holes
across 35 chains; the remaining 6 bails are genuinely unprovable.
Also: measure script reports per-chain hole counts and a summary (with holes / total
holes), and resolves its default target against the workspace root instead of cwd.
Cross-package equivalence for holes and the runtime-consumption e2e are integration-
gated on the v2 runtime consumer (skipped with an INTEGRATION marker).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…ections createRelationColumn's collectSelection called the renderer with a collector proxy and discarded the returned JSX, so nested declarative <HasMany>/<Field> inside relation-column renderers never registered their selections (the top-level proxy access only captured the immediate relation). the reference app worked around it with a .map() trick on the proxy instead of declarative JSX. Fix: walkRendererJsx now runs collectSelection on the renderer's returned JSX in addition to the proxy capture — in both the buildLeaf relatedSelection computation and the hasOne/hasMany cell configs. The JSX walk drives the collector proxy via HasMany.getSelection's map, registering nested fields into the parent scope, exactly as collectImplicitSelections does. Errors are contained per column. Benefits uncompiled apps too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…esolution; reference-app re-measure + docs Flesh out the previously-skipped hole-equivalence describe in holes.test.ts: for each hole-carrying fixture, transform with the Babel plugin, load the transformed module, and compare the COMPILED selection (holes resolved by applyCompiledSelection at collection time) against the ORACLE (untransformed module, runtime proxy pass resolves nested targets). All 8 fixtures agree exactly — createComponent/withCollector targets fold in their fields, plain targets are a shared blind spot (both register only the touched leaf). No divergences: both paths replay the same collector-proxy gets, so equality holds by construction. Extend endToEnd.test.tsx with a hole case: a host passing article.author to a nested createComponent target. Asserts (a) the host render fn is not executed during collection, (b) the nested target's field renders under <Entity> via MockAdapter (the hole put it in the fetch), (c) validate mode is silent. Re-measured the reference app: phase 2 with holes compiles 251/257 (98%, was 84% in phase 1); 35 chains carry 99 holes; 6 bails remain (5 ENTITY_IN_EXPRESSION_PROP, 1 ENTITY_REASSIGNMENT). Docs updated: selection-collection.md gains a holes section (incl. the plain-component shared blind spot + validate-mode warn); compiler-plan.md marks phase 2 and the dataview fix implemented with the numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…under-fetch)
A hole element's function props / render-prop children were dropped from the
emitted hole (non-literal), but a hole target's staticRender may INVOKE such a
closure during collection with a collector proxy — the reference app's SelectField does
`<HasOne field={props.field}>{e => props.children(e)}</HasOne>`. The runtime
oracle thus collected fields from the closure body (e.g. `it => <Field
field={it.name}/>` -> author.name) that the compiled path could not, so
compiled ⊂ runtime -> under-fetch -> UnfetchedFieldError in compiled apps.
This violated emit-or-bail (under-approximation must be impossible).
Fix: a dropped inline closure on a hole element is safe to omit IFF invoking it
cannot reach a selection scope — no reference to its OWN parameters
(transitively) and no captured entity roots. Otherwise the chain bails with the
new reason FUNCTION_PROP_ON_HOLE (default deny). Non-hole function children stay
statically walked (sound). New helper isHoleClosureSafe (resolve.ts) +
assertHoleClosuresSafe (jsx.ts); regression + safe fixtures.
the reference app (257 chains): 242/257 = 94% compiled (was an unsound 98%),
15 bails (9 FUNCTION_PROP_ON_HOLE, 5 ENTITY_IN_EXPRESSION_PROP, 1
ENTITY_REASSIGNMENT). Closure lifting to recover the rate is a phase-2.1
candidate (not implemented).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Adds `extraProps?: Record<string, () => unknown>` to CompiledHole — thunked (TDZ-safe) non-entity values the compiler lifts into a hole (module-scope bindings, render-scope-free closures). Hole resolution resolves each thunk and merges it into the assembled props before the entity proxies, so a target's staticRender that invokes one (`props.children(entity)`) collects the same fields the runtime oracle would — closing the identifier/closure under-fetch class by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
… extraProps
Phase 2.1: instead of dropping (and bailing on) a hole element's non-entity
props, lift what a target may invoke during collection into the hole's
extraProps, making compiled ≡ oracle by construction.
- Taint lattice (default deny) for identifier-valued hole props: module-scope
binding → lift; inert scalar/.use() render param → drop; render-local /
unresolvable → bail (new RENDER_LOCAL_ON_HOLE). Scope now tracks scalarParams
(safe-drop) and locals (bail; checked first so a shadowing local wins).
- Inline closure lifting: a function prop / render-prop child of a hole that
uses its own params but captures nothing from render scope is emitted verbatim
into extraProps (recovers most FUNCTION_PROP_ON_HOLE); param-less/root-free
closures still drop; entity-root / render-capture closures still bail.
- cond.* DSL in JSX props (`<Case if={cond.eq(x, 'v')}>`): record the FieldRef
args as touched leaves, drop the prop. Verified strictly equal to the runtime
Case/If getSelection oracle (a condition carries no selection beyond its args).
- JSX-element/fragment prop values (`draftSlot={<X/>}`): analyzed like children
(recurse), prop not emitted — an equal-or-superset union vs the oracle.
the reference app (257 chains): 94% → 99% (254/257); the 3 remaining bails
are all genuine (root-capturing handler, host-root-capturing closure, entity
reassignment). Docs updated (compiler-plan phase 2.1, selection-collection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Add itemOf/entityOf combinators and a CollectorContract object form to withCollector. A contract derives its staticRender automatically — one <HasMany>/<HasOne> per entry replaying the callback prop, guarded so a missing callback still registers the relation with no nested selection. The overload discriminates function (staticRender) vs object (contract) by a runtime typeof guard; the contract is also attached under the exported COLLECTOR_CONTRACT symbol for introspection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
… analysis
Phase 2.2 compiler side. A `withCollector(_, contract)` component declares how its
callback props are invoked (`itemOf`/`entityOf`); the analyzer now discovers that
contract and treats the callback exactly like a <HasMany>/<HasOne> child — no hole,
no lift, no FUNCTION_PROP_ON_HOLE/RENDER_LOCAL_ON_HOLE for contract targets.
Discovery (src/contracts.ts, ContractResolver + ContractFileCache):
- local `const Tag = withCollector(_, {...})` (TS wrappers unwrapped), or
- imported binding via a RELATIVE specifier (./x → x.tsx|ts|jsx|js / x/index.*,
ESM ./x.js → TS source) plus an optional `alias` prefix→path map; the sibling
module is parsed (no execution/type-checker) and cached by path+mtime.
Contract literals: object of `itemOf('…')`/`entityOf('…')` calls (combinators
imported from @contember/bindx*, string-literal args); a module-level const
identifier resolving to such a literal is accepted. Any deviation → no contract →
existing hole/bail rules (a fallback hole still resolves at runtime through the
derived staticRender, so soundness holds).
Threaded via analyzeProgram(program, { filename, alias, cache }) → BodyAnalyzer →
JsxAnalyzer as a ContractLookup; jsx.ts walkContractComponent replaces hole
formation for contract tags. parseProgram extracted to src/parse.ts to avoid an
analyze↔contracts cycle; resolve.unwrap exported for reuse.
Fixtures + oracle equivalence (strict): same-file + cross-file targets, footer-editor
replica (item callback capturing a host-root field), entityOf, dropped non-contract
fn prop, missing callback, and a negative unparseable-contract (spread) that falls
back to a FUNCTION_PROP_ON_HOLE bail.
Validated on a patched TEMP COPY of reference-app footer-editor.tsx + _shared.tsx (repo
unmodified): InitializingRepeater → { children: itemOf('field') } turns the
footer-editor L112 FUNCTION_PROP_ON_HOLE bail into OK (8/8). Full reference-app re-measure
unchanged at 254/257 (99%) since the app has not adopted contracts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Phase 3 runtime side: <Entity> (both by-mode and create-mode) gains a compiler-facing `compiledSelection?: CompiledSelection` prop. When present, the root SelectionMeta is built statically (fields under the fixed `entity` key + resolved holes) and children is never invoked with a collector. - Extract `resolveCompiledSelection` / `resolveCompiledRootSelection` from applyCompiledSelection (shared per-prop resolution, no fragments for the root). - New `useRootSelection` hook seams into <Entity> where useSelectionCollection was, deriving a stable queryKey from the compiled structure via the same buildQueryFromSelection hashing. - Validate mode also runs the (contained) children walk and applies the under-fetch-only diff, warning with the entity type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Phase 3 compiler side: statically prove <Entity> root selections and inject
compiledSelection={{ props: { entity: {...} }, holes: [...] }} onto the element,
so the runtime never invokes children with a collector (runtime side landed in
32d2927).
- New top-level scan (entityRoots.ts): find <Entity> elements whose tag resolves
to a @contember/bindx* import, anywhere in the file (routes, createComponent
bodies, any nesting). ImportBindings gains a separate `entity` set so a nested
<Entity> stays opaque to the host chain's per-chain walk.
- BodyAnalyzer.analyzeRootChildren: the children closure's first param IS the root,
so it is analyzed like a <HasOne>/<HasMany> callback (source 'entity'). All
existing machinery applies unchanged — paths, holes + extraProps, collector
contracts, cond-in-props, JSX props, branch union. Non-function children bail
ENTITY_NO_FUNCTION_CHILDREN; the runtime walk stays.
- Emit: entitySelectionAttr + plugin injection, idempotent (skips elements already
carrying compiledSelection). EntityRootResult / analyzeEntityRoots on public API;
measure reports entity roots separately.
- Tests: adapter-oracle equivalence (RecordingMockAdapter captures the QuerySpec;
transformed vs untransformed request the same root selection, superset for branch
unions) across scalars, nested has-one, compiled-component-in-closure, hole,
collector contract, branch union, create mode, Entity-in-component, bail; plus
children-not-invoked counter and emit/idempotence.
the reference app: chains unchanged 254/257; entity roots 84/105 compiled
(114 holes), 21 bailed (11 RENDER_LOCAL_ON_HOLE, 7 ENTITY_ESCAPES_TO_CALL,
2 ENTITY_NO_FUNCTION_CHILDREN, 1 FUNCTION_PROP_ON_HOLE).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Phase 3.1: classify a hole target so provably-inert props drop without bailing,
and surface entity roots hidden behind forwarding wrappers. Compiler-only — the
entityLike attribute rides the wrapper's {...props} spread into the real <Entity>,
which already consumes compiledSelection (no runtime change).
Part A — target-kind classification (src/targetKind.ts). Shared binding resolution
(local + relative-import + parse cache) extracted to src/moduleResolve.ts and taught
to resolve plain function/class declarations; ContractResolver refactored onto it.
Classify a hole tag into createComponent / plain / collectorStatic / unknown
(collector contracts still handled first, separately):
- createComponent → getSelection never reads scalars nor invokes function slots →
render-locals + function props/children drop, no bail; slots from .slots([...]).
- plain → no surface → every non-entity prop drops; hole still emitted.
- collectorStatic → parse the staticRender's referenced prop-name set (destructured
keys / props.x; rest/spread/aliasing → "all"); unreferenced dropped props are safe,
referenced ones keep the existing bails.
- unknown → conservative lattice unchanged (default deny).
holePolicyFor() feeds the decision into resolveHoleExtraProps.
Part B — entityLike roots. analyzeSource/analyzeProgram/entity-root scan/Babel plugin
gain entityLike?: string[]; matching prefers an import's original exported name over
its alias, else the local declaration name. measure gains --entity-like=Name,....
the reference app: chains unchanged 254/257; entity roots 84 -> 93/105 from
classification, and --entity-like=RefreshableEntity surfaces 35 hidden roots
(105 -> 140, 124 compiled). Fixtures + adapter-oracle equivalence in targetKinds.test.tsx.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…ution
BindingResolver now follows from-source re-exports — export { X } from,
export { X as Y } from, and export * from (barrel index files) — so a
createComponent/contract target hidden behind a barrel is classified instead of
conservatively bailing. The chase is depth-limited (5 hops) and cycle-guarded
(visited set); a star ambiguity or an exhausted budget stays unfollowable, so the
conservative hole/bail rules are preserved. Applies uniformly to both target-kind
classification and contract discovery, and follows aliased entry specifiers too.
On the reference app this clears the genuine barrel bail (Organization360Meetings
→ MeetingRecordCreateBody re-exported through a MeetingRecordDetail/index.ts barrel).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Add a first-class Vite plugin (bindxCompiler) that runs the selection compiler with enforce:'pre' and registers every cross-file module the analyzer consults via addWatchFile. The analyzer now threads an onDependency(absPath) callback from AnalyzeOptions down through BindingResolver (entry import + every re-export hop, incl. parse failures) so both contract discovery and target-kind classification report their reads. This stops a stale injected literal (from an un-retransformed file A after contract/target file B changes) from flipping into an under-fetch in dev. Wire the example under the existing BINDX_COMPILER=1 gate to use the plugin before react() instead of injecting the babel plugin into @vitejs/plugin-react. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Emit `v: 2` as the first property of the CompiledSelection object literal (both the chain second-arg and the <Entity> compiledSelection attribute, via selectionToAst). The runtime rejects any literal without `v === 2` and falls back to the proxy pass, so a stale/corrupt emit can never under-fetch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…killswitch Harden the runtime consumers of compiler-emitted selections so a malformed or throwing literal degrades to the runtime proxy pass instead of crashing or silently under-fetching (soundness: over-fetch OK, under-fetch never): - CompiledSelection now requires `v: 2`; add exported guard isValidCompiledSelection (object, v===2, props a plain object of objects, holes an array if present). - componentFactory: on an invalid literal OR a top-level throw from applyCompiledSelection, warn once with attribution and fall back to collectImplicitSelections (partial compiled entries cleared first). Per-hole containment is unchanged. - useRootSelection: validate + try/catch inside the memo; on failure warn and fall back to the children-collector walk, keeping hook order stable. - compiledSelection: move assembleHoleProps inside the per-hole try so a throwing extraProps thunk (TDZ/module-init) degrades only that hole. - Killswitch setCompiledSelectionsEnabled(false) (exported) makes both consumers ignore compiled literals — incident mitigation without a rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…dening
Contain unexpected (non-BailError) crashes per unit as a new INTERNAL_ERROR
bail so the sound runtime proxy pass takes over instead of failing the build;
the plugin also wraps discovery and each emit (one bad injection never loses
the others). Add a diagnostics option ('off'|'summary'|'verbose') threaded
through the babel and Vite plugins, with a single reporter deciding all output
and a buildEnd grand total. Deep-clone expressions copied into hole extraProps
so no AST node sits at two tree positions. Hoist each compiled <Entity> literal
to a module-scope const for stable identity across parent renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…p tests Cover per-chain crash containment (analysis wrapper + plugin-level via a spyOn'd BodyAnalyzer that throws only for the crashing chain), the diagnostics modes, and Entity literal hoisting/idempotency. Add a shared-AST interop test (compiler pass -> @babel/plugin-transform-react-jsx over the same nodes) plus a direct node-identity assertion proving extraProps is deep-cloned. Adds @babel/plugin-transform-react-jsx as a devDependency for the interop pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
…c API - imports.ts: resolve component kinds via a ReadonlyMap (no `as ComponentKind`) - astWalk.ts: add 'skip'/'stop' WalkControl, cast-free child access via Reflect.get and `'type' in value` node guard; fold the hand-rolled visitors in resolve.ts (anyIdentifier) and targetKind.ts (referencedMembersOf) onto walkAst - contracts.ts: import CallbackContract/CollectorContract from @contember/bindx-react (single source of truth) and drop the dead ContractFileCache alias; add a bindx-react project reference so tsc --build resolves the type import - index.ts: trim to bindxCompilerPlugin/bindxCompiler, analyze* + is*Bailed guards and public result/option types; tests import internals from their source modules - vite plugin: create one ModuleCache per instance and thread it through the babel plugin (BindxCompilerOptions.cache); singleton stays the bare-plugin default Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
- optionsPlumbing.test.ts: assert alias + entityLike take effect through the babel plugins-with-options tuple (emit only happens when the option is present) - plugin.test.ts: a hand-written `.render(fn, literal)` second arg is preserved and not injected over; a non-inline `.render(ref)` arg bails EXPLICIT_RENDER_FN Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
matej21
force-pushed
the
experiment/selection-compiler
branch
from
July 22, 2026 13:11
96251e4 to
72ebd7e
Compare
…povers The tests waited only for ANY button in the popover, so on a slow runner the click could hit the stale pre-filter option list (or a remounting node) — picking the wrong author / losing the click, then timing out on the save-button wait. Wait until the first option shows the filtered text instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
Filtered-option waits didn't fix CI: the click can land on an option node mid-remount (debounced fetch re-renders the list) and get silently lost. clickUntil re-clicks until the expected outcome materializes, checking the condition first so a registered click is never repeated (no multi-select toggle-off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG
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.
Build-time compiler that statically extracts field selections from
createComponent()chains and<Entity>roots and injects them as literals — the runtime proxy pass becomes a fallback instead of the hot path. Stacked on #62.Design
setStaticSelectionValidation) dual-runs in dev and warns on under-fetch only.What it compiles
.render(fn, compiledSelection)second arg; render fn no longer executes during collection.withCollector(runtime, { children: itemOf('field') })replaces hand-written staticRender; compiler discovers contracts cross-module (incl. barrel re-exports, MAX_HOPS=5).compiledSelectionprop injected on<Entity>;entityLikeoption covers forwarding wrappers. Hole targets classified cross-module (createComponent / plain / collectorStatic / contract) to decide what's safely droppable.Real-world numbers
With
~alias +entityLike: ['RefreshableEntity']: chains 254/257 (99 %), Entity roots 128/140 (91 %). Every remaining production bail is a named, verified-sound case (genuine dynamism or a staticRender that truly reads a dropped prop — fixable app-side via contract migration).Prod hardening
bindxCompiler(options)(enforce: 'pre') with cross-module watch invalidation — every file the analyzer touches is registered viaaddWatchFile, so editing a contract/target module re-transforms its dependents (no stale literals in dev).INTERNAL_ERRORbails per chain/root; a compiler bug can never fail the build.v: 2; malformed/mismatched literals warn and fall back to the proxy pass (never an empty selection, never a render crash). Per-hole error containment covers throwing extraProps thunks.setCompiledSelectionsEnabled(false)disables compiled selections at runtime without a rebuild.diagnostics: 'off' | 'summary' | 'verbose'plugin option; bail codes with file:line, per-build totals.cloneNodefor lifted closures, Entity literal hoisted to module const for stable identity), zeroascasts, trimmed public API.Also fixes two pre-existing runtime bugs found by the oracle: React's
$$typeofprobe polluting selections, andDataGridHasOneColumndiscarding renderer JSX.Testing
162 compiler tests (oracle equivalence, holes, contracts, entity roots, re-exports, crash containment, pipeline interop with
@babel/plugin-transform-react-jsx, idempotency, options plumbing) + runtime robustness tests; full suite 1828 pass with only the 10 known pre-existing failures. Spec and phase history indocs/compiler-plan.md.Not yet done: live app trial (compiled build running the real app) — recommended before merging out of experimental.
🤖 Generated with Claude Code
https://claude.ai/code/session_013zh1EN87d1Q7urtu7sDRYG