Repository navigation
json-rules 3.4.0: relations off by default, turned on by the relation object; context removed; lensVisit - #26
Merged
Merged
Conversation
Relations
- A relation is crossed only where the first narrowing over the base lens turns it on, along
the path (root.relations) or at the model default (mapDefaults…models.M.relations, nested
objects included). exposed1 = turn-on1 and not hide1; exposedk = exposed(k-1) and not hidek.
Later layers hide with omits or restate a shown relation to narrow that hop; naming one the
parent doesn't show is not_visible. picks names columns only (a relation in it is wrong_kind).
- A model-default relation crosses each edge Model.relation once per path; deeper recursion is
spelled under root.relations. One walk (resolveVisit) serves every posture: the gate,
walkLensPath, readLensValue, projections, sources, fetch and validateNarrowing.
- The first narrowing's grants read the schema; a later layer's only what its parent shows.
- toLensSelect / projectRows open exactly what is turned on plus grant columns; shallow fetch
and the rules option are cut; a column-less relation is selected by its key alone. A source
keyed on a relation, or a bare label naming one, is wrong_kind.
Context
- options.context is removed from check / toSql / toPrisma; caller values are binds. A bare
path is a root-row column on every rail, gated and narrowed like a field. toSql compiles a
column; toPrisma a field reference ({ __field }, resolved by executePrismaPlan) between two
columns of the same model, visit and type with equality / ordered operators and
IS [NOT] DISTINCT FROM null semantics; anything else throws, and validateRule / describeRule
report it first. timeZone is a string or a bind.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
agreenspan
force-pushed
the
feat/4.0-declared-relations
branch
from
October 8, 2026 04:35
73c99a9 to
2d19e42
Compare
…e refs, model-once, runtime grant refusal, step refs, negated column compares; lensVisit
- H1: a bare value path reads the root row, so only root.where may hold one; a relation grant,
model default or source eligibility where with one is invalid_value_source, and every posture
throws (LensRefusal) instead of reading the related row.
- H2: each Prisma step records where its own { __step } / { __field } references sit (refs);
executePrismaPlan resolves only those locations; a rule value holding __step / __field is
refused at compile.
- H3: a model-default relation never re-enters a model already on the path (root included);
spelled paths are followed as written. Enumerating walks visit each visit class once.
- M1: a later layer's grant crossing a relation its parent doesn't show throws at runtime too.
- M2: a column comparison inside a counting step (count / relation aggregate) is refused on
Prisma at compile and in validateRule / describeRule; a bare path there never binds to the target.
- M3: a negated column comparison (if, all) compiles to its complement with NULL arms.
- lensVisit(lens, relationPath): one projected visit on demand (first consumer: rules-builder 0.30).
- projectLens by path keeps a map's declared option labels and groups.
- Docs: LENS.md, README, CHANGELOG, VERBS.md, TIMEZONE.md, types JSDoc.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n their own row; enum column compares exact - F1/F2: model-default turn-ons grow a tree under each spelled node (the anchor and every root.relations path): breadth-first, each model at most once at its nearest reach (ties: earlier parent, then field order), never one already on the spelled path. Every posture walks the same tree (no class dedupe), so the gate, read, lensVisit, projections, sources, validateNarrowing and toLensSelect agree exactly and stay within spelled nodes x models. - F3: a scope ref that climbs out of a grant ($$ at its top) is scope_out_of_bounds in validateNarrowing and a LensRefusal in every posture. - F4: an enum column compares only with an enum column of its own type, by equality (natively on SQL); ordered and enum-to-text column compares are refused on both compilers and in validateRule / describeRule. The 558-shape column fuzz: supportedTargets matches compile, rails agree. - The rails harness loads org.users.posts so check sees what the compilers query. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…plies to; index-stable pointers; per-call trees; presence reads only shown columns; gate refuses what narrowRule can't re-root; list column compares - R6-1/R6-3: a later layer's grant is checked by one function (the gate over its parent's surface: every hop and the column at its end) — validateNarrowing at the visits it applies to (the composed lens's shown visits; the fetch and sources run as they will to cover the visits layer-1 grants cross), and every runtime posture at each visit it resolves. - R6-2: a source pointer drops its own layer's carried grants without re-indexing the chain, so later layers' grants still read through it (Policy.skipGrantsOf). - R6-4: model-default trees are cached per API call (Policy.trees), never on the narrowing. - R6-5: a relation that shows no column is fetched by a column it shows, or not at all. - R6-6: narrowRule's re-root refusals are LensRefusals; validateNarrowing refuses a grant at a to-one visit it can't re-root, and validateRuleInLens refuses a rule narrowRule can't narrow. - LOW: a list column compares with a column only by membership (contains / notContains of a scalar); anything else is refused on both compilers and in validateRule / describeRule. - Docs: nested default relation objects apply along spelled edges as well as tree edges. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nother column Overrides round-6 edge call 1: a relation showing no column is selected by its key alone (the join key, else id), even a hidden one, so fetch → projectRows(keepGrantColumns) → check(narrowRule) equals the database for posts any / none / atLeast; a viewer's projection drops the key. A model with no key is not fetched (documented: presence can't be re-checked there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ound from grant reads; never-applied grants unchecked; root key fetch; toSql column targets exact; tree cache by fingerprint - R7-1: every refusal narrowRule raises is a LensRefusal (to-many flat read, out-of-bounds scope); validators report it as an issue, runtime postures throw it. Fuzz: validators never throw. - R7-2: the composed-lens dry run is gone. validateNarrowing resolves, with the runtime's own vetting, every shown visit and every visit the grants / sources / labels / axes at them read through — found from readPaths, so unbound and bound lenses validate alike. - R7-3: a path node's child visits are filtered to those the composed lens shows; the enum inheritance chain is unvetted. - R7-4: a root that shows no column is selected by its id (a viewer's projection drops it). - R7-5: validateRule / describeRule refuse toSql for a substring, pattern or set operator against a column (save list membership); the column fuzz covers every operator (1728 shapes, 0 bad). - PERF: model-default trees are cached on the first narrowing under a fingerprint of its model defaults and each spelled node's own turn-ons and omits (R6-4's mutation safety kept); a later grant's check is memoized per grant, visit and parent chain within a call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es; source wheres narrow as rules - validateNarrowing runs the postures the runtime runs, in an inspect mode that compiles nothing: the projection by path and by model, lensVisit at every shown path, the source plans, the fetch select, and a rule reaching each shown visit narrowed. Each LensRefusal is an issue; ok holds exactly when no posture refuses. The hand-built visit loop is gone. - A source where is narrowed under the whole lens as narrowRule narrows a rule: grants on relations inside array conditions and on a path's hops and terminal relation apply, so an option never comes through a row the lens hides. - sourceReadsVisible keeps the chain whole and skips only the declaring layer's hiding. - The default-tree cache keys on the base lens (identity, maps, mapName, model) as well. - The source planner's and the fetch select's refusals are LensRefusals: a guarded label/axis hop, a to-many hop with a grant, an empty source, a windowed or counting grant on a to-many relation (checked before anything compiles). - toSourceQueries' SQL selects from the model's dbName. - Perf: one call memo (resolved visits with deferred vetting, visit places, model fields, grant refs), cached ancestor grants per path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A window toPrisma has no form for — in a source's own where or a grant carried into it — made toSourceQueries throw toPrisma's plain Error while validateNarrowing passed the lens. The planner now runs the shape check toLensSelect uses (counting steps allowed: the option query runs them) and refuses with a LensRefusal, so inspect-mode validation reports it. The round-8 fuzz generates windowed source wheres and asserts no plain windowing Error escapes the lens's own queries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… every rail - materializeSources checks the condition the option query compiles (the visit's own grants included); toLensSelect fetches, and projectRows(keepGrantColumns) keeps, what each projected source reads — value, label, axes, its condition's columns and relations (the inverse that carries the grants above included). Fetch → materialize now equals the database. - A path source is linked down its path through each declared inverse even with no grant above. - toSourceQueries(lens, options?) takes the clock. - A source where across a bridge has no query (prisma null, sql.error) instead of folding to TRUE. - Labels: least label wins on every rail (distinct on value + label; no distinct for a dotted label); options sharing a label order by value. - What toPrisma can't compile in a to-many grant (fetch select) or a source condition is read by validateRule (toPrisma, with the map) and refused as a LensRefusal; validateRule now also reports a case-insensitive list comparison and a count/aggregate over a relation that can't carry a group step. - Fuzz: round-8 agreement over 21 seeds at full shown depth, no plain throws from the lens's own queries on valid lenses; new 3-rail source fuzz including the library's fetch pipeline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; fetch reads below the visit - A relation node re-roots under a to-one hop by its field (`users any …` on an Org becomes `org.users any …`), so a path going to-one then to-many carries its link and grants instead of collapsing to an empty option list; a ref inside that climbs to the re-rooted row is refused, and a link the path can't carry is a LensRefusal, never `false`. - The fetch reads a source below its visit only: materializeSources walks the fetched tree (the path's link), each level's grants met, and checks the source's condition at its visit (`rowWhere`); the option query keeps the fully linked condition. - materializeSources throws on rows missing a key its sources or their path's grants read (viewer rows); a source across a bridge (where, label, axis, or a bridged pointer) has no query (`prisma: null`, sql.error names the path and the rows to supply) and is materialized from caller-supplied rows holding the far side. - A failed compile of a lens's own grant or source is a LensRefusal; a missing clock or unbound bind stays a usage error. validateRule (toPrisma) now reports case-insensitive Json, list literals holding null, element conditions over array columns, and accepts case-insensitive sets of members on list columns. - Fuzz: mulberry32 everywhere; the source fuzz gains an independent row-walk oracle and a distinct-lens check; new round-10 tests with a hand-walked oracle per path shape. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…links, error classes
- materializeSources requires every key each read walks — through relations and list elements to
the column, for the source's where, label, axes and every grant on the way — and each relation
as one row or a list as the map declares; viewer rows and incomplete bridged rows throw.
- A grant a source's path can't carry (no inverse) is a LensRefusal; a path across a bridge is
routed to caller rows (prisma: null). Nothing compiles to `false`.
- UsageError (missing/invalid now, invalid time zone, unbound bind) and LensRefusal are exported
with their names; compile wrappers keep them apart.
- toPrisma / toSql under { lens }: a failure the bare rule doesn't meet is the lens's grants —
a LensRefusal (code unsupported_target), never a plain Error.
- A wall-clock date example now picks its weekday in UTC, the zone the rule reads in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ageErrors - materializeSources: a to-one null while its key is set (a viewer's projection hid it) and a relation missing or misshapen on the source's own path throw UsageError instead of reading as absent. - UsageError for a row-path time zone, a lens passed with map/mapName/model, and a model source handed to materializeSources. - Unused imports removed. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
Why
Aron: "I thought you had to declare relationships... The lens defines things. If you don't, then you have effective access to the full map that any model would have access to... including recursive back-and-forth traversals." This PR restores the May 2026 design: relations are fields, and each delegate works on its parent's projection (CLAUDE.md, Lens rulings). It also removes
contextas a second channel for caller values. Final spec approved by Aron on 2026-10-08.Relations: fields, off by default
root.relations.org;mapDefaults…models.Org.relations.users, wherever Org is visited.ModelDefaultNarrowinggainsrelations, and nested relation objects are allowed.picks, which names columns only; a relation inpicksiswrong_kind.hiddeneverywhere:not_in_lens, with a hint to turn it on withrelations;walkLensPathandreadLensValuereporthidden;{ lens }compiles throw;projectLens,toLensSelectandprojectRowsleave it out.Policy.originpins layer 1, so a call site that filters the chain can't promote a different layer to first.omitsa relation (allowed besidepicks), or restate one its parent shows to narrow that hop. A restatement hides nothing else.not_visible.wheres and source-eligibilitywheres may read any relation in the schema. A later layer's may read only what its parent exposes.root.relations, the first narrowing's model-default turn-ons are followed breadth-first. Each model is included at most once, at its nearest reach; ties go to the earlier parent, then to field declaration order. A model already on the spelled path is never re-entered. Anything else must be spelled, and every spelled node grows its own tree. Every posture walks this same tree, so they agree exactly and stay within spelled nodes × models visits.lensVisitresolves one path on demand.label/groupBymay cross only relations shown at each projected visit; otherwise it isinvalid_sourceand dropped. A source keyed on a relation iswrong_kind.toLensSelect/projectRowsopen exactly what is turned on, plus the columns grants read. The shallow fetch and therulesoption are cut. A relation that shows no columns is selected by its key column only.contextremovedoptions.contextis gone fromCheckOptions,ToPrismaOptionsandToSqlOptions.timeZoneis a string or a{ bind }.pathis a root-row column on every rail, gated and narrowed like a field.readContextRefand the compile-only gate and narrow variants are deleted.'undefined%'parameter.{ __field: { model, field } }sentinel;executePrismaPlanresolves it to<delegate>.fields.<col>. Prisma rejects the raw sentinel.$.inside a relation filter), of exactly the same type, withequals/notEquals/lt/lte/gt/gteand no offset.IS [NOT] DISTINCT FROM, as check and toSql do.validateRule(rule, { target: 'toPrisma', map, model })(it now takes the schema) anddescribeRuledrop toPrisma for those rules up front.test/rails.columnRef.test.ts). The bind-deny test under a lens returns[1,3,4,5]on all three rails.Edge decisions (flagged)
contains/startsWith/endsWithas a LIKE pattern: orgxyzcontains plan_matched. Case-insensitive column compares are refused too, because they would compile to ILIKE.Org.relations.parent { where }) is allowed as long as each parent visit of the model either shows the relation or has already crossed that edge.Tests
bun run check: typecheck, biome, 2512 pass / 0 fail.test/lens.relationsOn.test.tsholds the spec cases, each checked across the gate,{ lens }compile, read, fetch and cut: the bare lens; on/off at each hop; the N1→N2 refusal; restatement hides nothing; omits that can't be undone; picks naming a relation; later-layer mapDefaults turn-ons; the oracle; edge-once recursion (spelled and multi-hop); sources; and bridges.test/rails.lens.test.ts: fetch →projectRows(keepGrantColumns)→check(narrowRule)equals the database, including under recursion and for grants that read a relation that is off.test/rails.columnRef.test.ts: three-rail column comparisons.Round-4 review (4ac5598)
Each finding got a failing test first, in
test/review.round4.test.ts.pathreads the root row, so onlyroot.wheremay hold one.where, it is nowinvalid_value_sourcein validateNarrowing.{ lens }, toLensSelect, projectRows, readLensValue, sources) throws instead of reading the related row.{ __step }/{ __field }refs sit (refs), andexecutePrismaPlanresolves only those locations. A forged sentinel in a value stays data.__step/__fieldkey is refused at compile.LensRefusal). validateNarrowing still reports it.settleLeafkeeps the column compare.negateadds the comparison column's NULL arm.if/allover a column compare compile, and the three rails agree.relations) and TIMEZONE.md (zone is a bind or a string).lensVisit(lens, relationPath)(export, VERBS, README) agrees with projectLens by path at every key, and returns null for a relation that is off or a model that is re-entered.Flagged
wherecan't hold a bare path. This applies even at the root, because a source'swherefilters option rows, not the root row.Round-5 review (32b5ca8)
Each finding got a failing test first, in
test/review.round5.test.tsandtest/review.round5.columnFuzz.test.ts.shownVisitswalks the exact tree.$$.at its top) isscope_out_of_boundsin validateNarrowing and aLensRefusalin every posture. A$$that stays inside the grant, one array down, is still its own row and allowed.::textcast) and on Prisma.supportedTargetsmatches what compiles with zero exceptions, and every compiled rail agrees with check.org.users.posts, so check sees the same rows the compilers query.Flagged (round 5)
$$that stays inside the grant. I refused only scope refs that climb above the grant's own row, rather than every$$.Round-6 review (e7cdb34)
Each finding got a failing test first, in
test/review.round6.test.ts.checkConditionAtVisit) run over the parent's surface. It covers every hop and the column at the end, so a grant on a column the parent hides is refused too.validateNarrowingalso runstoLensSelectandsourcePlanson the composed lens and reports anyLensRefusal.validateNarrowing.ok⇔ no posture refuses.Policy.skipGrantsOf, without re-indexing the chain. Later layers' grants still read through the pointing layer, so L1'sComment.where deleted = falsereaches L2'scomments.any.Policy.trees, built byresolvePolicy), never on the narrowing object. A narrowing mutated in place reads fresh on the next call.ssn). A relation that shows no column is selected for presence by its key alone: the join key, elseid, even if that key is hidden, the same way a grant's columns are fetched. It never picks another column. The fetch carries the key so the re-check can count rows, and a viewer's projection drops it. A model with no key is not selected, so presence on it can't be re-checked from the fetched rows (documented). Rails test: with posts atpicks: [], fetch → re-check ofposts any/none/atLeast 1equals the database. The R6-5 repro still never selectsssn.LensRefusals.validateRuleInLensrefuses a rule that narrowRule can't narrow, with narrowRule's message. It also reports a grant the lens refuses on the rule's visits as an issue, instead of throwing.validateNarrowingrefuses a grant at a to-one visit that narrowRule can't re-root.contains/notContainsof a scalar column, which SQL compiles exactly. Every other comparison involving a list column is refused on both compilers and invalidateRule/describeRule. The column fuzz now includes list leaves: 648 shapes, 0 bad.Flagged (round 6)
validateNarrowingis stricter than the compile. It refuses an un-re-rootable grant at any shown to-one visit, even if no rule crosses that hop. Examples: a$.ref, or an array condition in an Org grant reached byorg.validateRuleInLensis exact.Round-7 review (8b11242)
Each finding got a failing test first, in
test/review.round7.test.ts.narrowRuleraises is now aLensRefusal. That includes reading a to-many relation flat through a grant, and an out-of-bounds scope ref.validateRuleInLensandvalidateNarrowingnever throw.validateNarrowingnow resolves every shown visit with the runtime's own vetting. It also resolves every visit that the grants, source wheres, labels and axes at those visits read through, found fromreadPaths.validateNarrowingrefuses, andtoLensSelect(bindLens(...), { now })refuses too.id, hidden or not; a viewer's projection drops it. Rails test: Prisma accepts the select and the viewer never seesid.validateRuleanddescribeRulenow refuse toSql for a substring, pattern or set operator compared against a column. List membership is the one exception.lensVisit85 ms vs 66 ms at 32b5ca8 (1.3×);validateNarrowing61 ms vs 40 ms.Round-8 review (8edd426)
Each finding got a failing test first, in
test/review.round8.test.tsandtest/review.round8.fuzz.test.ts.validateNarrowingnow runs, in an inspect mode, the same code the runtime runs:projectLensby path and by model,lensVisitat every shown path, the source plans (whattoSourceQueriesandmaterializeSourcesplan),toLensSelect, and a rule reaching each shown visit, narrowed.LensRefusalbecomes an issue. Any other error propagates, and the fuzz asserts none occurs.okholds exactly when no posture refuses, by construction.whereis narrowed under the whole lens withnarrowAt, the same asnarrowRulenarrows a rule.condition, or infilterunderall, a window, or no condition) and on every hop and terminal relation of a dotted path. An option never comes through a row the lens hides.materializeSources, Prisma and SQL viatoSourceQueries+materializeSourceQuery).FROM "<Model>"while its joins useddbName.skipHidingOf), likeskipGrantsOf. Test: f (L1 label, L3 grant onorg).mapName/model, and the model defaults. Test: h. The cache fuzz shows 0 stale results for re-parent and root edits. In-place field-map mutation is still the accepted edge, now documented in LENS.md.LensRefusals, and validation reports them.sources: {}.toLensSelect: a to-many relation's grant that is windowed (windowRewritehas no form) or counting (a count op or aggregate). This is checked from the grant's shape before anything compiles, so inspect and runtime agree regardless of values.narrowAt's own to-many refusal. Tests: e, k, and a counting grant.validateNarrowingthrow. It is now aninvalid_sourceissue at its position.validateNarrowing.ok⇔ the bound runtime (withnow) never refuses.One call memo now holds resolved visits (with vetting deferred, so an unvetted resolve is reused by a vetted one), visit places (trail + turn-ons), model fields, and grant refs (one walk for bare/escaping).
Ancestor grants are carried once per path.
Spelled-heavy bench (dense 25×6, spelled to-one depth 4, 17,320 visits), against 8b11242:
lensVisitover every path, L2validateNarrowing, L2 (now running five postures)validateNarrowing, L1lensVisitvalidateNarrowingtoSourceQueriesFlagged (round 8)
posts any …source, and is checked there. It readssecret, which L1 hides on Post, so the runtime refuses and validation agrees. When L1 showssecret, both accept, and the options carry the grant (second b test). The alternative is to apply a later layer's grants only at visits its parent shows. That would let options come through rows the later layer hides, so I didn't take it.wherehop grants now takenarrowAt's form (underHopGrants). A hop shared between the where and an axis or label is guarded by each; the duplicate is harmless. The expectation insources.compositeGroupBychanged accordingly.toLensSelectuses. This covers a window in the source's ownwhereand a windowed grant carried into it. Counting steps are still allowed, because the option query runs them. The refusal is aLensRefusal, so validation reports it and both materializers refuse alike;materializeSourceswould otherwise have handled the window through check(). Tests are in R8-7. The fuzz now generates windowed source wheres and asserts that no plain windowing Error escapes.Round-9 review (c6c4745) — the source-options pipeline
Each finding got a failing test first, in
test/review.round9.test.tsandtest/review.round9.sourceFuzz.test.ts.materializeSourcesgave wrong options (fail-open both ways).materializeSourcesnow checks the exact condition the option query compiles, including the visit's own grants.SourcePlan.whereis the one condition every materializer evaluates.toLensSelectnow fetches everything each projected source reads, as it does grant columns: the value, label, axes, and every column and relation in its condition, including the inverse relation that carries the grants above.projectRows(…, { keepGrantColumns: true })keeps them; a viewer's projection drops them.User.org.children). A path source is now linked down its path through each relation's inverse even when no grant sits above.toSourceQueries(lens, options?)takes the clock (now/timeZone/weekStart). Withoutnow, a relative date is a plain usage error at runtime, not a refusal; validation is unaffected. Binds go throughbindLens, as for the compilers. Documented.prismaisnullandsql.errorsays "crosses a bridge… materialize it with materializeSources". Validation is unaffected. Tests show both rails return no query andmaterializeSourcesgives the right set.toLensSelectthrew plain toPrisma errors.validateRule(…, { target: 'toPrisma', map, mapName, model }), memoized per condition. A failure is aLensRefusal. The hand-written shape check is gone; only the select-specific counting-step check remains.validateRulenow also reports a case-insensitive list comparison, and a count or aggregate over a relation that can't carry a group step (it asks the compiler's owngroupPath).matches/fuzzy/case-insensitive lists in grants andmatchesin a source where.distinctis the value plus a sibling label; a dotted label drops it. Options that share a label are ordered by value. A rails test checks equality.materializeSourcesover the library's own fetch, both as fetched and askeepGrantColumnsrows. It covers hidden to-one and list rows, NULLs, narrow roots, nested sources, labels, axes and pointers. 0 mismatches.sourceRailsfuzz in pipeline mode (6 seeds × 200): 0 mismatches. agree8 on seeds 901/911/915/916/920/31: 0 under-strict, 0 over-strict.lensVisitover every path 0.71–0.90 s;validateNarrowing~0.56 s, with plans shared between inspect phases.toLensSelectrose from ~0.18 s to ~0.35 s on this lens, which has a source on every model, because it now plans sources.Flagged (round 9)
prisma: null) rather than makingtoSourceQueriesthrow aLensRefusal. A throw would fail the whole call for a lens that validation accepts and must keep accepting (bridges are allowed), which would break validation ⇔ runtime.SourceQuery.prismais nowSourcePrismaQuery | null.materializeSourcesno longer takes viewer rows. It needs fetched rows orkeepGrantColumnsrows, and now re-applies the visit grants (documented). An old test that encoded the opposite contract was rewritten.Round-10 review (8676135)
Each finding got a failing test first, in
test/review.round10.test.ts. Expected options come from an independent oracle: the seeded rows are walked down the path by hand, each level's grant checked as a plain predicate. The lens's own plan is never used as the oracle.prefixConditionFieldsrefused to re-root an array rule, and the planner collapsed that tofalse.users any …on an Org becomesorg.users any …on its User.$$.inside it) is still refused. A link the planner can't carry is now aLensRefusal, neverfalse.rowWhere(visit grants + own eligibility + guards + allowed values).materializeSourceswalks the fetched tree as the path's link, checking each level's grants.toSourceQuerieskeeps the fully linkedwhere.childrenin the select.materializeSourcesnow throws "needs fetched or keepGrantColumns rows, not viewer rows" when a row at a source's path, or on its way down, lacks a key that its source or a grant there reads.prisma: null, andsql.errornames the path and the rows to supply.materializeSourceshandles it, and bridged pointers too, from caller-supplied rows holding the far side inline. Rows without that side throw.toLensSelectemits no bridge select.LensRefusal.nowor an unbound bind stays the caller's usage error, marked with an internalUsageErrorsubclass ofError.validateRule(toPrisma) now reports, from the shape alone: a case-insensitive comparison on Json, a list literal holding null, and an element condition over an array column (scalar list or Json array).in/notInset on a list column is accepted again, as toPrisma compiles it.mulberry32(test/fuzz/mulberry32.ts).id, R7-4). The oracle is fixed.query.prismaexample now handlesnull.lensVisitover every path 0.77–0.92 s;validateNarrowing0.57–0.59 s. Bridge detection is skipped when the lens has no bridges.Round-11 review (fe33dee)
Each finding got a failing test first, in
test/review.round11.test.ts.materializeSourceschecked only the first segment of each read (fail-open). It now walks every read through each relation and list element to the column, and requires every key on the way. Walking stops at a null row or an empty list, and a Json column's inside is not checked.UsageError.r11_viewercases,r11_bridge(a missing far column, and the far side given as a list), and no false positives (a null to-one row, an empty list, a to-one label). The pipeline tests and fuzzes cover kept rows, id-only picks and relation-off sources.falselinks.ancestorGrantsnever returnsfalseany more.LensRefusal, which validation reports.prisma: null). A pointer with nothing to carry still compiles against the far map.r11_bridge2(both rails agree), and a one-sided map with a root grant.lens.security(the author source),lens.sourceFromMapDefaults(bridged path source), and the round-5 p2c fixture, whose incidental grant was dropped.UsageErrornow covers a missing or invalidnow, an invalid time zone (checked once per zone name), a time-zone bind that isn't a string, and an unbound bind in the compilers or incheck/materializeSources.compileOrRefuseandcompileWithLenspass aUsageErrorthrough unchanged.LensRefusalandUsageErrorare exported fromindex.tswithnameset, and documented in the README "Error Handling" section and in docs/VERBS.md.nowand an invalid time zone, a missing binding inmaterializeSources, and both names.toPrismaandtoSqlunder{ lens }now run throughcompileWithLens. When the narrowed rule fails to compile but the bare rule compiles against the same base, the failure is the lens's: aLensRefusalwith codeunsupported_target.check()runs the lens), not an invalid lens.validateNarrowingstill accepts these lenses, and the agreement fuzz treats the code as a compile limit.validateRuleInLenstakes no target, so the compile is where this is enforced.validateRule(toPrisma)passes a rule that toPrisma or Prisma then rejects are filed as validateRule(toPrisma) misses Prisma shape and literal gaps: Json path arrays, case-insensitive Json, literal types, ordered list/enum ops #30, with the probe and the categories.date-operations"complex date validation" depended on the wall clock and failed under the check'sTZtoday. It now picks its weekday in UTC, the zone the rule reads in.lensVisitover every path takes 0.64–0.81 s andvalidateNarrowing~0.52 s. On the reviewer's probe,lensVisittakes 4–15 ms per pass andvalidateNarrowing3–9 ms.Release polish (23e2833)
Tests are in
test/review.round12.test.ts.materializeSourcescatches two more kinds of bad input, asUsageError:keepGrantColumnsrows are unaffected; the pipeline tests and fuzzes stay green.mapIdset,map: null) now models its unreachable label hop with a nulldefinitioninstead.UsageError: a row path given as the time zone, a lens passed together withmap/mapName/model, and a pointer source handed tomaterializeSources.bun run checkpasses with 0 warnings.Downstream
picks.rulesis removed fromtoLensSelect/projectRows.contextcallers must switch to binds.{ by: 'model' }surface.lensVisitreplaces itsrawView.org.usersback to the User root, or the longer of two routes to one model) must now spell that path.sourceValuesin validateRuleInLens, which rules-builder asked for.🤖 Generated with Claude Code