Skip to content

feat: build-time selection compiler (experimental)#69

Open
matej21 wants to merge 34 commits into
feat/use-api-static-collectionfrom
experiment/selection-compiler
Open

feat: build-time selection compiler (experimental)#69
matej21 wants to merge 34 commits into
feat/use-api-static-collectionfrom
experiment/selection-compiler

Conversation

@matej21

@matej21 matej21 commented Jul 22, 2026

Copy link
Copy Markdown
Member

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

  • Replaces only the per-component proxy-execution pass. Cross-component composition (getSelection walk, slots, fragments) stays at runtime, so the transform is purely local per file — no type checker, no whole-program analysis.
  • Progressive enhancement, emit-or-bail. Every chain/root either compiles or bails with a machine-readable reason code (16 codes, default deny) and falls back to the runtime collector. Over-approximation (extra fields) is allowed; under-approximation is impossible by construction — every drop/lift decision is proven safe or it bails.
  • Runtime collector as oracle. The test harness compares compiled output against proxy-collected selections; validate mode (setStaticSelectionValidation) dual-runs in dev and warns on under-fetch only.

What it compiles

  • Chains.render(fn, compiledSelection) second arg; render fn no longer executes during collection.
  • Holes — entity values passed to nested components resolve via the target's selection surface at runtime (path replay), with extraProps lifting for module-scope values and non-capturing closures (taint lattice, default deny).
  • Collector contractswithCollector(runtime, { children: itemOf('field') }) replaces hand-written staticRender; compiler discovers contracts cross-module (incl. barrel re-exports, MAX_HOPS=5).
  • Entity rootscompiledSelection prop injected on <Entity>; entityLike option 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

  • Vite plugin bindxCompiler(options) (enforce: 'pre') with cross-module watch invalidation — every file the analyzer touches is registered via addWatchFile, so editing a contract/target module re-transforms its dependents (no stale literals in dev).
  • Crash containment — internal compiler errors become INTERNAL_ERROR bails per chain/root; a compiler bug can never fail the build.
  • Versioned literals + runtime fallback — emitted selections carry 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.
  • KillswitchsetCompiledSelectionsEnabled(false) disables compiled selections at runtime without a rebuild.
  • Diagnosticsdiagnostics: 'off' | 'summary' | 'verbose' plugin option; bail codes with file:line, per-build totals.
  • Emit hardening (AST cloneNode for lifted closures, Entity literal hoisted to module const for stable identity), zero as casts, trimmed public API.

Also fixes two pre-existing runtime bugs found by the oracle: React's $$typeof probe polluting selections, and DataGridHasOneColumn discarding 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 in docs/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

matej21 and others added 30 commits July 21, 2026 14:05
.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
…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
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
matej21 and others added 2 commits July 22, 2026 14:24
- 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
matej21 force-pushed the experiment/selection-compiler branch from 96251e4 to 72ebd7e Compare July 22, 2026 13:11
matej21 and others added 2 commits July 22, 2026 15:27
…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
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