Skip to content

feat!: json-rules 3.4 — gate against the narrowed lens, read each scope's visit (0.30.0) - #9

Merged
agreenspan merged 4 commits into
mainfrom
feat/json-rules-3.4
Oct 8, 2026
Merged

agreenspan merged 4 commits into
mainfrom
feat/json-rules-3.4

Conversation

@agreenspan

@agreenspan agreenspan commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Adapts rules-builder to json-rules 3.4.0 (inixiative/json-rules#26): relations are fields, off until the first narrowing turns them on; context removed.

Do not merge until json-rules 3.4.0 is on npm. Developed and checked against the PR build (bun run check green: typecheck, examples typecheck, lint, 413 tests, against json-rules PR head fe33dee). package.json declares ^3.4.0 but bun.lock still resolves 3.3.1 — after the train publishes 3.4.0, run bun install and commit the lockfile, then CI can go green.

Why 0.29 breaks on 3.4

  • It gated, coerced and described every rule against projectLens(…, { by: 'model' }). That surface is a bare lens, which turns no relation on, so every relation rule was invalid, coercion skipped relation paths, and describe() errored.
  • Element scopes re-anchored a bare createLens at the related model, which dropped nested relations. Presets that went through a relation failed validation.

What changed

  • resolve(source) returns a LensView: lens is the narrowed lens, used as the gate. Fetched sourceValues ride the visit at their own path: the picker offers them there, and a leaf's valid holds its value to them. The gate takes no per-path fetched set, so another path to the same model keeps its declared values. visit(at) gives the fields the lens shows at each dotted path, resolved on demand by json-rules lensVisit (nothing enumerated).
  • Every scope (root, element scopes, branch groups, decoration facets) reads the visit at its own path. Leaf and array valid, coerceRule, describeRule and preset validation all run against the narrowed lens, in situ.
    • A relation is offered only where the lens turns it on.
    • A model outside json-rules' model-default tree (a second reach of a model) is not offered unless spelled.
  • validateDecoration(view, …) validates presets in situ:
    • A models[…] list is checked at every element scope the lens reaches through a list relation.
    • A model that is never an element scope is reported.
  • RuleBuilderSource.narrowing may be a list of layers, outermost first.
  • withAllRelations(source) turns every relation on in the source's first layer.
  • rawView(source) is the raw record that permissions and transitions gate: the view of withAllRelations over the record (model defaults grow json-rules' tree; the anchor's own relations are spelled, so self-relations are on).
  • lensValuePicker / lensScopeSurface now start at { at }. Loop portals carry their own at.
  • Fields whose map entry declares options keep their labels and groups. (The path projection rebuilds them from the allowed values alone, which drops both.)
  • Example app:
    • authors against the ref's layered source;
    • vip-active and a new with-orders narrowing turn relations on;
    • grant editors turn every relation on in their menu.

Breaking API

  • buildRoot(cond, view, …).
  • Decoration functions take a ViewAt scope where they took a Lens; validateDecoration and useFacetFields take the view.
  • buildActionRoot({ view }).
  • useRuleBuilder returns lens (the narrowed lens) and view.
  • lensValuePicker and lensScopeSurface take at in place of mapName/model.
  • describeScopeFields sits beside describeModelFields.

Downstream: template, Kingdom and Tribe useEmailVariableScope call lensScopeSurface(resolve(source)) and re-anchor with { mapName, model }. They need lensScopeSurface(composeNarrowed(source), { at: frame.loop.at }) when they adopt 0.30.

Tests

New test/relations.test.tsx covers these cases. A narrowed lens with relations on lets the builder offer a relation rule, validate it (including two levels deep and a dotted to-one path), and coerce and describe it. Presets through a relation work, including per-model presets checked in situ. A relation that is off is not offered, and a saved rule that crosses it is invalid. The edge-once rule holds. test/resolve.test.ts covers the view, layered narrowings, withAllRelations and rawView against the json-rules gate. Existing fixtures were moved to sources that turn relations on.

An adversarial review found three gate problems, all fixed in 5b8a365: a per-model fold of fetched values onto the maps, a stale model-repeat cut in the pickers, and dotted list fields built without their subtree.

Updated to the final 3.4 in 11f1e10: views read lensVisit (no duplicated policy logic, no eager projection); model defaults grow a tree; the declared-options patch is dropped because projectLens now keeps them.

🤖 Generated with Claude Code

agreenspan and others added 4 commits October 8, 2026 01:59
…pe's visit (0.30.0)

json-rules 3.4 turns relations off until the first narrowing turns them on. 0.29 gated,
coerced and described every rule against the projectLens by-model surface, which is a bare
lens: every relation rule was invalid, coercion skipped relation paths, describe() errored,
and element scopes re-anchored a bare lens that dropped nested relations.

- resolve(source) returns a LensView: the narrowed lens (the gate, fetched sourceValues
  folded in) and visit(at), the fields the lens shows at each dotted path.
- buildRoot, the decoration functions, validateDecoration and the pickers read visits;
  validity, coerceRule, describeRule and preset validation use the narrowed lens, in situ.
- RuleBuilderSource.narrowing may be a list of layers; withAllRelations turns every
  relation on in the first layer; rawView is the raw record permissions/transitions gate.
- lensValuePicker / lensScopeSurface start at { at }; loop portals carry their at.
- Peer and dev dependency @inixiative/json-rules ^3.4.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adversarial review fixes:
- Drop the per-model fold of sourceValues onto the maps: it gated a sourced column on
  every path to its model and unioned fetched values past a declared set. A leaf's valid
  now folds in the value set its visit offers; preset options are checked at their slot's
  visit.
- lensValuePicker / lensScopeSurface: no model-repeat cut — the lens ends every path.
- A dotted list field outside a branch resolves its relation, so its subtree is built.
- validateDecoration groups a models[...] violation across visits and walks only visits
  that can reach a models key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- createView resolves each visit on demand with json-rules lensVisit; rawView is that view
  over withAllRelations. No policy logic (edge-crossing, visit resolution) is duplicated,
  and nothing is projected whole. The declared-options patch is gone (projectLens keeps
  them).
- Model defaults now grow a tree (each model once, at its nearest reach). withAllRelations
  also spells the anchor's own relations at the root so a raw record's self-relations are
  on; tests that reached a model a second way now spell the path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@agreenspan
agreenspan merged commit d6571c2 into main Oct 8, 2026
1 check failed
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