Repository navigation
How wide should a relation's declared range be #359
Description
Activity
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% ofknows'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
- 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.
- 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). - 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.
- Item 3 dissolves. Ranges follow the model's typing, so museum-as-
Organizationis absorbed intovisited's range instead of forcing a re-cut of the entity vocabulary.
Consequences
- The derivation contract for LlmRecommendationStrategy #353: the proposer gains an input (the observed endpoint table from a permissive pass over its sample) and a job (semantic pruning of it).
- The future evolution loop (its own map) consumes the widening-candidate report as a named input.
- unstructured2graph: gliner2's compiled-schema cache is keyed on a memory address #365 applies: the shadow sample and the production schema are two compiled schemas in one process, so both must be held.
- Near-synonymous relation labels all fire on the same pair #360: the 43 label-confusion edges belong there.
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?":
visited's declared range was(Location,). The model types "Museum of Modern Art" asOrganization. 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 onlyattended(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:
Organization-vs-Locationis 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 onProductandOrganizationbeing distinct.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.