Skip to content

feat: ruleReferences(lens, rule) — which rows a rule names, from the … - #14

Closed
vados-lab wants to merge 1 commit into
inixiative:mainfrom
vados-lab:ZLT-3692
Closed

vados-lab wants to merge 1 commit into
inixiative:mainfrom
vados-lab:ZLT-3692

Conversation

@vados-lab

@vados-lab vados-lab commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Why

ruleSourceValues answers "which values does this rule name at each declared source". A source exists to supply a picker's vocabulary, so declaring one commits the lens to fetching that vocabulary.

There was no way to ask the other question: which rows does this rule name? A rule can name a row whose option set is supplied out of band, or that is never offered in a picker at all. Asking it through sources forced a scan nobody wanted — in our case a DISTINCT over a very large table, returning unlabelled uuids and only rows someone had already touched.

Consumers were working around this with hand-written tables of "which picks are references", which drift from the schema.

What

ruleReferences(lens, rule) — the rows a rule names, derived from the field map, declared by nobody:

  • a foreign key names a row of the relation's target (fromFields)
  • an identity another row would point at names a row of its own model (toFields)

Both facts the map already carries, so no lens author restates them.

ruleSourceValues(narrowing, { field: 'accountUuid', operator: 'equals', value: 'A1' })  // []
ruleReferences  (narrowing, { field: 'accountUuid', operator: 'equals', value: 'A1' })  // 1 reference

Policy stays with the caller: every foreign key answers, including tenant keys and owner back-pointers. Filter by referencedModel.

Refactor, not duplication — the leaf walk (path resolution, scope refs, literal extraction, dedupe, dynamic) moved to an internal ruleLeafValues taking a gate callback. ruleSourceValues now sits on it. One walker, two consumers; that is the −119.

Edge cases handled

  • Composite keys are not references. One column of a two-column key identifies a set, not a record.
  • Cross-map. The identity set is resolved from the map the leaf landed in, not the root map — a bridge relation crosses maps.
  • Memoized per FieldMap via WeakMap; the identity scan is O(models × fields) and this runs on every save.
  • dynamic is reported the same way ruleSourceValues does: a bind/path value or a non-enumerating operator means the rows cannot be listed. An empty values does not mean "references nothing".

Testing

  • 12 new tests; 1474 total, all green
  • Public API is purely additive: diffed the generated .d.ts against mainREMOVED: none, ADDED: RuleReference, ruleReferences; the RuleSourceValues type is byte-identical
  • Mutation control on the refactor: removing the source gate turns 16 existing tests red, so the delegation is genuinely covered

Minor bump: 2.23.0 → 2.24.0.

@agreenspan agreenspan closed this Sep 15, 2026
@agreenspan

Copy link
Copy Markdown
Contributor

Closing this one — the extraction gap turned out to be a lens spelling problem, not a missing primitive. Sources already do value = id / label = name when declared on the referenced model (that is how group.uuid works); missions, questions and forms were just spelled as the child-side FK column, which is why they needed the hand-rolled picker entries and never registered as references. userevidence/Zealot-Monorepo#2305 applies the Groups spelling to the other three and deletes the hand-rolled entries. Once every referenced model has an identity source, ruleSourceValues filtered by the RuleReference registry axis answers the reference question with no engine change. The one json-rules gap 3692 does hit is source.label accepting only a sibling column while groupBy takes a dotted to-one path — that is what integrationMap.uuid needs.

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.

2 participants