Skip to content

Feature: Portable UI IR#89

Merged
jhweir merged 9 commits into
devfrom
feat/portable-ui-ir
Jul 17, 2026
Merged

Feature: Portable UI IR#89
jhweir merged 9 commits into
devfrom
feat/portable-ui-ir

Conversation

@jhweir

@jhweir jhweir commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

The backend-neutral query layer (QueryIR)

Summary

Introduces WE's neutral query IR — the specified, versioned query language a template's $query
resolves to and a backend adapter compiles from — and proves it works end-to-end over a non-AD4M
backend
. It's the substance of the portable-UI thesis at the data layer: the same template renders
through a backend that has nothing to do with AD4M.

The layer is: a QueryIR grammar (+ Zod), validation against a ModelManifest, adapter-capability
planning (native / compute-up / fail-loud), a reference execution engine, and a back-compat shim from
the current $query. The harness's live renders now route through it ($query → shim → QueryIR → engine → render) over the in-memory backend — with zero changes to the shared renderer.

Additive and non-breaking. Nothing in the main app consumes the IR yet and the renderer is
unchanged; the main app is untouched. Wiring the IR into the main app (with a lossless AD4M
round-trip) is deliberately a separate, later feature — see Known follow-ups.

Changes (@we/schema-shared)

  • queryIR.ts — the IR: QueryIR (entity, uniform recursive Filter tree, ordered sort,
    offset/cursor page, recursive include, first-class aggregate, live) + a recursive Zod schema
    and validateQueryIR (structural).
  • queryValidation.tsvalidateQueryAgainstManifest: cross-checks entity/property/relation/
    aggregate names against a manifest (fields are real props, includes are real relations validated
    against the target entity, sort resolves, aggregates target relations, no alias-shadow), so bad
    queries fail loudly at author time.
  • queryCapabilities.tsAdapterCapabilities + planQuery: classifies each feature a query
    uses as native / compute-up (JS fallback) / unsupported (hard fail — cursor on a cursor-less
    adapter, live on a no-change-feed backend). The mechanism behind the L0–L4 tiers.
  • legacyQuery.tstranslateLegacyQuery: maps the current AD4M-flavored $query
    QueryIR, and returns an unsupported list surfacing the shapes that need a design decision before
    the main-app switch (parent, single/filtered $-projections).
  • queryEngine.tsexecuteQueryIR: runs a QueryIR over in-memory rows (filter tree +
    relation filters, multi-key sort + aggregate-alias + to-one relation paths, offset paging, recursive
    include + first-unwrap, count/sum/min/max/avg, scalar-projection select). The compute-up toolkit;
    proves the IR is executable. (Cursor paging is intentionally out — adapter-specific, can't be minted
    in JS.)

Changes (harness @we/playground-portable-slice)

  • src/inMemoryBackend.ts — rewired as a thin adapter over the shared engine: translates the
    renderer's legacy opts → QueryIRexecuteQueryIR. This is what makes the harness prove the
    chain live without touching the renderer.
  • package.json — add @we/schema-shared as a direct dep.

Known follow-ups

  • Main-app wiring (Path B), when justified (e.g. a second live backend, or a need to migrate WE's
    templates — not for internal cleanliness alone): resolve the two shim gaps — parent → a filter
    via the manifest's reverseOf; single/filtered $-projections → an as alias on IncludeSpec
    then swap createQuerySignal to consume QueryIR with a lossless AD4M round-trip, and browser-
    verify the main app.
  • A real NextGraph adapter — the IR compiled to a genuinely different engine (the ultimate
    neutrality proof; the grant deliverable).
  • Remaining Phase-2 bits: the vocab rename; a converter from AD4M buildModelManifest → the neutral
    ModelManifest.

Test plan

  • @we/schema-shared: 415 tests pass — IR structural validation, manifest-aware validation,
    capability planning, the legacy shim, and the execution engine (filter/sort/aggregate/include/
    paging/relation-filter/select).
  • @we/playground-portable-slice: 5 tests pass through the live IR path (the real feed template
    • isolation diagnostics), and the harness vite build is green with zero @coasys/ad4m in its
      dependency graph.
  • Note: package dist/ is gitignored and regenerated by pnpm build; the harness imports
    @we/schema-shared's dist, so a build must precede running/testing the harness (normal monorepo
    flow — CI builds packages before test).

jhweir and others added 9 commits July 16, 2026 14:39
The versioned, specified query IR a template's $query resolves to and a backend adapter compiles
from — replacing the opaque AD4M-flavored pass-through.

