Feature: Portable UI IR#89
Merged
Merged
Conversation
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]>
✅ Deploy Preview for coasys-we ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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.
The backend-neutral query layer (QueryIR)
Summary
Introduces WE's neutral query IR — the specified, versioned query language a template's
$queryresolves 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
QueryIRgrammar (+ Zod), validation against aModelManifest, adapter-capabilityplanning (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 recursiveFiltertree, orderedsort,offset/cursor
page, recursiveinclude, first-classaggregate,live) + a recursive Zod schemaand
validateQueryIR(structural).queryValidation.ts—validateQueryAgainstManifest: 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.ts—AdapterCapabilities+planQuery: classifies each feature a queryuses as
native/compute-up(JS fallback) /unsupported(hard fail — cursor on a cursor-lessadapter, live on a no-change-feed backend). The mechanism behind the L0–L4 tiers.
legacyQuery.ts—translateLegacyQuery: maps the current AD4M-flavored$query→QueryIR, and returns anunsupportedlist surfacing the shapes that need a design decision beforethe main-app switch (
parent, single/filtered$-projections).queryEngine.ts—executeQueryIR: runs aQueryIRover 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 therenderer's legacy opts →
QueryIR→executeQueryIR. This is what makes the harness prove thechain live without touching the renderer.
package.json— add@we/schema-sharedas a direct dep.Known follow-ups
templates — not for internal cleanliness alone): resolve the two shim gaps —
parent→ a filtervia the manifest's
reverseOf; single/filtered$-projections → anasalias onIncludeSpec—then swap
createQuerySignalto consumeQueryIRwith a lossless AD4M round-trip, and browser-verify the main app.
neutrality proof; the grant deliverable).
buildModelManifest→ the neutralModelManifest.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 templatevite buildis green with zero@coasys/ad4min itsdependency graph.
dist/is gitignored and regenerated bypnpm build; the harness imports@we/schema-shared's dist, so a build must precede running/testing the harness (normal monorepoflow — CI builds packages before test).