ADR 0015: Relation extraction — an opt-in slot with model-derived / unverified provenance markers
Add a RelationExtractor Protocol slot beside build_relation_graph; the default stays byte-identical, LLM-extracted edges are stamped model-derived + unverified, screened through the SecurityGate, and never silently trusted.
Status: accepted · Date: 2026-07-13
Immutable record. Exempt from drift tracking (no
covers). Supersede, don't edit.
Part of the 0002 product direction. Constrained by 0005 (experimental capability isolated from the default path) and 0010 (the deterministic security gate). Shipped in rag-spine 0.9.0.
Context
The structural relation graph (build_relation_graph) is fully deterministic and citable:
every edge derives from a controlled dimension — profile config, facts, or chunks. But some
useful relations live only in narrative text ("A partners with B"), reachable only by an
LLM, which is non-deterministic and can fabricate. We want that capability without letting
a model-asserted edge masquerade as a controlled, verified fact, and without touching the
byte-identical default path.
The key insight is the same shape as ADR 0010: a model-extracted relation is useful but untrusted. It must be labelled as such, and the two hard guarantees — RESTRICTED isolation and competitor refusal — must hold on this new input path too, deterministically, regardless of what the model returns.
Decision
Add a RelationExtractor Protocol slot (src/ragspine/graph/extractor.py), consumed by
build_relation_graph(..., relation_extractor=None):
- The default is
None→ the base graph is byte-identical to before (no extra edges). - The default extractor (when one is wanted) is
DeterministicRelationExtractor: rule-based same-document entity co-occurrence (co_occurs_with), zero LLM, deterministic, with clean lineage (doc source only, no model markers) — genuinely distinct from the base doc→entitymentionsedges. LLMRelationExtractoris opt-in (behind the[llm]extra) and mirrors the narrative graph extractor's degrade discipline. Every edge it emits is stampedderived=model-derived+verified=unverified(never silently trusted); its lineage is stamped from the chunk (the caller), never the model's self-report; RESTRICTED chunks never reach the LLM; and both endpoints of every relation are screened through the deterministicSecurityGate— a competitor / external endpoint drops the edge. It degrades to nothing onProviderError/ bad JSON, and is bounded bymax_relations.- Selection mirrors
make_narrative_graph:make_relation_extractor(spec, *, provider, profile)/RAGSPINE_RELATION_EXTRACTOR(none→None;deterministic/rule/cooccurrence;llm/on→ honest degrade toNonewithout a provider).
Principle: capability beside the default path, never within; model-asserted edges are labelled and screened, never silently adopted.
Alternatives considered (rejected)
- Materialize LLM edges with the same clean lineage as controlled edges. Rejected — it would let a fabricated relation read as a verified fact, defeating the provenance invariant.
- Skip the
SecurityGateon extracted edges (trust the base graph's node isolation). Rejected — a model could name a competitor that is never present as a node, smuggling a competitor relation in. Screen at extraction time instead. - Wire extraction into the default
build_relation_graphunconditionally. Rejected — it breaks the byte-identical default and puts a non-deterministic step on the controlled path.
Consequences
- The relation graph gains an opt-in narrative-relation capability while the default path stays byte-identical and every model-derived edge is self-labelling and auditable.
- The two hard guarantees (RESTRICTED isolation, competitor refusal) now hold on the new extraction input path, enforced deterministically rather than trusted from the model.
- New artifacts: a
RelationExtractorProtocol, a deterministic default and an LLM implementation, a factory / env selector, and the provenance-marker constants (derived=model-derived,verified=unverified) written into each edge's metadata.
ADR 0014: Dify workflow as a service (analyze / compile / run) with three-layer safe execution
Extend the existing ragspine service with three Dify endpoints of increasing trust — analyze and compile always on, run disabled by default behind a static import gate, a restricted in-process sandbox, and (Linux) subprocess isolation; the provider is always decided server-side.
ADR 0016 — Retrieval productization config
Metadata filtering, multi-index routing, parent-child preset, and economy mode, with every retrieval invariant preserved.