- QueryIR (entity, filter, sort, page, select, include, aggregate, live) with a uniform recursive
  filter tree (field-compare leaves + and/or/not + relation-scoped), ordered sort keys, first-class
  aggregates (referenceable in sort), recursive include, offset/cursor paging.
- Zod schema (recursive via z.lazy) + validateQueryIR() for structural validation.
- Proven against the spec's worked-example feed plus rejection cases.

Structural only for now; cross-checking entity/field/relation names against a ModelManifest, adapter
capabilities, and wiring the renderer to emit/consume it are follow-up increments.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Cross-check every entity/property/relation/aggregate name a query references against the manifest,
so a bad query fails loudly at author time instead of silently returning nothing at runtime.

validateQueryAgainstManifest walks the query recursively: filter fields must be real properties
(relations are filtered via `rel`); includes must be real relations and their nested specs validate
against the TARGET entity; sort keys must resolve to a property, an aggregate alias, or a to-one
relation path (to-many paths rejected); aggregates must target a relation, require a field for
sum/min/max/avg, and their alias must not shadow a real property.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
An adapter declares what it does natively (operators, boolean combinators, relation filters, include
+ maxDepth, aggregate fns, sort kinds, pagination, live). planQuery classifies each feature a query
uses as native / compute-up (derivable in JS by the fallback toolkit) / unsupported (hard fail).

Most gaps are compute-up (a minimal adapter still runs a rich feed via JS fallbacks); the genuine
hard-fails are the unfakeable ones — cursor pagination on a cursor-less adapter, and a live query on
a backend with no change feed. runnable = no unsupported gaps. This is the mechanism behind the
L0–L4 tiers and "fail loudly instead of silently returning nothing"; the compute-up toolkit itself
is separate.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e yet)

The back-compat translator for the wiring step: maps the current AD4M-flavored $query
(model/where/order/limit/offset/include/subscribe) to QueryIR, so existing templates keep their
syntax. where → the Filter tree (eq/ne/nin/contains/exists + OR/AND/NOT, siblings implicit-AND),
order → sort, count-projections → top-level aggregates (alias keeps the $ so `$item.$x` reads still
work), subscribe → live, perspective dropped.

Returns { ir, unsupported } — and the unsupported list is the point: it surfaces the shapes that
DON'T map losslessly and need a design decision before the live switch — `parent` (needs a
reverse-relation mapping) and single/filtered `$`-projections (need include-aliasing in the IR).
Purely additive; nothing consumes this yet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
executeQueryIR runs a QueryIR over plain in-memory rows the way a backend without a native engine
would — filter tree (incl. relation filters), sort (multi-key, aggregate aliases, to-one relation
paths, nulls), offset/limit paging, recursive include (to-one/to-many, per-include filter/sort/page,
first-unwrap), and aggregates (count/sum/min/max/avg over a filtered related set). Pure + framework-
agnostic; operates on tables + a relation map.

Proves the IR is executable end-to-end, and is the fallback a weak adapter uses for features it can't
push down. Path A: this is what lets the harness run template → IR → engine → render over a non-AD4M
backend with zero shared-renderer changes (wiring next).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…oof)

Rewire the in-memory backend as a thin adapter over the shared engine: it translates the renderer's
legacy query opts → QueryIR (translateLegacyQuery) and executes via executeQueryIR. So the harness now
proves the full chain live — template $query → shim → QueryIR → engine → render — over a non-AD4M
backend, with ZERO changes to the shared renderer (no main-app risk).

All 5 harness tests pass through the new path; build green; zero @coasys/ad4m in the dep tree.
(Also folds in the linter's reformat of queryEngine.ts.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…comments

Describe what does/doesn't map losslessly, not the migration roadmap ("the wiring step", "before the
live switch") — those would orphan since the plan docs live outside the repo.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Format the query-IR source to the repo's prettier config and add the standard eslint-disable header
to the engine test (result rows are Record<string, unknown>; tests cast to read hydrated relations).
Lint now clean under --max-warnings 0. No logic change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
executeQueryIR now honours select — keeps id + the selected scalar props, preserving derived fields
(aggregate aliases + included relations, which select never strips). The reference engine now matches
the IR spec it validates. Cursor paging stays intentionally out (adapter-specific; can't be minted in
JS) — documented inline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for coasys-we ready!

Name Link
🔨 Latest commit 278667d
🔍 Latest deploy log https://app.netlify.com/projects/coasys-we/deploys/6a5a37dfcc4d4b0008226920
😎 Deploy Preview https://deploy-preview-89--coasys-we.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@jhweir
jhweir merged commit 95cb6b5 into dev Jul 17, 2026
3 of 5 checks passed
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