Skip to content

How wide should a relation's declared range be #359

Description

@antejavor

Part of #344

Question

A domain/range binding that is one label too narrow deletes real signal at decode time. What rule sets the width, and do #348's two compilations get the same one?

#350 measured the cost on a question the corpus actually asks — "how many days passed between my visit to MoMA and the Ancient Civilizations exhibit?":

permissive    visited(User:'user' -> Organization:'Museum of Modern Art')
constrained   (suppressed)

visited's declared range was (Location,). The model types "Museum of Modern Art" as Organization. Because typing is enforced at candidate construction (scoring.py:621-647, per #345), the edge is never scored, let alone written. The constrained arm kept only attended(User:'user' -> Event:'MoMA tour').

So constraining's cost is not paid in junk — it is paid in answer-bearing edges when a range misses a type the model plausibly assigns. #347 made derivation fully automatic with no human approving the result, which is exactly the regime where a too-narrow range ships unnoticed.

Open:

  1. Who decides width. Is it a derivation-time judgment (The derivation contract for LlmRecommendationStrategy #353's proposer picks ranges), or mechanical — every entity type observed as an endpoint of that relation in the sample gets included, with the LLM only naming the relation?
  2. Do the two compilations diverge? Extraction-time constraints vs post-hoc validation #348 decided "same spec in both, no deliberate loosening", and that buys a real invariant: a GLiNER2 post-hoc violation means a cross-window merge or a bug. Widening only the extraction-time compilation would keep the MoMA edge and forfeit the invariant; keeping them equal keeps the invariant and keeps losing the edge. Which side pays, and is there a third option (same spec, but the post-hoc half reports rather than flags for a widening candidate — a type seen as an endpoint but not declared)?
  3. Is this really an entity-typing problem? Museum-as-Organization-vs-Location is ambiguity in the entity vocabulary, not in the relation. Fewer, broader entity types would dissolve it — and would also blunt the constraint's value, since Wire a constrained ontology and read the edges #350's suppressed junk (works_for(Product:'Trello' -> Organization:'new job')) depends on Product and Organization being distinct.
  4. What detects a too-narrow range in production? Does LightRAG consume domain/range too #349's zero-instance coverage signal catches a declared relation type with no instances. It cannot see a type that quietly loses half its edges to a missing endpoint label. Is there an observable — e.g. counting, in the post-hoc pass, entity type pairs that co-occur in a chunk with a relation's declared head but no edge?

Interacts with #348 (the two-compilation decision this pressures), #353 (derivation emits the bindings), #349 (the reporting-only coverage signal), and #360 (both are "what shape must a derived vocabulary have").

Evidence: prototype/gliner2-constrained-edges, section A of the prototype README.

Activity

added a commit that references this issue on Sep 28, 2026

antejavor commented on Sep 28, 2026

@antejavor
ContributorAuthor

Resolution

The ticket asked what rule sets a relation's range width. The answer depends on the loop the whole model sits in: hygm proposes v1, sessions are ingested, and the model is periodically revised to fit the data (#347's error-correction path, which is its own future map). Under that loop, v1's width doesn't have to be right. What it must guarantee is that the loop can see its mistakes, and a too-narrow range is the one mistake that hides its own evidence.

Evidence: section G of the prototype README on prototype/gliner2-constrained-edges (32d1928), range_width.py / range_width.log. It uses the clean #350 run (#365's correction); the script asserts 1004/567.

What the declared ranges suppress

All 266 permissive-arm edges the constrained ranges would reject, grouped by (relation, head type, tail type), each group judged by reading its examples:

verdict n e.g.
real, recovered by a wider range 29 (11%) visited(User -> Organization:'museum of modern art') (the MoMA edge), prefers(User -> Organization:'dropbox'), located_in(Product:'shoes' -> Location:'closet')
label confusion (a correct declared relation exists) 43 visited(User -> Event:'moma tour') ×26, which attended covers (#360's territory)
junk, rightly suppressed 194 works_for(Product:'lumetri color panel' -> Organization:'premiere pro'), knows(User -> Topic:'codecs'), self-loops

So the constraint keeps out about 6.7 junk edges per real one it loses. 19 of the 29 real losses have an Organization endpoint (museums, brands, a school). (In the discussion I'd put this at 22 of 29; the script's count is 19.) The model's typing there is consistent, not noisy: brooklyn museum is Organization ×24 and never Location, and levi's is Organization ×10. "A museum is a Location" is an ontologist's intuition, and this model disagrees every single time.

Neither mechanical rule separates the real losses from the junk:

  • By frequency, the junk is the larger share. knows(User -> Topic) is 31% of knows's permissive edges, all junk; visited(User -> Organization) is 11%, all real. No threshold keeps the second and drops the first, and "every observed type" collapses to permissive.
  • Type ambiguity ("is this text typed with a declared type anywhere else?") passes 17% of real losses against 20% of junk.

What separates them is semantic: visiting an organization makes sense, while "knowing" a codec is a different sense of know.

The asymmetry that decides it

A too-wide range lets junk in as conformant, and it's visible in the graph, so the loop can prune it. A too-narrow range suppresses at decode time: the edge is never written, so the graph contains nothing saying "visited is missing Organization". A loop that revises only against the graph would look at visited pointing only at Locations and conclude v1 was right. Over-narrow errors conceal themselves, so the loop needs a channel that shows it what decoding threw away.

Decisions

  1. Extraction-time constraints vs post-hoc validation #348's same-spec invariant stands (item 2). Both compilations get the same width, and a GLiNER2 post-hoc violation still means a cross-window merge or a bug. Rejected: decoding wide and validating narrow. It would put the evidence in the graph, but it forfeits the invariant and lands 47% of raw output flagged.
  2. The loop sees suppression through a shadow sample (item 4). A periodic permissive extraction over a small sample of new sessions reports, per relation, the endpoint type pairs the current range suppresses, as widening candidates (visited -> Organization: 6). It is reporting-only: it never blocks and never writes to the graph, and it is the input the revise step consumes. This is the production detector Does LightRAG consume domain/range too #349's zero-instance signal couldn't be, because that one only sees a declared type with no instances, not one quietly losing edges. Cost: one permissive pass per sample (~78s per 10 sessions measured).
  3. v1 width is observed, then LLM-pruned (item 1). At derivation, the same permissive pass runs once over the derivation sample and gives the The derivation contract for LlmRecommendationStrategy #353 proposer a table of the endpoint type pairs this model actually produces per relation. The proposer keeps the pairs that make semantic sense. So v1 starts from the model's real typing, not intuition, and junk never lands. Its mistakes (a real pair dropped) surface in decision 2's report. It's non-deterministic, so it's covered by The starter relation vocabulary #347's stability check. Rejected: all observed pairs, loop prunes, which writes ~194 junk edges as conformant until a revision and still needs the same semantic judge later; and intuition only, which starts with known, avoidable losses.
  4. Item 3 dissolves. Ranges follow the model's typing, so museum-as-Organization is absorbed into visited's range instead of forcing a re-cut of the entity vocabulary.

Consequences

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions