Documentation
¶
Overview ¶
Package fusion is the generic deterministic-fusion engine: fan-out sub-query dispatch, dedup, rank, and budget enforcement over a GraphQueryClient, plus the product-supplied Lens SPI (lens.go). It is a near-leaf package — its only domain dependencies are the message package (for Triple in Entity and StorageReference as the Hydrate handle) and the leaf storage interface package (the Hydrate backend-deref contract); no NATS, no research-domain types (Intent, ExecutionOutput, RouteAction). Domain-coupled callers (processor/research-graph-execute) compose this engine and wrap the result in their own payload types; products supply a Lens.
Index ¶
- Constants
- Variables
- func SeedIDs(seeds []Seed) []string
- type BM25Args
- type BodyReason
- type BodyResolver
- type Budget
- type Direction
- type Edge
- type EdgeSpec
- type Engine
- type Entity
- type EntityStateArgs
- type Evidence
- type Facet
- type FuseOptions
- type FuseResult
- type GraphEdge
- type GraphEvidence
- type GraphFact
- type GraphNode
- type GraphProjection
- type GraphQueryClient
- type Hydration
- type Impact
- type IndexState
- type IndexStatus
- type Lens
- type Locator
- type MapStoreResolver
- type Miss
- type Node
- type Path
- type PredicateWalkArgs
- type Provenance
- type RankSignals
- type Ref
- type Request
- type ResolveMode
- type ResolveQuery
- type Response
- type RetrievalClient
- type Seed
- type StoreResolver
- type SubQuery
- type SubQueryType
- type TemporalRangeArgs
- type Unhydrated
- type UnhydratedReason
- type ViewRevision
- type Want
Constants ¶
const ( // DefaultMaxParallelism caps concurrent sub-query execution. // Keeps fan-out from saturating the responding gateways under // a wide-decompose scenario; operators can raise for staging // environments with more capacity. DefaultMaxParallelism = 8 // DefaultMaxResultsPerSubquery caps per-sub-query result count // before ranking + budget enforcement run. Prevents a single // runaway sub-query from dominating the evidence array. DefaultMaxResultsPerSubquery = 50 // DefaultBudgetTokens is the fallback evidence-budget when the // caller doesn't supply one. Should never normally fire when // callers supply a domain default, but acts as a safety net. DefaultBudgetTokens = 4000 // DefaultPerSubqueryTimeoutFraction is the fraction of the // overall context deadline each sub-query gets as its per-call // timeout. 0.5 leaves slack for fan-out overhead and ranking. DefaultPerSubqueryTimeoutFraction = 0.5 // DefaultExecuteTimeout is the fallback per-query timeout when // the caller's context has no deadline. Used only to bound // per-sub-query execution when ctx has no deadline set. DefaultExecuteTimeout = 60 * time.Second )
Default engine constants. Callers that pass zero-value FuseOptions fields fall through to these.
const ( GraphDirectionOutgoing = "outgoing" // the seed is the edge's subject (source) GraphDirectionIncoming = "incoming" // the seed is the edge's object (target) )
GraphEdge.Direction values, relative to the projected seed node. Source and Target always carry the TRUE subject→object orientation regardless of direction — Direction only says which side of the edge the seed is on.
const ( FacetRelations = Facet(WantRelations) // the per-node relations map (forward + reverse roles) FacetPaths = Facet(WantPaths) // the outgoing paths DFS FacetImpact = Facet(WantImpact) // the incoming impact BFS )
The three edge-walk facets — values structurally pinned to their Want twins.
const ContractVersion = "1"
ContractVersion identifies the wire shape of Request/Response.
const DeferReasonAllSeedsUnhydrated = "all_seeds_unhydrated"
DeferReasonAllSeedsUnhydrated is the engine-level defer cause for a query whose seeds all resolved but none could be read. It is not a graph.DeferReason — no readiness gate produced it; the index was healthy and the hydration failed — so it is named here alongside the other engine-level causes that reach Response.DeferReason.
Variables ¶
var ErrReadinessUnknown = errors.New("fusion: graph readiness is unknown")
ErrReadinessUnknown marks a readiness answer the RetrievalClient cannot vouch for: the producer never published, or its feed went quiet past the freshness window holding a last-known value. It is DISTINCT from a wiring failure (a transport with no KV capability, a bucket that cannot be opened) because the two want different responses — an unknown feed is a fail-closed degrade the caller sees as an honest empty envelope, while broken wiring is an operator's bug that must stay loud rather than masquerading as "the graph is busy" forever (ADR-084 D6).
Wrap it with %w; callers match with errors.Is.
Functions ¶
Types ¶
type BodyReason ¶
type BodyReason string
BodyReason is why a node's requested verbatim body (WantBody) could not be loaded. It is a DISTINCT type from UnhydratedReason — a body-hydration failure and a seed-hydration failure are different surfaces: an Unhydrated seed produced NO node at all, whereas a BodyReason rides on a node that exists and ranks; only its Body is absent (gh#616, #600). The set is CLOSED and shares UnhydratedReason's vocabulary value-for-value so the two failure surfaces read the same on the wire.
const ( // BodyNotFound is a body whose reference did not resolve to a stored object: // the lens produced no hydrate handle for the entity, or the handle could not // be resolved. It does NOT license "the entity has no body" as a fact — only // that this lookup produced none. BodyNotFound BodyReason = "not_found" // BodyError is a genuine fault reading the stored body — the deref returned an // error. Distinct from BodyNotFound so a caller can tell a missing object from // a backend fault. BodyError BodyReason = "error" )
The closed body-hydration-reason set. Kept in lockstep with the UnhydratedReason vocabulary (pinned by a test).
type BodyResolver ¶
type BodyResolver struct {
// contains filtered or unexported fields
}
BodyResolver dereferences a Lens.Hydrate handle to its verbatim bytes. It is the engine-side helper the assemble step uses to populate a node body from the handle a lens returned — backend-agnostic via the injected StoreResolver.
func NewBodyResolver ¶
func NewBodyResolver(r StoreResolver) *BodyResolver
NewBodyResolver builds a BodyResolver over the given StoreResolver. A nil resolver is permitted (ResolveBody then errors on any non-nil handle) so a deployment with no verbatim-body stores still constructs cleanly.
func (*BodyResolver) ResolveBody ¶
func (b *BodyResolver) ResolveBody(ctx context.Context, ref *message.StorageReference) ([]byte, error)
ResolveBody returns the verbatim body bytes for a Hydrate handle.
Key granularity (gh#376 coordination): the handle's Key addresses the EXACT verbatim body — one pre-sliced body blob per entity (keyed by entity ID or content hash) — so Get returns the body byte-for-byte with NO engine-side line math. The lens's Locator (path + line range) is for citation/display only; the body comes pre-sliced through the handle, not by trimming a whole file.
- A nil ref means "no verbatim body" — returns (nil, nil), NOT an error. (Lens.Hydrate returns (nil, nil) for body-less entities; this preserves that signal end-to-end.)
- A non-nil ref with an empty StorageInstance, or an instance with no registered store, is a wiring/producer fault — returns an error so the caller can degrade the node (the engine omits the body; hydration is best-effort and does not fail the fuse — see Lens.Hydrate).
- The store's Get error is propagated wrapped.
type Budget ¶
type Budget struct {
MaxNodes int `json:"max_nodes,omitempty"`
MaxBytes int `json:"max_bytes,omitempty"`
}
Budget bounds a response. Zero fields take engine defaults.
type EdgeSpec ¶
type EdgeSpec struct {
Predicate string
OutgoingRole string
IncomingRole string // "" to skip the reverse direction
Facets []Facet // nil/empty = all facets; else only the named facets
}
EdgeSpec declares one relationship predicate the engine should expand around a seed, with the role labels for its forward and (optional) reverse directions. For code: {Predicate: "code.relationship.calls", OutgoingRole: "callee", IncomingRole: "caller"}. An empty IncomingRole skips the reverse direction.
Facets optionally restricts which edge-walk facets this predicate feeds. An empty Facets means the edge participates in ALL three facets (relations, paths, impact) — the backward-compatible default. A non-empty Facets restricts the edge to exactly the named facets, so e.g. a containment edge can populate the relations map (Facets: {FacetRelations}) without inflating the impact walk.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine runs the lens-driven deterministic fusion pipeline over a RetrievalClient, hydrating verbatim bodies through a BodyResolver.
func NewEngine ¶
func NewEngine(graph RetrievalClient, body *BodyResolver) *Engine
NewEngine builds a lens-driven engine. body may be nil — node bodies are then omitted (the response degrades gracefully rather than panicking).
func (*Engine) Fuse ¶
Fuse resolves req against the graph through lens and returns the fused response.
The honesty rule is that an empty result is never ambiguous. An UNHEALTHY graph yields an empty envelope the caller must fall back on; a healthy graph that found nothing yields a miss with near-matches; seeds that resolved but could not be read yield Unhydrated and NO miss; and a backend failure is surfaced as an error rather than silently becoming a "not found".
ADR-084 narrowed the first case from coverage to health: a healthy index that is merely behind now SERVES, reporting its view age on the envelope, because withholding on lag sent callers to fall back on a graph that could have answered.
func (*Engine) WithMetrics ¶
func (e *Engine) WithMetrics(registry *metric.MetricsRegistry) *Engine
WithMetrics wires the app MetricsRegistry so this engine's body-hydration- failure counter (fusion_body_hydration_failures_total, gh#616) registers into the /metrics-scraped registry. Returns the engine for chaining. Deliberately a builder rather than a NewEngine parameter so the constructor signature stays stable for the library's callers.
The counter is resolved PER ENGINE against the given registry: two engines sharing one registry increment the SAME series (register-or-get-existing), two engines with different registries each get their own, and a nil registry (or an engine built without WithMetrics) counts against the default registerer. There is no process-global state pinning the counter to whichever registry was seen first.
func (*Engine) WithSignals ¶
func (e *Engine) WithSignals(s RankSignals) *Engine
WithSignals attaches the framework ranking signals — ontology specificity + predicate salience (ADR-062 increment 5, gh#396) — folded into ranking on top of resolve-order + lexical. Returns the engine for chaining. Passing nil (the default) keeps ranking at resolve-order + lexical.
type Entity ¶
Entity is the minimal projection a Lens reads: the entity's ID and its current triples. Kept deliberately small (no graph.EntityState, no NATS) so pkg/fusion stays a near-leaf — the Lens reads human-facing fields off the triples.
type EntityStateArgs ¶
type EntityStateArgs struct {
EntityIDs []string `json:"entity_ids"`
}
EntityStateArgs carries IDs to fetch.
type Evidence ¶
type Evidence struct {
// EntityID is the graph entity this evidence refers to. Required.
EntityID string `json:"entity_id"`
// Tier is the retrieval tier that surfaced this hit: "0" (rules /
// predicate queries), "1" (BM25), or "2" (neural — deferred to
// Phase 2). Operators may use it to filter evidence by retrieval
// method.
Tier string `json:"tier"`
// Source is the retrieval source within the tier — e.g.
// "classifier", "predicate_walk", "bm25_index". Required.
Source string `json:"source"`
// Score is the within-tier ranking score (higher = better).
// Cross-tier comparison is not meaningful in Phase 1 (per-tier
// ordering + recency tie-break); Phase 2 may add a learned ranker.
Score float64 `json:"score,omitempty"`
// SnippetText is a short inline preview (prompt-injection
// friendly). Omit when the evidence has no readable preview.
SnippetText string `json:"snippet_text,omitempty"`
// ObjectStoreRef is the ObjectStore key for the full body when
// the evidence has bulk content. Empty when the evidence is fully
// expressed in the EntityID + triples on the graph.
ObjectStoreRef string `json:"objectstore_ref,omitempty"`
}
Evidence is one item in a fused evidence set — a single entity hit or evidence snippet the retrieval fan-out surfaced. Provenance is mandatory: every evidence item carries enough metadata (EntityID + Tier + Source) for the caller to verify it back against the graph and for downstream consumers (e.g. synthesize_answer's quote-back validation) to reject fabricated refs.
ObjectStoreRef is set when the body of the evidence (long text, document, etc.) lives in ObjectStore; consumers read it via that ref. SnippetText carries a short inline preview for prompt-injection readability without triggering ObjectStore round-trips on every hit.
type Facet ¶
type Facet string
Facet names one of the three edge-walk facets the engine derives from a lens's EdgeSpecs. It is a distinct type from Want (so WantBody, which is not edge-driven, is unpassable), but its values are DERIVED from the matching Want constants below so the two vocabularies cannot drift. (WantBody has no Facet.)
type FuseOptions ¶
type FuseOptions struct {
// MaxParallelism caps concurrent sub-query execution. 0 uses DefaultMaxParallelism.
MaxParallelism int
// MaxResultsPerSubquery caps result count per sub-query before
// ranking + budget enforcement run. 0 uses DefaultMaxResultsPerSubquery.
MaxResultsPerSubquery int
// BudgetTokens is the estimated-token budget for the returned
// evidence set. 0 uses DefaultBudgetTokens.
BudgetTokens int
}
FuseOptions controls the engine's concurrency, per-subquery result cap, and evidence budget. Zero values fall through to the Default* constants.
type FuseResult ¶
type FuseResult struct {
// Evidence is the kept evidence set after dedup, sort, and budget
// enforcement. May be empty (degraded path or zero queries).
Evidence []Evidence
// Degraded flags incomplete fan-out — e.g. one or more sub-query
// errors or a timeout. The caller should surface this to operators
// or downstream LLM consumers as low-confidence input.
Degraded bool
// DegradedReason explains why Degraded is set. Multiple per-sub-query
// failures are joined with "; ".
DegradedReason string
// BudgetTokensUsed is the estimated token cost of the kept evidence
// array after budget enforcement. Useful for operator observability
// and tuning budget parameters.
BudgetTokensUsed int
}
FuseResult is the engine's output: dedup'd + ranked + budget-enforced evidence, plus observability fields.
func Fuse ¶
func Fuse(ctx context.Context, gq GraphQueryClient, queries []SubQuery, opts FuseOptions, logger *slog.Logger) (FuseResult, error)
Fuse runs the materialized sub-query set in parallel, deduplicates by EntityID (across tiers), sorts by tier + score + entity-ID tie-break, enforces the token budget, and returns a FuseResult.
gq must not be nil. An empty queries slice is not an error — it returns FuseResult{Degraded: true, DegradedReason: "..."}.
Per-sub-query failures are degrading, NOT chain-failing: they set FuseResult.Degraded and append to DegradedReason without cancelling sibling sub-queries. The only hard-fail is a parent-context cancellation, which returns a non-nil error.
The logger parameter is optional; nil uses slog.Default().
type GraphEdge ¶
type GraphEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Predicate string `json:"predicate"`
Direction string `json:"direction"`
Evidence []GraphEvidence `json:"evidence,omitempty"`
Truncated bool `json:"truncated,omitempty"`
}
GraphEdge is one directed relationship. Source and Target are node handles in TRUE subject→object orientation regardless of which side the seed is on; Direction reports the seed's side; when BOTH endpoints are seeds the edge is discovered outgoing-first, so Direction reads "outgoing" relative to the subject seed (deterministic first-discovery). Edge identity is (source, predicate, target) — rendered as the stable ID "<source>|<predicate>|<target>" — so parallel predicates between the same pair are distinct edges, opposite directions are distinct edges with swapped Source/Target, and multiple assertions of the same edge merge into ONE edge with multiple inspectable Evidence entries. Truncated reports the per-edge evidence cap.
type GraphEvidence ¶
type GraphEvidence struct {
Source string `json:"source,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Confidence *float64 `json:"confidence,omitempty"`
Context string `json:"context,omitempty"`
}
GraphEvidence is one stored assertion's provenance, projected verbatim: absent values are OMITTED from the wire, never defaulted or synthesized. Confidence is a pointer so an absent confidence is distinguishable from an asserted value — the stored float zero value is the unset default and is therefore omitted, never fabricated as a literal 0. Timestamp is RFC 3339 and omitted when the stored time is zero.
type GraphFact ¶
type GraphFact struct {
Predicate string `json:"predicate"`
Value json.RawMessage `json:"value"`
Datatype string `json:"datatype,omitempty"`
Evidence []GraphEvidence `json:"evidence,omitempty"`
// Truncated reports the per-fact evidence cap was hit; surviving entries
// are a prefix of the discovered contributions, and the projection-level
// Truncated flag is set alongside.
Truncated bool `json:"truncated,omitempty"`
}
GraphFact is one typed property fact: the ORIGINAL predicate verbatim, the value as typed JSON passthrough (no coercion, no stringification), the verbatim datatype (absent stays absent), and the contributing evidence. Multi-valued predicates produce multiple facts; duplicate assertions of the same (predicate, value) merge into one fact carrying one evidence entry per contributing triple.
type GraphNode ¶
type GraphNode struct {
Handle string `json:"handle"`
Facts []GraphFact `json:"facts,omitempty"`
// FactsTruncated reports the fact list was capped; FactsDropped counts the
// distinct facts cut. Both are omitted when nothing was dropped.
FactsTruncated bool `json:"facts_truncated,omitempty"`
FactsDropped int `json:"facts_dropped,omitempty"`
}
GraphNode is one projected seed entity: the same opaque Handle as Node.Handle (never parse or construct it) plus the entity's typed property facts. FactsTruncated + FactsDropped report the per-node fact cap, so a consumer can always tell a small entity from a capped one.
type GraphProjection ¶
type GraphProjection struct {
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges,omitempty"`
ViewRevision ViewRevision `json:"view_revision"`
// Truncated reports that ANY node- or edge-level cap below cut content
// (facts, edges, or evidence), or that an edge walk faulted and left the
// edge set incomplete — a partial projection never reads as exact.
Truncated bool `json:"truncated"`
}
GraphProjection is the optional structured graph facet carried on Response.Graph when the request Wants "graph". Nodes are the response's seed entities (same opaque handles as Node.Handle); Edges are the directed relationships around them, in true subject→object orientation. The projection is lossless but bounded: its caps are independent of the v1 node/body budget, and every cut is reported through FactsTruncated / FactsDropped / Truncated — never silently.
ViewRevision carries revision OBSERVATIONS, not a consistency claim: the engine assembles a projection from N independent reads with no snapshot and no read transaction, so no response field can prove the reads hit one indexed revision (ADR-083). A consumer that needs a genuinely coherent single-revision view must use pkg/graphview (ADR-081); this facet is best-effort ranked evidence.
type GraphQueryClient ¶
type GraphQueryClient interface {
EntityState(ctx context.Context, args EntityStateArgs, tier, source string, limit int) ([]Evidence, error)
PredicateWalk(ctx context.Context, args PredicateWalkArgs, tier, source string, limit int) ([]Evidence, error)
TemporalRange(ctx context.Context, args TemporalRangeArgs, tier, source string, limit int) ([]Evidence, error)
BM25(ctx context.Context, args BM25Args, tier, source string, limit int) ([]Evidence, error)
}
GraphQueryClient is the narrow retrieval surface the fusion engine consumes. Production implementations wrap NATS-direct subjects; tests substitute in-memory fakes so the test matrix doesn't need a live graph stack.
Per-method semantics:
- EntityState: returns the current Triples for the named entities, projected into Evidence (one per entity).
- PredicateWalk: from each seed, returns Evidence for entities reachable within MaxHops via Predicates (empty Predicates = all).
- TemporalRange: returns Evidence for entities within the time window, optionally filtered by topic.
- BM25: text search via graph-query's existing BM25 surface.
All methods MUST stamp Tier + Source on every returned Evidence (taken from the SubQuery they were dispatched for) so provenance stays honest end-to-end. The engine does NOT re-stamp.
type Hydration ¶
type Hydration struct {
// Entities are the hydrated entities IN REQUESTED ORDER.
Entities []*Entity
// Unhydrated names every requested ID absent from Entities. Nil when complete.
Unhydrated []Unhydrated
}
Hydration is a batch fetch's outcome: the entities that loaded, in requested order, plus every requested ID that did not, with a reason.
The two lists together account for every requested ID exactly once. That totality is the point: before it, an ID whose read came back not-found was simply missing from a shorter slice, and no caller could tell which one — or whether anything had gone wrong at all (gh#597).
type Impact ¶
type Impact struct {
Nodes int `json:"nodes"`
Files int `json:"files"`
Truncated bool `json:"truncated"`
}
Impact summarizes the transitive reverse-relation closure of the response's seeds — the blast radius of changing them — carried on Response.Impact. It counts distinct affected nodes and files (a lens Location path); Truncated marks the node cap was hit, so the counts are a lower bound.
type IndexState ¶
type IndexState string
IndexState mirrors the graph readiness phase.
const ( StateBuilding IndexState = "building" StateReady IndexState = "ready" StateDegraded IndexState = "degraded" StateResetRequired IndexState = "reset_required" )
The readiness phases. NOTE: none of them licenses a not-found conclusion — see IndexStatus.Ready. Ready reports COVERAGE (the index applied every committed revision up to its target); it says nothing about whether a source ever published the thing you were looking for, which is the question an absence claim actually turns on (ADR-084).
type IndexStatus ¶
type IndexStatus struct {
Ready bool `json:"ready"`
State IndexState `json:"state"`
Code string `json:"code,omitempty"`
Reason string `json:"reason,omitempty"`
// BootstrapComplete reports whether the producer finished its initial build in
// its current process lifetime (ADR-084 D2) — the wire-observable form of the
// gh#474 cutover window. Absent reads false (fail closed). See
// graph.IndexStatusResponse.BootstrapComplete for the full contract; fusion
// mirrors the field verbatim so the direct decode keeps working.
BootstrapComplete bool `json:"bootstrap_complete"`
// IndexedRevision / TargetRevision / Lag expose the exact revision-lag so a
// caller that knows its own target revision can gate on IndexedRevision >=
// myRev rather than the coarse global Ready bool (ADR-066).
IndexedRevision uint64 `json:"indexed_revision,omitempty"`
TargetRevision uint64 `json:"target_revision,omitempty"`
Lag uint64 `json:"lag,omitempty"`
// FailedCount / FailedReasons / FirstFailureAt relay a producer's BOUNDED failure
// detail (#613): a degraded producer's outage-vs-poison-entities breakdown reaches
// the operator through the response envelope fusion already attaches, decoded
// field-identically from the producer's GRAPH_STATUS envelope (no new endpoint).
// All additive/omitempty — a producer without failures (e.g. graph-index) carries
// none, so the wire is unchanged. See graph.IndexStatusResponse for the full
// contract; fusion mirrors the fields verbatim so the direct decode keeps working.
// Field ORDER matches graph.IndexStatusResponse (between Lag and StalenessMs) so the
// gate-projection round-trip stays byte-identical.
FailedCount uint64 `json:"failed_count,omitempty"`
FailedReasons map[string]uint64 `json:"failed_reasons,omitempty"`
FirstFailureAt string `json:"first_failure_at,omitempty"`
// StalenessMs is the age of the view in milliseconds (ADR-083). 0 carries no
// information (Ready, or not computable); any computed staleness is >= 1ms.
// See graph.IndexStatusResponse.StalenessMs for the full presence encoding —
// fusion mirrors that field verbatim so the direct decode keeps working.
StalenessMs uint64 `json:"staleness_ms,omitempty"`
Phase string `json:"phase,omitempty"`
Revision string `json:"revision,omitempty"`
LastSynced string `json:"last_synced,omitempty"`
}
IndexStatus is attached to every response: the honesty envelope a caller uses to calibrate trust.
Ready means the index is CAUGHT UP (revision-lag, ADR-066), not merely started. It does NOT license an authoritative not-found, and a response is no longer withheld merely because it is false — ADR-084 retired that license and regated reads on HEALTH (State + BootstrapComplete). Coverage cannot answer absence: an index caught up to every revision ever committed still knows nothing about a source that never published. Callers wanting "is my write visible?" compare their own revision against IndexedRevision; callers wanting "is this data fresh enough?" read StalenessMs.
Field-identical to graph.IndexStatusResponse — the RetrievalClient decodes the producer's envelope directly into this; the two change together.
type Lens ¶
type Lens interface {
// Name identifies the lens (e.g. "code", "docs"). Used as the registry key.
Name() string
// ResolveMode picks how to turn the raw query into seeds.
ResolveMode(query string) ResolveMode
// Edges are the relationship predicates to expand around each seed.
Edges() []EdgeSpec
// Label is the entity's human name (e.g. from dc.terms.title).
Label(e *Entity) string
// Kind is a short human kind for the entity (e.g. "function", "doc").
Kind(e *Entity) string
// Location returns the entity's domain-general place (file path or URL,
// optional section fragment, optional line range).
Location(e *Entity) Locator
// Hydrate returns a HANDLE to the entity's verbatim body — never the bytes
// themselves and never a filesystem read. The engine resolves the handle's
// StorageInstance to a registered storage.Store and reads the body with
// Get(Key) (ADR-062 hydration contract, increment 4). Returning a handle
// (not a string) is what makes fusion deployment-independent: a remote
// caller of a standalone fusion service (semsource ADR-0006) cannot read the
// service's worktree, so bodies must be addressable through a backend-
// agnostic store. Return (nil, nil) when the entity has no verbatim body.
//
// Error policy (engine contract): hydration is best-effort. A non-nil error
// DEGRADES the response — the engine omits that node's body and continues —
// it does NOT fail the fuse. This mirrors the engine's degrade-don't-fail
// posture for sub-query failures (see Fuse). Lenses should return an error
// only for genuine retrieval faults, not for "no body" (use (nil, nil)).
Hydrate(ctx context.Context, e *Entity) (*message.StorageReference, error)
}
Lens supplies the only domain-specific parts of fusion (ADR-062). The engine owns resolve / expand / rank / budget / envelope; the lens declares which edges to walk, how to read an entity's human-facing fields, and how to hydrate its verbatim body.
type Locator ¶
type Locator struct {
Path string // file path or URL
Fragment string // section / anchor (docs)
Lines [2]int // line range (code); zero value means line-less
}
Locator is a domain-general place: a file path or URL, an optional section / anchor fragment (docs), and an optional line range (code). One of Lines or Fragment is typically set, not both.
type MapStoreResolver ¶
MapStoreResolver is a static StoreResolver over a name→Store map. The zero value (nil map) resolves nothing. Safe for concurrent reads (Go map reads are safe without writers; the map is build-once at wiring time).
type Miss ¶
type Miss struct {
Query string `json:"query"`
DidYouMean []string `json:"did_you_mean,omitempty"`
}
Miss reports a query that resolved to nothing, with near-matches.
A Miss is NOT an absence proof and is no longer tied to Ready (ADR-084): it is reachable under ordinary lag now that healthy-but-behind indexes serve, and it was never evidence the entity does not exist — only that this lookup found nothing. Callers that must distinguish "found nothing" from "could not read" want Unhydrated, which is a statement about the read.
type Node ¶
type Node struct {
Name string `json:"name"`
Kind string `json:"kind,omitempty"`
Path string `json:"path,omitempty"`
Fragment string `json:"fragment,omitempty"`
// Lines is [start,end] for code, nil for line-less domains (docs) — a slice
// so omitempty actually omits it rather than emitting a spurious [0,0].
Lines []int `json:"lines,omitempty"`
Body string `json:"body,omitempty"`
// BodyReason names why Body is absent when a body was requested (WantBody) but
// could not be loaded — a bounded, closed reason (not_found / error) so a
// missing body is a partial-result SIGNAL, never a silent empty string
// (gh#616, #600). It is DISTINCT from Response.Unhydrated: a missing body is a
// node that exists and ranks, so the reason rides on the node itself rather
// than the top-level list. A failed body hydration never defers the response
// or synthesizes a Miss — the node still ships.
//
// Omitted from the wire when the body hydrates, so a fully-hydrated response is
// byte-unchanged for existing consumers.
BodyReason BodyReason `json:"body_reason,omitempty"`
Relations map[string][]Ref `json:"relations,omitempty"`
// Class is the BFO/CCO class IRI (provenance/debug; the agent ignores it).
Class string `json:"class,omitempty"`
// Handle is an opaque continuation token (internally the entity ID). Not an
// addressing scheme: never parse or construct it.
Handle string `json:"handle,omitempty"`
// Rank is this node's 1-based position in RESOLVE order — where the index put it
// before the engine re-ranked. Present only when the request set IncludeScores.
//
// Deliberately not the node's position in the response: that is the array index,
// which the caller can already count. Ranking reorders (lexical, ontology,
// salience), so the GAP between resolve rank and response position is the whole
// signal — it is what makes "why did this come out third?" answerable.
Rank int `json:"rank,omitempty"`
// Similarity is the resolve mode's own relevance score, present only when
// IncludeScores was set AND the mode reports one (semantic does; symbol and prefix
// do not). Joined to this node by entity ID, never by position.
//
// A POINTER so the wire is self-describing: absent means the mode does not score,
// present means this is the score — including a genuine 0.0, which a bare
// `float64,omitempty` would erase into indistinguishability from "unavailable".
// A separate has_similarity bool round-trips correctly in Go but forces every
// non-Go consumer to learn that an absent key means zero rather than nothing.
Similarity *float64 `json:"similarity,omitempty"`
}
Node is one fused result: verbatim body plus the structure around it. Domains differ only in which roles populate Relations (code: callers/callees; docs: links/sections).
type Path ¶
Path is a bounded outgoing relation walk from a seed, rendered as a sequence of human names (never entity IDs), carried on Response.Paths. Truncated marks a path cut short at the depth cap or a cycle — the partial path is kept, not dropped.
type PredicateWalkArgs ¶
type PredicateWalkArgs struct {
Seeds []string `json:"seeds"`
Predicates []string `json:"predicates,omitempty"`
MaxHops int `json:"max_hops,omitempty"`
}
PredicateWalkArgs carries seed IDs + optional predicate filter. MaxHops 0 resolves to 1 at the executor.
type Provenance ¶
type Provenance string
Provenance records how an answer was produced so callers can calibrate trust.
const ( ProvenanceDeterministic Provenance = "deterministic" // exact lookup + structural walk ProvenanceEmbedding Provenance = "embedding" // seeds came from semantic search ProvenanceLLM Provenance = "llm" // an LLM reasoned over the result )
The provenance tiers, in increasing order of uncertainty.
func ProvenanceForMode ¶
func ProvenanceForMode(mode ResolveMode) Provenance
ProvenanceForMode maps a ResolveMode to the honesty-envelope provenance tier: symbol/prefix seeds are deterministic exact lookups; nl seeds come from embedding search. (The research_graph LLM sibling is the only "llm" source — the deterministic engine never sets it.) Exported so the engine's assemble step and any lens-driven caller stamp provenance consistently.
Deliberate divergence from semsource's provenanceFor (source/fusion/engine.go), whose catch-all is "deterministic" and only embedding is named: there, ResolveMode is a CLOSED int enum so "unknown" is unreachable. Here ResolveMode is an OPEN string type, so the catch-all must be the conservative tier — "embedding" — because provenance is a claim about HOW an answer was produced, and an unclassified seed has not earned the stronger claim. (The reasoning once rested on "only deterministic permits an authoritative not-found"; ADR-084 retired that license entirely — no provenance tier licenses an absence claim — but the conservative default stands on its own.) CAUTION: this means any NEW deterministic mode added to the enum MUST be added to the deterministic case below, or it silently degrades to embedding (no compile error — open string enum).
type RankSignals ¶
type RankSignals interface {
// ClassSpecificity scores how specific an ontology class IRI is: more
// specific (deeper in the BFO/CCO subclass tree) = higher; 0 for an
// unknown or unset class. A deeper class carries more information, so within
// the same resolve/lexical tier a precisely-typed entity reorders ahead of a
// vaguely-typed peer.
ClassSpecificity(classIRI string) float64
// PredicateSalience returns a predicate's stored salience weight, SIGNED:
// positive boosts (an entity carrying the fact reorders ahead), negative
// demotes (it reorders behind), 0 is neutral. Production reads vocabulary
// PredicateMetadata.Weight. A negative weight is how a consumer down-ranks
// structurally-identifiable noise (tests, generated code, mocks) that carries
// the same boosted predicates as the real thing — additive boosting alone
// cannot separate them (gh#441). The engine folds an entity's strongest boost
// and strongest demotion together (see entitySalience), so a demotion is a
// bounded secondary reordering, never an exclusion.
PredicateSalience(predicate string) float64
}
RankSignals supplies the framework ranking signals the lens engine folds into rankEntities on top of resolve-order + lexical: ontology specificity and predicate salience (ADR-062 increment 5, gh#396). Injected through an interface so pkg/fusion stays a leaf (no vocabulary / bfo / cco imports); production wires the vocabulary registry + BFO/CCO subclass helper (see pkg/fusion/fusionvocab). A nil RankSignals leaves ranking at resolve-order + lexical — the increments 1–4 behavior — so attaching signals is purely additive to the pipeline (nil → unchanged). The salience signal itself is DIRECTIONAL: a predicate's weight is signed, so it can down-rank as well as boost (gh#441).
type Ref ¶
type Ref struct {
Name string `json:"name"`
Path string `json:"path,omitempty"`
Fragment string `json:"fragment,omitempty"`
Line int `json:"line,omitempty"`
}
Ref points to a node by what a human reads — never the entity ID.
type Request ¶
type Request struct {
Query string `json:"query"`
Want []Want `json:"want,omitempty"`
// Scope optionally constrains NL seed resolution to entities whose ID
// matches at least one of these dot-delimited entity-ID prefixes
// (OR-matched). Empty/absent means no filter — today's behavior. It lets a
// lens instance over a shared embedding index retrieve only its own domain
// so a smaller domain is not diluted by a larger co-resident one (ADR-071).
// A list, not a scalar: one domain often spans several prefixes (semsource
// "all code" = golang/python/ts/svelte). NL-only; ignored by symbol/prefix
// resolve modes.
Scope []string `json:"scope,omitempty"`
Budget Budget `json:"budget,omitzero"`
// IncludeScores asks for per-node rank and resolve similarity. Opt-in so the
// default response is byte-unchanged: scores are debugging and calibration
// signal, and most callers act on the ordering rather than the numbers.
IncludeScores bool `json:"include_scores,omitempty"`
}
Request is the fused query, keyed by what an agent already knows — never an entity ID.
type ResolveMode ¶
type ResolveMode string
ResolveMode picks how the engine turns a raw query string into seed entities. The Lens classifies the query; the engine maps the mode to a resolve strategy (symbol → graph.query.byName exact lookup, prefix → graph.query.prefix, nl → semantic search). Provenance follows from the mode: symbol/prefix are deterministic, nl is embedding.
const ( // ResolveModeNL routes the query to semantic (embedding) search. Seeds are // embedding-ranked, so the response provenance is "embedding", not // "deterministic". ResolveModeNL ResolveMode = "nl" // ResolveModeSymbol routes to deterministic exact name/title lookup // (graph.query.byName). The provenance the byName index makes honest. ResolveModeSymbol ResolveMode = "symbol" // ResolveModePrefix routes to deterministic prefix lookup // (graph.query.prefix) — e.g. a path or namespace stem. ResolveModePrefix ResolveMode = "prefix" )
type ResolveQuery ¶
type ResolveQuery struct {
Query string
Mode ResolveMode
Scope []string
Limit int
}
ResolveQuery is the argument set for RetrievalClient.Resolve. Mode selects the resolve strategy; Scope is honored only for ResolveModeNL (a filter on embedding candidates), where empty/nil means no filter.
type Response ¶
type Response struct {
Index IndexStatus `json:"index"`
Provenance Provenance `json:"provenance"`
Nodes []Node `json:"nodes,omitempty"`
Paths []Path `json:"paths,omitempty"`
Impact *Impact `json:"impact,omitempty"`
// Graph is the structured graph projection (WantGraph): typed property
// facts, explicit directed edges, and verbatim evidence, with its own caps
// and view-revision contract. Its truncation metadata is self-contained —
// graph-facet truncation NEVER sets the top-level Truncated bit, and a
// request without the graph want omits the field entirely (the v1 wire
// shape is unchanged for non-requesting callers).
Graph *GraphProjection `json:"graph,omitempty"`
Misses []Miss `json:"misses,omitempty"`
// Unhydrated names requested seeds that did not load — DISTINCT from Misses. A
// Miss says "the graph was asked and had nothing"; an Unhydrated entry says "we
// could not read this one", a statement about the read rather than about the
// world. Neither licenses an absence conclusion (ADR-084), and the
// all-seeds-unhydrated case deliberately synthesizes NO Miss: claiming a miss
// there would assert exactly the thing the failed read left unknown.
//
// Omitted when everything hydrated, so a complete response is byte-unchanged.
Unhydrated []Unhydrated `json:"unhydrated,omitempty"`
// Deferred reports that the engine WITHHELD rather than answered: the empty
// result is a refusal to look, not a finding.
//
// It is an explicit field because the honesty envelope cannot carry the fact.
// Index describes the GRAPH-INDEX producer, and a defer can be caused by
// something else entirely — an internal read against graph-ingest or
// graph-embedding returning the readiness transient while graph-index is
// perfectly healthy. Re-sampling graph-index then yields a HEALTHY envelope
// attached to an empty response, so a consumer applying the canonical gate to
// Index would conclude "healthy, and it found nothing" — precisely the
// misreading ADR-084 exists to prevent, arrived at from the other side.
//
// Check this, not Index, to answer "did I get an answer?".
Deferred bool `json:"deferred,omitempty"`
// DeferReason is why, from graph.DeferReason's closed set plus the engine-level
// causes (an internal dependency's readiness transient). Empty when not deferred.
DeferReason string `json:"defer_reason,omitempty"`
Truncated bool `json:"truncated"`
ContractVersion string `json:"contract_version"`
}
Response is the fused answer. Nodes is the payload; Index and Provenance are the honesty envelope. Paths, Impact, and Graph are optional facets, present only when the request Wants them (WantPaths / WantImpact / WantGraph).
type RetrievalClient ¶
type RetrievalClient interface {
// Status reports graph readiness for the honesty envelope. Callers gate on
// HEALTH (graph.EvaluateReadinessGate), not on the Ready coverage bit — which
// never licensed a not-found conclusion and no longer withholds a response
// (ADR-084). A quiet or unvouchable feed returns ErrReadinessUnknown; a wiring
// failure returns a plain error.
Status(ctx context.Context) (IndexStatus, error)
// Resolve maps a query to seeds, most relevant first. Both its argument and its
// result are structs rather than bare values, for the same reason: the NL-only
// Scope must not force symbol/prefix callers to pass an ignored value, and a
// future resolve dimension should add a field instead of re-breaking every impl
// and fake (ADR-071).
Resolve(ctx context.Context, q ResolveQuery) ([]Seed, error)
// Entity returns an entity by ID, or (nil, nil) if the read found nothing.
// "Found nothing" is not proof of absence — see Hydration.
Entity(ctx context.Context, id string) (*Entity, error)
// Entities batch-fetches entities by ID and reports what it could not hydrate.
// A non-nil error means a BACKEND failure, which callers MUST distinguish from
// an entity that is simply absent.
//
// It returns a struct rather than a bare slice for the same reason Resolve takes
// one (see above): partial hydration needed a second output, and a future one
// should add a field instead of re-breaking every impl and fake.
//
// IMPLEMENTATIONS MUST return Entities in the REQUESTED ORDER. The engine's
// resolve-rank base is position-derived, so hydration order is the ranking prior,
// not a presentation detail — a transport that returns "whatever order was
// convenient" silently reorders results by cache residency (see
// fusionnats.Client.Entities, which restores order for exactly this reason).
Entities(ctx context.Context, ids []string) (Hydration, error)
// Neighbors returns edges from id along the given predicates in a direction.
Neighbors(ctx context.Context, id string, predicates []string, dir Direction) ([]Edge, error)
// Names suggests entity display names near a query (for a miss's did_you_mean).
Names(ctx context.Context, query string, limit int) ([]string, error)
}
RetrievalClient is the resolve/expand surface the lens-driven Engine composes over (ADR-062 lens-driven entry). It is DISTINCT from GraphQueryClient (the sub-query executor surface the package-level Fuse consumes): this one maps a query to seeds and walks structure, whereas GraphQueryClient runs pre-built sub-queries. The ADR's eventual convergence unifies them; for now they serve the two engine entries side by side.
Production wraps NATS request/reply (graph.query.{status,byName,prefix, semantic,batch,relationships,entity}); tests use an in-memory fake. Keeping the engine behind this interface is what lets it stay deterministic and unit-testable without a live graph (the production impl + the readiness status subject are PR B / gh#397).
type Seed ¶
type Seed struct {
// ID is the resolved entity ID.
ID string
// Similarity is the resolve mode's own relevance score, when it has one.
// Semantic resolve reports cosine similarity; symbol and prefix report none.
Similarity float64
// HasSimilarity distinguishes "this mode scored it 0" from "this mode does not
// score". Without it a prefix seed would surface as a perfect zero-relevance
// match, which is a claim the prefix wire never made.
HasSimilarity bool
}
Seed is one resolved candidate, most relevant first.
It carries Similarity because the graph.query.semantic wire already reports a per-result score and the resolve surface was throwing it away — the engine could rank only by the ORDER seeds arrived in, so a caller asking "how confident is this match?" had nothing to read. Modes that carry no score (symbol, prefix) leave it zero, which is why HasSimilarity exists rather than treating 0 as a value.
type StoreResolver ¶
type StoreResolver interface {
// Store returns the store registered under instance, and whether one exists.
Store(instance string) (storage.Store, bool)
}
StoreResolver maps a StorageReference.StorageInstance name to the storage.Store that holds its data. Wiring supplies it — typically MapStoreResolver over the deployment's registered stores. Kept as an interface so the engine never assumes one backend and tests can substitute fakes.
Canonical key (gh#376 coordination with semsource): StorageInstance is the storage COMPONENT INSTANCE NAME (e.g. "objectstore", "filestore-media"), per StorageReference's own doc ("identifies which storage component holds the data … enables federation across multiple storage instances") — NOT the bucket name. Producers stamp the component instance name; wiring registers each store under that same name. (semstreams' own auto-stamp is inconsistent today — component.go uses the instance name, store.go uses the bucket — tracked separately; this helper, the first consumer, fixes the convention at instance-name.)
type SubQuery ¶
type SubQuery struct {
Type SubQueryType `json:"type"`
Tier string `json:"tier"` // "0" (predicate) or "1" (BM25)
Source string `json:"source"` // e.g. "walk_seeds:drone-001"; surfaces on Evidence.Source
// Per-type args. Exactly one is populated based on Type — the
// executor switch enforces. Optional fields allow tests + future
// templates to omit per-type primitives that don't apply.
EntityState *EntityStateArgs `json:"entity_state,omitempty"`
PredicateWalk *PredicateWalkArgs `json:"predicate_walk,omitempty"`
TemporalRange *TemporalRangeArgs `json:"temporal_range,omitempty"`
BM25 *BM25Args `json:"bm25,omitempty"`
}
SubQuery is a typed retrieval request. Each variant carries the minimum fields its tier executor needs; the Type discriminator drives dispatch in the engine's executeSubQuery.
Tier and Source travel with the SubQuery so the per-tier executor can stamp them onto every Evidence it produces without re-deriving — keeps provenance honest end-to-end.
type SubQueryType ¶
type SubQueryType string
SubQueryType is the closed set of Tier 0+1 primitives. Phase 2 adds spatial_polygon + neural; Phase 1 stays minimal.
const ( // SubQueryTypeEntityState fetches current state of named // entities. Tier 0; used for walk_seeds anchoring + decompose's // entity_type axis when classifier candidates are present. SubQueryTypeEntityState SubQueryType = "entity_state" // SubQueryTypePredicateWalk traverses predicate(s) from seed // entities. Tier 0; used for walk_seeds neighborhood expansion + // decompose's free-form axis fallback. SubQueryTypePredicateWalk SubQueryType = "predicate_walk" // SubQueryTypeTemporalRange queries entities within a time // window. Tier 0; used for decompose's time axis. Phase 1 ships // the type but the executor falls through to BM25 on topic when // graph-index-temporal isn't wired — operators see the // "temporal degrade" hint on the produced Evidence. SubQueryTypeTemporalRange SubQueryType = "temporal_range" // SubQueryTypeBM25 text-searches via graph-query's existing // BM25 surface. Tier 1; always added to widen coverage beyond // purely-structural retrieval (intent-shaped routing can miss // keyword matches the classifier surfaced). SubQueryTypeBM25 SubQueryType = "bm25" )
type TemporalRangeArgs ¶
type TemporalRangeArgs struct {
// Start / End are RFC3339 strings; timezone handling stays in the
// executor where the upstream graph-index-temporal surface lives.
Start string `json:"start"`
End string `json:"end"`
Topic string `json:"topic,omitempty"`
}
TemporalRangeArgs carries start/end + an optional topic filter. Empty Topic widens to all entities in the window (may be heavy; callers should always pass a topic in practice).
type Unhydrated ¶
type Unhydrated struct {
// Handle is the opaque token for the seed that did not load. Never parse or
// construct it.
Handle string `json:"handle"`
Reason UnhydratedReason `json:"reason"`
}
Unhydrated names one seed that did not load.
The field is Handle, not ID, deliberately: fusion's contract keys everything a consumer touches by what a human reads or by an opaque token, never by a parseable entity ID (see Node.Handle). The value is the same string the batch wire calls `id` — the layer boundary is the point. Naming it `id` on the product surface would invite consumers to parse and construct entity IDs, which is exactly what the handle convention exists to prevent.
type UnhydratedReason ¶
type UnhydratedReason string
UnhydratedReason is why one requested ID did not hydrate. The set is CLOSED and mirrors graph.MissingReason value-for-value (pinned by a test) — fusion keeps its own type because it is the product-facing contract and its wire name differs (`unhydrated` here, `missing` on the batch subject).
const ( // UnhydratedNotFound is a read that did not find the key. It does NOT license the // conclusion that the entity never existed — see the Response.Unhydrated docs. UnhydratedNotFound UnhydratedReason = "not_found" // UnhydratedError is a per-ID fault that did not fail the whole call. Reserved // while the handler's first-error contract stands. UnhydratedError UnhydratedReason = "error" // UnhydratedUnknown is synthesized when a requested ID appears in neither the // handler's entity list nor its missing list — a handler that under-reported. // Naming it beats inventing not_found, which would assert something unobserved. UnhydratedUnknown UnhydratedReason = "unknown" )
The closed unhydrated-reason set.
type ViewRevision ¶
ViewRevision is a pair of observations: the graph's indexed revision as sampled before seed resolution (Start) and re-sampled after the facet's last graph fetch (End). Since ADR-083 distributes status on a heartbeat, both samples are heartbeat-grained — equal bounds mean the status feed did not visibly advance during assembly, which can never prove the reads in between hit one revision. The former Coherent bool claimed exactly that and was removed (ADR-083, third break): two samples agreeing cannot establish the absence of motion between them, before or after the transport change. A failed re-sample reports End=0 — the engine degrades honestly rather than guessing a revision.
type Want ¶
type Want string
Want enumerates the optional facets a caller can request. Empty defaults to body plus immediate relations.
const ( WantBody Want = "body" // verbatim source/passage WantRelations Want = "relations" // callers/callees, links/sections WantPaths Want = "paths" // bounded outgoing relation paths from the seeds WantImpact Want = "impact" // transitive reverse-relation closure of the seeds WantGraph Want = "graph" // lossless structured projection: typed facts + directed edges + evidence )
The requestable facets.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package fusionnats is the production NATS implementation of fusion.RetrievalClient (ADR-062 increment "B2"): it wires the engine's resolve/expand/status surface onto the existing public graph query subjects so the lens-driven fusion engine can run against a live graph.
|
Package fusionnats is the production NATS implementation of fusion.RetrievalClient (ADR-062 increment "B2"): it wires the engine's resolve/expand/status surface onto the existing public graph query subjects so the lens-driven fusion engine can run against a live graph. |
|
Package fusionvocab is the production implementation of fusion.RankSignals (ADR-062 increment 5, gh#396): it wires the lens engine's ranking signals onto the vocabulary registry (predicate salience) and the BFO/CCO subclass helper (ontology specificity).
|
Package fusionvocab is the production implementation of fusion.RankSignals (ADR-062 increment 5, gh#396): it wires the lens engine's ranking signals onto the vocabulary registry (predicate salience) and the BFO/CCO subclass helper (ontology specificity). |
|
Package lensregistry holds the explicit, instance-scoped registry of fusion Lens factories (ADR-062 increment 3).
|
Package lensregistry holds the explicit, instance-scoped registry of fusion Lens factories (ADR-062 increment 3). |