domain

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package domain defines the types and invariants for call graph extraction.

The aggregate root is CallGraphRecord: the static call graph of a module, composed of CallNode and CallEdge value objects plus extraction metadata and a content hash.

Invariants:

- Determinism: CallGraphRecord.Sort establishes a canonical ordering of Nodes and Edges. Sort MUST be called before the content hash is computed (CallGraphRecordHasher hashes the canonical JSON with ContentHash zeroed), so two analyses of the same module produce byte-identical records and the same hash regardless of the order the analyser emitted nodes/edges. - Integrity: a record read back from storage is rejected unless its recomputed canonical hash matches the stored ContentHash.

The package is pure: no I/O, no toolchain invocation, no clock. It reuses coordinate.ModuleCoordinate as the module identity.

Index

Constants

View Source
const (
	// ConflictFieldAnalysisSource is a record naming a kind of source this build
	// cannot read.
	ConflictFieldAnalysisSource = "analysis_source"
	// ConflictFieldArtefactIdentity is two sets of bytes for one pinned version.
	ConflictFieldArtefactIdentity = "artefact_identity"
	// ConflictFieldCallGraph is two analyses of the same artefact, at the same
	// completeness, disagreeing about the graph.
	ConflictFieldCallGraph = "call_graph"
)

The field names a CallGraphConflict can carry. They are constants rather than literals at the three sites that raise them so that Remedy, and the contract test that pushes every remedy line through the CLI's own parser, enumerate the set from the code instead of from a hand-copied list a new conflict would fall quietly outside of.

View Source
const CallGraphSchemaVersion = "13"

CallGraphSchemaVersion is the version of the CallGraphRecord JSON schema. Bump when the serialisation format changes in a backwards-incompatible way. v4 adds the ecosystem scope marker. v5 adds the per-node body-level fact fields (UsesUnsafePointer, IsAssemblyOrLinkname) used by capability analysis. v6 adds FailedPackages: the machine-readable set of packages that failed to typecheck, so verdicts over a Partial graph can be scoped to the exact packages whose edges were dropped rather than inferred from node/edge totals. v7 redesigns the edge confidence vocabulary (Direct, CHA-overapprox, VTA, Framework, Unknown), folds reflect-dispatched edges into Unknown, and records the reflect origin as the per-edge ReflectDispatch attribute. v8 adds Completeness: the per-module fidelity level (BUILT_WITH_BODIES, TYPE_ONLY, METADATA_ONLY, FAILED, VERSION_NOT_IN_TOOLCHAIN) so a before/after diff can assert completeness parity rather than read a "resolved"/"unaffected" verdict off an asymmetric comparison. v9 adds UsesPlugin: the per-node body-level fact that a function references the Go plugin package (plugin.Open / (*Plugin).Lookup), a soundness leaf sink that loads code the static graph never sees. v10 adds ArtifactKind: whether the analysed module builds a command (application) or is consumed as a library. Reachability roots are conditioned on it, so an application roots all of its own code and capability sinks the runtime dispatches to dynamically are no longer false-negatives. v11 qualifies anonymous-function node IDs with their enclosing function's identifier, so a closure declared in a method keeps that method's receiver. Previously closures in same-named methods on different receivers collapsed onto one ID and merged the edge sets of unrelated functions. Node IDs are persisted and hashed, so the identities themselves change. v12 stops flagging closures and other SSA-synthetic functions as IsExportedAPI. A closure is not nameable by a consumer, so rooting library reachability at one asserts a path that cannot be triggered. v13 opens two axes the graph previously left unmeasured and unstated. CallNode.IsTest plus CallGraphRecord.TestScope bring _test.go declarations into the graph as first-class nodes and record whether that axis was measured at all, so a callers query over a symbol only tests exercise is no longer a confident RESOLVED-ABSENT. Interfaces and Implementations make interface types and their method sets addressable, so "which concrete types must change with this port" is answerable from the graph rather than from a grep.

AnalysisSource and WorktreeDigest joined the record WITHOUT a bump, and that is deliberate. Both are json omitempty, so a v13 record re-marshals to the bytes it was sealed over and still verifies; and both read as "not recorded" when absent, which is the truth about a record written before the field existed. Bumping would make every stored record unreadable through the schema gate — a purge by another name, and the opposite of what an append-only ledger is for. Bump only when a change makes an OLD record say something false, not merely something less.

SynthesisedGoMod joined on the same terms, and for the same reason. It is omitted from the sealed shape when zero, so every stored record re-marshals to the bytes it was sealed over; and an absent value is not an unrecorded third state but the truth — nothing synthesised a go.mod before the field existed, so no old record can be one that did.

View Source
const ExclusionReasonConfig = "excluded_by_config"

ExclusionReasonConfig is the CallGraphRecord.ExclusionReason value used when a module was skipped because its path is listed in callgraph.exclude.

Variables

View Source
var ErrNoRecordsToCompose = errors.New("no call graph records to compose")

ErrNoRecordsToCompose is returned by Compose when handed no records. It is a programming error rather than an absence: absence is reported by the store as "not found", and composing nothing has no meaningful answer.

Functions

func CompletenessCaveat added in v0.3.2

func CompletenessCaveat(level CompletenessLevel, phase AnalysisPhase) string

CompletenessCaveat returns the phase-appropriate caveat for a module analysed below full fidelity, or "" when the level is BUILT_WITH_BODIES (no caveat is warranted). Inclusion degrades and explains; coding instructs a rebuild. This reuses the existing degradation path — the caveat rides alongside the verdict rather than suppressing it.

func CompletenessParity added in v0.3.2

func CompletenessParity(before, after CompletenessDescriptor) (ok bool, reason string)

CompletenessParity reports whether two analysed sides are fidelity-comparable, and when they are not, the specific axis that differs. A diff that produces a "resolved"/"unaffected" verdict must first check parity: a green result across asymmetric fidelity is worse than no answer, because the finding (or its reachability) may have changed only because one side was analysed at lower fidelity. When ok is false, reason names the mismatch for the operator.

func ConflictFields added in v0.7.0

func ConflictFields() []string

ConflictFields lists every field a CallGraphConflict reports on.

func ExternalEntryPointReason added in v0.7.0

func ExternalEntryPointReason(symbol, receiver string) string

ExternalEntryPointReason names why a node is entered from OUTSIDE the analysed module's own call structure, or returns the empty string when nothing about the node says so.

It lives beside SelectReachabilityRoots because it answers the same kind of question — what starts a path — and the two must not drift into different ideas of an entry point. It reads only the node's own identity, which is what makes it usable from a stored record with no source in hand.

The three facts it can witness, and the limit of each:

  • Package initialisation, recognised by IsInitSymbol. The runtime runs it when the package is loaded, so it runs unconditionally — a stronger claim than reachable, not a weaker one. It is named separately from the HTTP case because the two are not equally attacker-facing and a bare "ingress" would flatten them.
  • The process entry point, recognised as a receiverless "main". A function named main outside package main would also match; that is a vet error and the graph does not record which package is the command, so the over-approximation is stated rather than hidden.
  • An http.Handler implementation, recognised as a "ServeHTTP" method. The graph records no signatures, so this is the method NAME, not a proof the type satisfies http.Handler. In Go the name has essentially one meaning, and the reason string says what was matched so a reader can check it.

A registered route — a handler function stored into a router — is NOT witnessed here. Registration is a value flowing into a data structure, and nothing in a node's identity records it; the caller edges do, several hops up. Claiming it from a node fact would be a guess, so a root reached only that way classifies on its callers instead.

func GraphDigest added in v0.6.0

func GraphDigest(r CallGraphRecord) string

GraphDigest is a hash of everything a record says about the call graph, and of nothing else.

It exists because ContentHash cannot answer "do these two records agree". The content hash covers the time of measurement and the provenance of the bytes, so two runs of the analyser that produced the identical graph a second apart carry different content hashes. Blanking those fields leaves the claim.

It is NOT a second seal and is never persisted. It reuses the canonical marshal, so it changes only when the hashed shape does, and no stored record's own hash depends on it.

func IsInitSymbol added in v0.3.0

func IsInitSymbol(symbol string) bool

IsInitSymbol reports whether a symbol name denotes a package init function. The Go compiler names the user-written init as "init" and any additional generated package-initialisation work as "init#1", "init#2", ... All of them run unconditionally when the package is loaded.

func IsModuleExcluded

func IsModuleExcluded(modulePath string, exclude []string) bool

IsModuleExcluded reports whether modulePath is excluded from call-graph analysis by the given exclude list. Matching is by exact module path; the list is operator-supplied module paths (see budgets rationale).

func NormaliseExclusions

func NormaliseExclusions(exclude []string) []string

NormaliseExclusions returns a sorted, de-duplicated copy of the configured callgraph.exclude list with blank entries dropped. Callers store the result on the record so the exclusion policy is reproducible and hashes deterministically regardless of config-file ordering. A nil or all-blank input yields nil (an absent policy, not an empty-but-present one).

func ParseInterfaceMethodID added in v0.6.0

func ParseInterfaceMethodID(id string) (interfaceID, method string, ok bool)

ParseInterfaceMethodID splits an interface-method ID of the form "pkg/path.(Name).Method" into the interface ID "pkg/path.Name" and the method name. It reports false for any other shape, including a concrete method ID with a pointer receiver ("pkg/path.(*Name).Method"), which names a node, not an interface method.

func RecordArtefactIdentity added in v0.6.0

func RecordArtefactIdentity(r CallGraphRecord) (fetchdomain.ArtefactIdentity, error)

RecordArtefactIdentity reads back the artefact identity r was derived from.

The zero identity means the record does not record one — it predates the field, or it describes no fetched artefact — and is returned without error, so absence is a value a caller can test with IsZero rather than an error it has to classify. A field that is present but unreadable is still an error: a corrupt identity must never be mistaken for an absent one.

It is a free function rather than a method because CallGraphRecord is aliased into pkg/kanonarion as a result type, and result types are read shapes that must not grow behaviour.

func RecordIsCacheable added in v0.7.0

func RecordIsCacheable(r CallGraphRecord) bool

RecordIsCacheable reports whether a record may satisfy a later extraction of the same coordinate, or whether that extraction must re-derive instead.

It is the call-graph ledger's answer to the question fetch answers with its own RecordIsCacheable, and it lives here, beside Compose, rather than in the extraction use case: the vuln stage's on-demand call-graph spawner asks the same question from a different context, and a rule each caller re-decides is a rule that holds in whichever of them was edited last.

Three cases, and the third is the one that carries the argument:

  • An environment failure is never cacheable. Its status describes a run that never measured the module, so serving it back makes one bad moment permanent: every later run reports the same toolchain error, and no amount of repairing the environment clears it. Re-attempting costs one analysis on a run that would otherwise have skipped it, and is the only way the record can ever be superseded on its own.

  • A module failure is cacheable, exactly like a successful extraction. A module that genuinely cannot be built is a real, stable finding, and re-deriving it every walk would pay full analysis cost to rediscover it. Keeping that distinction is the whole point of the axis.

  • A record that FAILED and states no cause is not cacheable. It predates the axis, so nothing about it says the module was at fault, and treating "we do not know" as "the module is broken" is the exact collapse this type exists to prevent. It costs one re-attempt per such record, once: the re-attempt writes a record that does state its cause, and from then on the coordinate settles either way. Records that did not fail are unaffected — they are the overwhelming majority, they keep answering, and no purge or pipeline-version bump is owed for any of this.

It is a free function rather than a method, on the same terms as ImplementersOf: CallGraphRecord is a result type carrying facts, and cache policy is behaviour over those facts rather than one of them.

func RecordIsFailure added in v0.7.0

func RecordIsFailure(r CallGraphRecord) bool

RecordIsFailure reports whether a record describes an extraction that produced no call graph at all.

Partial is deliberately absent: a partial graph is a graph, its incompleteness is scoped by FailedPackages, and every query over it is caveated per package. ExcludedByConfig is absent too — a module the operator chose not to analyse is a decision, not a failure, and re-attempting it every run would ignore the decision.

func ResolveSymbolModule

func ResolveSymbolModule(symbolID string, analysedModulePaths []string) (string, bool)

ResolveSymbolModule reports whether symbolID falls under one of the analysed module paths. It returns the longest matching module path and true, or "" and false when no analysed module could contain the symbol.

A module path matches when symbolID equals it or continues with '.' or '/', so "example.com/m" matches "example.com/m.Fn" and "example.com/m/pkg.Fn" but not "example.com/much.Fn". This lets callers distinguish "the symbol's module was never analysed" (unresolved) from "analysed, genuinely zero edges" (resolved but empty) — see.

func SelectReachabilityRoots added in v0.3.0

func SelectReachabilityRoots(candidates []RootCandidate, kind ArtifactKind) []string

SelectReachabilityRoots returns the reachability roots for an analysis over a call graph, conditioned on what the analysed module is.

For an application (kind ArtifactApplication) every module-owned (non-external) node is a root. An application's functions are entered in ways no static analysis can enumerate — framework dispatch, registered callbacks, goroutine entry functions — so rooting only the exported API would leave those subgraphs dark and under-report the capabilities the shipped code really exercises. Whole-graph rooting witnesses them with zero framework knowledge.

For a library it is every owned node that is either part of the public API or a package init function: a library only runs what its consumer can reach. Package init runs whenever the package is loaded, so init-reachable code is reachable in any real execution and must root the traversal too; omitting it makes init-only sinks a false-"safe" under-approximation. When no node qualifies it falls back to every owned node so the analysis still reasons about the analysed code.

Results are sorted for determinism.

func SymbolMethodName added in v0.3.2

func SymbolMethodName(symbolID string) string

SymbolMethodName extracts the short method or function name from a call-graph symbol ID: the trailing identifier after the final '.'. It returns "" for an empty ID or one ending in '.'. Examples: "pkg.(*T).M" -> "M", "pkg.Fn" -> "Fn".

Types

type AnalysisPhase added in v0.3.2

type AnalysisPhase string

AnalysisPhase is the operating phase whose trust model decides how missing fidelity is handled. The completeness signal is the same across phases; the response to it is not.

const (
	// PhaseInclusion is dependency vetting of untrusted code in a hermetic
	// environment that must never execute the target — so generators are never
	// run to recover fidelity; a below-full verdict is degraded and caveated.
	PhaseInclusion AnalysisPhase = "inclusion"
	// PhaseCoding is analysis of a local working tree the developer controls and
	// can rebuild — so a below-full verdict is an instruction to rebuild
	// (go generate / go build), not a silent degradation.
	PhaseCoding AnalysisPhase = "coding"
	// PhaseDiff is a before/after comparison, where missing fidelity is handled
	// by the parity guard (see CompletenessParity) rather than a per-module
	// caveat.
	PhaseDiff AnalysisPhase = "diff"
)

type AnalysisSource added in v0.6.0

type AnalysisSource string

AnalysisSource names what an analysis read to build a call graph.

It is a DIMENSION, not a ladder. A graph built from a published module zip and one built from a working tree are both correct and neither supersedes the other; they answer different questions about different bytes. Composition therefore never picks between values of it — a reader asks for the one it wants, and the record carries which one it is.

The field exists because the distinction was previously smuggled into the VERSION component of the coordinate: a working-tree ingest was stored at a synthetic "v0.0.0" that no published module ever resolves to. That is a version column stating something untrue, invisible to every query that filters on version, and unusable as an identity — two checkouts of the same module path collide on one key. Naming the source directly is what lets the two be told apart without reading a convention out of a version string.

const (
	// AnalysisSourceUnrecorded is the zero value: the record predates the field
	// and says nothing about what was analysed. It is a distinct value from all
	// the others and is never fidelity-comparable with them — a record that does
	// not say what it read cannot be shown to have read the same thing as one that
	// does.
	//
	// That holds for grouping and for reporting. It does NOT extend to comparing
	// what two records say about the graph: an absent value there is not a third
	// answer, it is the absence of a field, and hashing it as an answer made two
	// records with an identical graph contradict each other. analysedFrom resolves
	// it for that one purpose.
	AnalysisSourceUnrecorded AnalysisSource = ""
	// AnalysisSourceModuleZip is a graph built from a fetched module zip. The
	// bytes are pinned, the artefact identity names them, and re-analysing the
	// same coordinate reads the same bytes.
	AnalysisSourceModuleZip AnalysisSource = "zip"
	// AnalysisSourceWorktree is a graph built from a directory on disk. Nothing
	// was fetched, so there is no artefact identity — not missing, inapplicable —
	// and the tree mutates between runs, so two analyses of "the same" coordinate
	// may legitimately describe different code. WorktreeDigest is what tells them
	// apart.
	AnalysisSourceWorktree AnalysisSource = "worktree"
)

func AnalysisSources added in v0.6.0

func AnalysisSources() []AnalysisSource

AnalysisSources is every source this type defines. Consumers that must cover the dimension range over it rather than restating the list.

func RecordAnalysisSource added in v0.6.0

func RecordAnalysisSource(r CallGraphRecord) (AnalysisSource, string)

RecordAnalysisSource projects a record onto the source it was analysed from, together with what distinguishes that source from another of the same kind.

For a zip the discriminator is the artefact identity — which bytes were read. For a working tree it is the tree digest. A record naming no source has neither, and the empty discriminator is what stops it being grouped with any record that does name one.

It is a free function rather than a method so CallGraphRecord stays a read-shaped result type with no behaviour.

func (AnalysisSource) String added in v0.6.0

func (s AnalysisSource) String() string

String renders the source, showing the zero value as "not recorded" rather than as an empty field a reader would take for an absence of source.

type ArtifactKind added in v0.3.2

type ArtifactKind string

ArtifactKind describes what the analysed module is, which decides how reachability roots are chosen. An application's code all runs under entry points the analysis cannot enumerate (framework dispatch, registered callbacks, goroutine entries), so every owned function is a root; a library is only exercised through what a consumer can call.

const (
	// ArtifactLibrary is a module with no command: it is reached only through
	// its exported API and package init. It is the zero value, so a record
	// persisted before this field existed keeps the pre-existing behaviour.
	ArtifactLibrary ArtifactKind = ""
	// ArtifactApplication is a module that builds a command — it contains a
	// package main defining func main.
	ArtifactApplication ArtifactKind = "Application"
)

type CallEdge

type CallEdge struct {
	FromID     string
	ToID       string
	CallSite   SourcePosition
	Confidence EdgeConfidence
	// ReflectDispatch is true when the edge was resolved through a reflect
	// call. Such edges carry ConfidenceUnknown — reflection is not a distinct
	// confidence rank — but the reflect provenance is recorded here so the
	// verdict-soundness layer can attribute the UNRESOLVED signal to reflection
	// specifically rather than a generic unresolved dispatch.
	ReflectDispatch bool
}

CallEdge is a directed call relationship between two nodes.

type CallGraphAlgorithm

type CallGraphAlgorithm string

CallGraphAlgorithm names the static analysis algorithm used to produce the call graph.

const (
	// AlgorithmCHA uses Class Hierarchy Analysis: conservative, fast,
	// over-approximates virtual dispatch.
	AlgorithmCHA CallGraphAlgorithm = "CHA"
	// AlgorithmRTA uses Rapid Type Analysis: more precise than CHA, slower.
	AlgorithmRTA CallGraphAlgorithm = "RTA"
	// AlgorithmStatic records only direct (non-virtual) calls.
	AlgorithmStatic CallGraphAlgorithm = "Static"
)

type CallGraphConflict added in v0.6.0

type CallGraphConflict struct {
	// Coordinate is the module and version the disagreeing records describe.
	Coordinate coordinate.ModuleCoordinate

	// PipelineVersion is the pipeline version every conflicting record was
	// written under.
	PipelineVersion string

	// Field names what the records disagree on: one of ConflictFields.
	Field string

	// Values are the distinct values recorded for Field, sorted, so the report is
	// stable across runs.
	Values []string

	// ContentHashes name the records carrying each of Values, in the same order.
	ContentHashes []string
}

CallGraphConflict is two call graph records that composition must not resolve by picking.

It reports three disagreements, and keeping them distinct is the point.

  • "analysis_source": the ledger holds records built from different KINDS of source — a fetched module zip and a working tree — for one coordinate, and the reader did not say which it wanted. Neither supersedes the other; they are answers about different bytes.
  • "artefact_identity": two identities for one pinned module version, which means the same module at the same version yielded two different sets of bytes. There is no ladder between answers about different bytes.
  • "call_graph": two records describing the SAME artefact at the SAME completeness that disagree about the graph. That narrow case is evidence of non-determinism in the analyser and is worth surfacing rather than absorbing.

The third is deliberately narrow, and narrower than it first looks. A call graph is NOT a function of the module's own bytes plus the analyser version: devirtualisation is gated on how much of the dependency closure was built, so a run with more of the closure available resolves interface dispatch an earlier run could not, and the graph grows edges without any input changing. Runs that differ that way differ in COMPLETENESS, the ladder orders them, and the more complete graph is strictly better evidence. Only when the ladder cannot separate two records is a disagreement between them a finding rather than a refinement.

It mirrors fetch's Divergence, licence's LicenceConflict and iface's InterfaceConflict, including reporting the content hashes of the records carrying each value.

Every conflict names a remedy. A refusal the store rules make permanent and that says nothing about what to run is a dead end: the reader is left to guess at --force, and guessing at --force is exactly what a caller should not do.

func (CallGraphConflict) Error added in v0.6.0

func (c CallGraphConflict) Error() string

Error renders the conflict as a message. CallGraphConflict satisfies error so the store can return it directly.

The remedy is part of the message rather than something a caller may print alongside it: this error crosses the store and the ports layer before anything renders it, and a remedy the renderer had to remember to add is one that goes missing on the paths nobody thought of.

func (CallGraphConflict) Remedy added in v0.7.0

func (c CallGraphConflict) Remedy() Remedy

Remedy names what to run about this conflict.

Each field gets its own, because the routes out are genuinely different: an unreadable source is a build that is too old, two artefact identities are two sets of bytes, and two graphs at one completeness are a measurement to take again. None of them is --force on its own, which is why none of them says only that.

type CallGraphRecord

type CallGraphRecord struct {
	SchemaVersion string
	// Ecosystem declares the schema's scope; always fetchdomain.EcosystemGo.
	Ecosystem  string
	Coordinate coordinate.ModuleCoordinate
	Algorithm  CallGraphAlgorithm
	// Completeness is the per-module fidelity level at which this graph was
	// analysed (BUILT_WITH_BODIES down to FAILED/VERSION_NOT_IN_TOOLCHAIN),
	// derived from the build outcome at extraction time. It is the machine
	// signal a diff keys completeness-parity off, and the per-module phase
	// caveat keys off, so neither has to infer fidelity from node/edge totals.
	Completeness CompletenessLevel
	// ArtifactKind is what the analysed module is — an application that builds a
	// command, or a library. Reachability roots are conditioned on it: an
	// application roots every owned node, because code the runtime dispatches to
	// dynamically is still the application's own code and its capabilities are
	// really exercised. Empty means library, so pre-v10 records keep their
	// original rooting.
	ArtifactKind ArtifactKind
	Nodes        []CallNode
	Edges        []CallEdge
	// Interfaces are the interface types the analysed module declares, and
	// Implementations the concrete types of that module satisfying them. They
	// are the type-level half of "what must change together", which the edge
	// collections cannot express: an interface method has no callers, only
	// implementations.
	Interfaces      []InterfaceType
	Implementations []InterfaceImplementation
	// TestScope records whether _test.go declarations were part of this
	// analysis. The zero value means the record makes no claim, which consumers
	// must treat as unmeasured rather than as an absence of test code.
	TestScope TestScope
	// TestScopeDetail explains a TestScopeExcluded value. Empty otherwise.
	TestScopeDetail string
	OverallStatus   CallGraphStatus
	FailureDetail   string
	// FailureCause says what a failing OverallStatus is a statement about: the
	// module, or the run that tried to analyse it. FailureDetail is the prose a
	// human reads; this is the machine axis, classified where the failure was
	// still a value rather than recovered from that prose later.
	//
	// Empty on a record that did not fail, and on a failure record written before
	// the axis existed. Both read as "no cause stated", never as "the module was
	// at fault" — see FailureCause and RecordIsCacheable.
	FailureCause FailureCause
	// FailedPackages is the sorted, deduplicated set of import paths within the
	// analysed module that failed to typecheck (or failed SSA construction).
	// It is populated when OverallStatus is Partial and drives sound verdict
	// caveating: a reachability / callers / callees query whose root or reached
	// nodes fall in one of these packages is under-resolved (edges were dropped)
	// and must be reported as unresolved rather than a confident "none".
	// FailureDetail is the human-readable companion; this is the machine set.
	FailedPackages []string
	// ExclusionReason is non-empty when the module was skipped rather than
	// analysed; currently always ExclusionReasonConfig.
	ExclusionReason string
	// ExclusionList is the callgraph.exclude list that was active when this
	// record was computed, sorted for determinism. Recorded for every record
	// so callgraph-show can report the policy in force at extraction time.
	ExclusionList   []string
	NodeCount       int
	EdgeCount       int
	ExtractedAt     time.Time
	PipelineVersion string
	ContentHash     string
	// ArtefactIdentity names the fetched artefact this record was derived from,
	// in the "zip:h1:..." / "gomod:h1:..." form fetchdomain.ArtefactIdentity
	// renders. It answers the question the coordinate cannot: which bytes
	// produced this finding. A coordinate names a module version, and the fetch
	// record for that coordinate may since have been re-measured, so a link by
	// coordinate is a link by convention; this one is by fact, and is covered by
	// ContentHash, so the claim is as tamper-evident as the finding itself.
	//
	// Empty on records written before the field existed, and on records derived
	// from no fetched artefact at all. Both read as "not recorded", never as
	// "derived from nothing". Read it back through RecordArtefactIdentity,
	// which draws that distinction; never hand this field to
	// ParseArtefactIdentity directly.
	ArtefactIdentity string
	// SourceContentHash is the content hash of the fetch record that supplied
	// those bytes. ArtefactIdentity says which artefact; this says which
	// measurement of it, so a reader can fetch that record and check the claim
	// against it. Empty exactly when ArtefactIdentity is.
	SourceContentHash string
	// AnalysisSource names what the analysis read: a fetched module zip, or a
	// working tree on disk. It is a dimension rather than a ladder position — see
	// AnalysisSource — so it belongs to the record's identity and composition
	// never chooses between its values.
	//
	// Empty on records written before the field existed, which reads as "not
	// recorded" and never as "analysed from nothing".
	AnalysisSource AnalysisSource
	// WorktreeDigest identifies WHICH working tree a worktree analysis read, as a
	// digest over the Go source the analysis could see. Empty for every other
	// source.
	//
	// It is here because a worktree record has no artefact identity — nothing was
	// fetched, so there is nothing to name — and without a discriminator two
	// checkouts of one module path are one row. The absolute path the analysis ran
	// in is deliberately not used: where a tree happened to be mounted is
	// provenance, and two different trees at one path (a branch switch, a
	// rebuild) would share it while two copies of one tree would not.
	WorktreeDigest string
	// SynthesisedGoMod is non-zero when the analysed tree is not the published
	// tree: the module zip shipped no go.mod and kanonarion wrote one before
	// loading. It states which module path and which go directive that file
	// declared, so the graph's semantics are readable rather than assumed.
	//
	// The zero value means the published bytes were analysed as published. See
	// SynthesisedGoMod for why that is a statement and not merely an absence.
	SynthesisedGoMod SynthesisedGoMod
}

CallGraphRecord is the aggregate root for a module's call graph extraction result. It is immutable once ContentHash is set.

func Compose added in v0.6.0

func Compose(records []CallGraphRecord, req ComposeRequest) (CallGraphRecord, error)

Compose returns the call graph record a reader gets for one coordinate and pipeline version, given every record the ledger holds for it.

Records must be supplied in the order they were appended, and each must already have verified its own content hash: a record that cannot be checked stops the read before it reaches here, so composition never has to decide what to do about an unverifiable row.

The ladder is COMPLETENESS, then recency. A BUILT_WITH_BODIES graph outranks a METADATA_ONLY one regardless of which was written later, exactly as the fetch ledger serves the strongest anchor rather than the newest measurement: the weaker record analysed less of the same module, so it is a lesser measurement rather than a competing answer. Recency is only ever the last tiebreaker.

The analysis source is NOT on that ladder. It is a dimension: a zip graph and a worktree graph answer different questions, and serving one for the other would answer a question the caller did not ask. Composition therefore selects a source first and only then ladders within it.

func NewExcludedRecord

func NewExcludedRecord(coord coordinate.ModuleCoordinate, algorithm CallGraphAlgorithm, exclusionList []string) CallGraphRecord

NewExcludedRecord builds the CallGraphRecord persisted for a module that was skipped because it is listed in callgraph.exclude. It has no nodes or edges; the caller still sets ExtractedAt, PipelineVersion, and the content hash. exclusionList must already be normalised (see NormaliseExclusions).

func (*CallGraphRecord) Sort

func (r *CallGraphRecord) Sort()

Sort puts all collections into a canonical, deterministic order. Must be called before hashing.

type CallGraphRecordHasher

type CallGraphRecordHasher struct{}

CallGraphRecordHasher computes and embeds a content hash into a CallGraphRecord. The hash covers the canonical JSON serialisation with ContentHash zeroed.

func (CallGraphRecordHasher) Marshal

Marshal returns the canonical JSON bytes for a CallGraphRecord, including its ContentHash field. Call SetContentHash before this.

func (CallGraphRecordHasher) SetContentHash

SetContentHash computes the canonical hash of r (with ContentHash zeroed), sets r.ContentHash, and returns the updated record.

func (CallGraphRecordHasher) Unmarshal

func (CallGraphRecordHasher) Unmarshal(data []byte) (CallGraphRecord, error)

Unmarshal parses a CallGraphRecord from its canonical JSON representation.

func (CallGraphRecordHasher) VerifyContentHash

func (CallGraphRecordHasher) VerifyContentHash(r CallGraphRecord) error

VerifyContentHash re-computes the canonical hash and checks it matches r.ContentHash. Returns nil if valid.

type CallGraphStatus

type CallGraphStatus int

CallGraphStatus describes the outcome of call graph extraction.

const (
	// CallGraphStatusUnknown is the zero value and should never appear in a
	// persisted record.
	CallGraphStatusUnknown CallGraphStatus = iota
	// CallGraphStatusExtracted means the call graph was fully constructed.
	CallGraphStatusExtracted
	// CallGraphStatusPartial means the graph was constructed but some packages
	// had load errors; the result covers only the packages that loaded cleanly.
	CallGraphStatusPartial
	// CallGraphStatusLoadFailed means package loading failed fatally; no graph
	// was produced.
	CallGraphStatusLoadFailed
	// CallGraphStatusOutOfMemory means the extraction hit the configured memory
	// budget and was terminated cleanly.
	CallGraphStatusOutOfMemory
	// CallGraphStatusCancelled means extraction was interrupted by context
	// cancellation.
	CallGraphStatusCancelled
	// CallGraphStatusExtractionFailed covers all other fatal errors.
	CallGraphStatusExtractionFailed
	// CallGraphStatusExcludedByConfig means the module was skipped before
	// traversal because its path is listed in callgraph.exclude. No graph was
	// produced; ExclusionReason and ExclusionList carry the provenance.
	CallGraphStatusExcludedByConfig
)

func (CallGraphStatus) String

func (s CallGraphStatus) String() string

String returns the human-readable name of the status.

type CallNode

type CallNode struct {
	// ID is a stable, unique identifier in the form "pkg/path.FuncName" for
	// free functions or "pkg/path.(*RecvType).MethodName" for methods.
	ID            string
	Module        string // module path owning this node; empty for unknown
	Package       string // import path of the package
	Symbol        string // short function/method name
	Receiver      string // receiver type name (empty for free functions)
	IsExternal    bool   // true if this node is outside the analysed module
	IsExportedAPI bool   // true if this node is part of the module's public API
	Position      SourcePosition
	// UsesUnsafePointer is true when the function's own body performs an
	// unsafe.Pointer conversion. This is a body-level capability fact that a
	// callee-identity sink map cannot witness (the unsafe package exposes no
	// callable functions), so it is captured at extraction time and treated as
	// an UNSAFE_POINTER sink during capability analysis.
	UsesUnsafePointer bool
	// IsAssemblyOrLinkname is true when the function has no Go body — it is
	// implemented in assembly or provided via //go:linkname. Such functions are
	// call-graph leaves with no edges into them, so this fact is captured at
	// extraction time and treated as an ARBITRARY_EXECUTION sink during
	// capability analysis.
	IsAssemblyOrLinkname bool
	// UsesPlugin is true when the function's own body references the Go plugin
	// package (plugin.Open / (*Plugin).Lookup). A plugin boundary loads code the
	// static call graph never sees, so an empty callers/callees answer over such
	// a node cannot be trusted as a negative — this fact is captured at extraction
	// time and treated as a leaf soundness sink that downgrades a negative verdict
	// to UNRESOLVED. Capability analysis already witnesses plugin use via the
	// package-import sink map, so this fact is verdict-layer only.
	UsesPlugin bool
	// IsTest is true when the function is declared in a _test.go file or in an
	// external test package. It is the node role that lets a query separate the
	// production blast radius of a change from its test surface without hiding
	// either: both are in the graph, and the caller chooses.
	IsTest bool
}

CallNode is a function or method node in the call graph.

type CompletenessDescriptor added in v0.3.2

type CompletenessDescriptor struct {
	Level     CompletenessLevel
	Algorithm CallGraphAlgorithm
}

CompletenessDescriptor is the per-side fidelity signature a diff compares for parity. The completeness level names how much of the module was built, and the algorithm captures the algorithm/devirt tier that produced the graph, so equality across both fields is "same completeness level, same algorithm/devirt tier".

func RecordCompleteness added in v0.3.2

func RecordCompleteness(r CallGraphRecord) CompletenessDescriptor

RecordCompleteness projects a record onto its completeness descriptor. It is a free function rather than a method so CallGraphRecord stays a read-shaped result type with no behaviour.

type CompletenessLevel added in v0.3.2

type CompletenessLevel string

CompletenessLevel records the fidelity at which a module's call graph was analysed. The same module can be analysed at very different fidelity from run to run — fully built into SSA, registered from type information with no bodies, reduced to package metadata, or failed to load — and that fidelity, not a code change, can make an edge appear or disappear between two runs. Recording the level per module lets a diff assert that a before/after comparison is fidelity-symmetric before it trusts a "resolved"/"unaffected" verdict.

The levels are ordered most to least complete. A verdict is only ever as sound as the least-complete side of the comparison that produced it.

Every level here has a production producer, and that is a standing property of this type rather than an observation about it. A level nothing writes tells a reader — of the type, of a record, or of the docs — that the analyser can report a fidelity it has never once reported, and the ladders built on this one (composition of call-graph generations, the soundness rung for a negative reachability answer) then order on a rung nobody can occupy. TestEveryCompletenessLevelHasAProducer pins it.

const (
	// CompletenessUnknown is the zero value: no completeness was recorded (a
	// legacy record, or a path that consulted no call graph). It participates in
	// parity like any other level — Unknown vs a concrete level is a mismatch.
	CompletenessUnknown CompletenessLevel = ""
	// CompletenessBuiltWithBodies means the module's target packages were built
	// into SSA with function bodies, so interface dispatch and intra-body call
	// edges were resolvable. This is the only level a confident negative verdict
	// may rest on.
	CompletenessBuiltWithBodies CompletenessLevel = "BUILT_WITH_BODIES"
	// CompletenessTypeOnly means the module's packages type-checked and were
	// registered with the SSA program, but not one of them was built into SSA
	// with function bodies. Types are known, so the interface/implementation
	// relation and every declaration are visible; method bodies were never
	// analysed, so call edges out of them are absent.
	//
	// It is written when SSA construction fails for every target package that
	// type-checked — see the staticcha analyser. That is the state the level
	// names, and it is strictly more than METADATA_ONLY: the distinction between
	// "we had types but no bodies" and "we had neither" is known at the point the
	// build result is read and must not be discarded there.
	CompletenessTypeOnly CompletenessLevel = "TYPE_ONLY"
	// CompletenessMetadataOnly means only package metadata (names, imports) was
	// loaded — no types, no bodies. Nothing about dispatch can be concluded.
	CompletenessMetadataOnly CompletenessLevel = "METADATA_ONLY"
	// CompletenessFailed means loading or SSA construction failed and no usable
	// graph was produced for the module.
	CompletenessFailed CompletenessLevel = "FAILED"
)

func CompletenessLevels added in v0.6.0

func CompletenessLevels() []CompletenessLevel

CompletenessLevels is every level this type defines, most to least complete, with the zero value last. It is the ladder other domains mirror and the list the producer guard walks; adding a level without adding it here is caught by TestCompletenessLevels_IsExhaustive.

VERSION_NOT_IN_TOOLCHAIN was removed from this ladder rather than given a producer. The condition it named — the module resolved to a version outside the analysed project's toolchain — is a property of an isolated SCAN, not of a call graph's fidelity, and it is already reported where it belongs and with a real producer, as vuln domain's UnscanReasonVersionNotInToolchain. Restating it as a call-graph level asserted a second, unmaintained account of one fact.

func (CompletenessLevel) IsBuiltWithBodies added in v0.3.2

func (l CompletenessLevel) IsBuiltWithBodies() bool

IsBuiltWithBodies reports whether the module was analysed at full fidelity — the only level on which a confident "not reachable" / "resolved" verdict is sound.

func (CompletenessLevel) String added in v0.3.2

func (l CompletenessLevel) String() string

String returns the human-readable name of the level, rendering the zero value as "Unknown".

type ComposeRequest added in v0.6.0

type ComposeRequest struct {
	// Source restricts composition to records built from one kind of source. The
	// zero value names none, and DefaultSource then decides.
	Source AnalysisSource
}

ComposeRequest asks for the record a reader gets for one coordinate and pipeline version. Its zero value is the unrestricted read.

type EdgeConfidence

type EdgeConfidence string

EdgeConfidence describes how a call edge was resolved, so consumers can weight edges by resolution tier and the verdict layer can key soundness decisions off the tag. The vocabulary is ordered from most to least precise: a Direct edge names a unique concrete callee; CHA-overapprox and VTA are progressively refined interface-dispatch resolutions; Framework is bound by a framework model; Unknown is an unresolved edge that must flag verdicts as UNRESOLVED.

const (
	// ConfidenceDirect is a statically-known call to a unique concrete callee,
	// including an interface site devirtualised to its sole implementer.
	ConfidenceDirect EdgeConfidence = "Direct"
	// ConfidenceCHAOverapprox is an unrefined Class Hierarchy Analysis
	// over-approximation of an interface dispatch: every type-compatible method
	// is a possible callee.
	ConfidenceCHAOverapprox EdgeConfidence = "CHA-overapprox"
	// ConfidenceVTA is an interface dispatch resolved by the Variable Type
	// Analysis tier, narrowing the CHA over-approximation to the types that
	// actually flow to the call site.
	ConfidenceVTA EdgeConfidence = "VTA"
	// ConfidenceFramework is an edge bound by a framework model or thunk rather
	// than observed in the analysed source.
	ConfidenceFramework EdgeConfidence = "Framework"
	// ConfidenceUnknown is an edge the analyser cannot resolve, including
	// reflect-dispatched calls (see CallEdge.ReflectDispatch). It is a soundness
	// sink: verdicts reaching such an edge must be reported as UNRESOLVED.
	ConfidenceUnknown EdgeConfidence = "Unknown"
)

func MigrateConfidence added in v0.3.2

func MigrateConfidence(stored string) (EdgeConfidence, bool)

MigrateConfidence maps a legacy stored confidence string onto the current vocabulary, deterministically. The pre-v7 values DynamicDispatch and Reflection are folded: DynamicDispatch becomes CHA-overapprox, and Reflection becomes Unknown with the reflect-origin flag set so the reflect provenance is preserved as an edge attribute. All current values pass through unchanged. The boolean result reports whether the edge originated from a reflect call.

type FailureCause added in v0.7.0

type FailureCause string

FailureCause names what a failed call-graph extraction is a statement about: the module, or the run that tried to analyse it.

The distinction is the difference between a finding and a measurement that never happened. A module whose source does not type-check is a real, stable property of that module — re-analysing it tomorrow rediscovers it at full analysis cost. A run that could not find a usable Go toolchain has measured nothing at all, and the same module analysed a minute later on a repaired box may well produce a complete graph.

The fetch ledger draws the same line for the same reason, and states it as "a failure is a statement about the lookup, not about the module, and a later attempt may well succeed. Collapsing the two lets one network flake be recorded — and cached — as a property of a dependency." Here the flake is a toolchain rather than a network, and the record is a call graph rather than a checksum, but the failure mode is identical: without the axis, one bad run is filed as a fact about a dependency and served on every subsequent run.

The cause is classified where the failure is still a value — at the analyser boundary, from whether the environment can run the toolchain at all — and never by re-reading the prose of a stored FailureDetail, on the same terms x/mod/sumdb's errors are classified at ClientOps rather than recovered from a flattened string.

It is not a ladder and composition never picks between its values: it qualifies a failure, and a record carrying a graph outranks any failure regardless of what caused it. What it decides is cache eligibility — see RecordIsCacheable.

const (
	// FailureCauseUnrecorded is the zero value. It is carried by every record
	// that did not fail at all, and by failure records written before the axis
	// existed. It states no cause, and must never be read as one: a failure that
	// does not say what caused it is not evidence that the module is at fault.
	FailureCauseUnrecorded FailureCause = ""

	// FailureCauseModule means the module could not be analysed: its published
	// bytes carry no Go packages, its source does not type-check, its module
	// graph cannot be resolved from what it ships. The finding is about the
	// module and is stable across runs, so it is served from cache exactly as a
	// successful analysis is.
	FailureCauseModule FailureCause = "module"

	// FailureCauseEnvironment means the analysis environment could not run: no
	// usable go on PATH, a toolchain that is absent or unresolvable, a cancelled
	// context, the memory cap. Nothing was learned about the module, so the
	// record is kept as evidence that this run could not run — the ledger never
	// goes silent — but it is not eligible as a cache hit.
	FailureCauseEnvironment FailureCause = "environment"
)

func (FailureCause) String added in v0.7.0

func (c FailureCause) String() string

String renders the cause, showing the zero value as "not recorded" rather than as an empty field a reader would take for an absence of cause.

type ImplementedMethod added in v0.6.0

type ImplementedMethod struct {
	Method string
	NodeID string
}

ImplementedMethod binds one interface method name to the concrete call-graph node that satisfies it, so the per-method form of an implementers query answers with node IDs the edge queries also accept.

type InterfaceImplementation added in v0.6.0

type InterfaceImplementation struct {
	// InterfaceID is the InterfaceType.ID this implementation satisfies.
	InterfaceID string
	// TypeID is the concrete type in receiver form: "pkg/path.(*Store)" for a
	// pointer-receiver implementation, "pkg/path.(Value)" for a value one. It is
	// the node-ID prefix every method of the implementation shares.
	TypeID   string
	Package  string
	Position SourcePosition
	// IsTest is true when the concrete type is declared in a _test.go file —
	// the test fakes that a port-signature change must be updated alongside.
	IsTest bool
	// Methods maps each interface method to the concrete node implementing it,
	// sorted by method name.
	Methods []ImplementedMethod
}

InterfaceImplementation records that a concrete named type in the analysed module satisfies an interface the module declares.

The relation is computed over the analysed module's own declarations on both sides. A type in another module that satisfies the same interface is not recorded here — that module's analysis does not own the interface, and computing satisfaction against every interface in the dependency graph is a different, far larger measurement. Query output states this scope rather than letting the omission read as an empty set.

func ImplementersOf added in v0.6.0

func ImplementersOf(rec CallGraphRecord, interfaceID string) (impls []InterfaceImplementation, declared bool)

ImplementersOf returns the implementations of interfaceID recorded in rec, and whether rec's module declares the interface at all. The two results are distinct answers: an interface this module does not declare is outside the measurement, whereas a declared interface with no implementations is a measured empty set.

It is a function rather than a method because CallGraphRecord is a result type: it carries facts, and query behaviour over those facts lives beside it, not on it.

type InterfaceType added in v0.6.0

type InterfaceType struct {
	// ID is "pkg/path.Name", matching the node-ID convention for a free
	// declaration. The per-method form is "pkg/path.(Name).Method", matching the
	// receiver-parenthesised convention for methods.
	ID      string
	Package string
	Name    string
	// Methods is the interface's full method set including embedded interfaces,
	// sorted. Names only: the signature lives in the declaration this ID points at.
	Methods  []string
	Position SourcePosition
	// IsTest is true when the interface is declared in a _test.go file.
	IsTest bool
}

InterfaceType is an interface declared in the analysed module, made addressable so a port-signature change can ask the type question — which concrete method sets must change together — rather than the edge question. An interface method is not a call-graph node (nothing calls it; calls go to implementations), so it needs its own identity.

func InterfaceByID added in v0.6.0

func InterfaceByID(rec CallGraphRecord, id string) (InterfaceType, bool)

InterfaceByID returns the interface rec's module declares under the given ID.

func (InterfaceType) MethodID added in v0.6.0

func (i InterfaceType) MethodID(method string) string

MethodID renders the addressable ID of one of the interface's methods.

type NegativeVerdictInputs added in v0.3.2

type NegativeVerdictInputs struct {
	// MethodName is the short name of the queried symbol's method/function, used
	// to match interface invoke sites that dispatch on the same name.
	MethodName string
	// QueriedNode is the node the query is rooted at; the zero value when the
	// symbol is not a node in any analysed graph (Found is false).
	QueriedNode CallNode
	// Found reports whether QueriedNode was located.
	Found bool
	// ModuleLevel is the completeness level of the module owning the queried
	// symbol. Below BUILT_WITH_BODIES (and not Unknown) it is itself a sink.
	ModuleLevel CompletenessLevel
	// Edges are the edges of the owning module's graph, scanned for interface
	// invoke sites when ScanDispatch is set.
	Edges []CallEdge
	// NodesByID indexes the owning module's nodes by ID, so an invoke site's
	// callee method name can be read.
	NodesByID map[string]CallNode
	// ScanDispatch enables the interface-dispatch scan. It applies to a callers
	// query, where a missing invoke->target edge hides a caller; a callees query
	// cannot observe an empty bound-implementer set from stored edges, so it
	// leaves this false.
	ScanDispatch bool
	// TestScope is the owning module's test-scope axis. Anything other than
	// Analysed means the answer covers production code only.
	TestScope TestScope
	// TestScopeDetail explains an excluded test scope, when the record recorded
	// a reason.
	TestScopeDetail string
	// TestsExcludedByRequest is set when the caller asked for the test surface
	// to be dropped. It is not a soundness sink — the scope was chosen, not
	// missed — but it still narrows what an empty answer means, so it is named
	// on the verdict line rather than left implicit.
	TestsExcludedByRequest bool
}

NegativeVerdictInputs carries the soundness signals gathered about an empty callers/callees answer so the queried node's absence can be classified.

type Remedy added in v0.7.0

type Remedy struct {
	// Lead introduces the invocations. It ends without punctuation of its own;
	// String supplies the colon.
	Lead string
	// Lines are whole invocations, "kanonarion" included, each parseable on its
	// own. Prose never appears here.
	Lines []string
}

Remedy is what an operator should run when composition refuses: a lead sentence, then whole invocations.

The invocations are held apart from the prose because they are a contract rather than decoration — a refusal that prints a command the tool then rejects costs the reader the round trip the refusal existed to save. Keeping them as data is what lets a test push every one through the CLI's argument parser.

func (Remedy) String added in v0.7.0

func (r Remedy) String() string

String renders the remedy as the tail of a refusal: the lead, then one indented invocation per line.

type RootCandidate added in v0.3.0

type RootCandidate struct {
	ID            string
	Symbol        string
	IsExternal    bool
	IsExportedAPI bool
}

RootCandidate is the minimal node view SelectReachabilityRoots needs to classify a node as a reachability root. It is deliberately decoupled from CallNode (and from any adapter's projection type) so every reachability analysis can feed the shared selector its own node representation and the root-selection rule can never drift between them.

type SinkKind added in v0.3.2

type SinkKind string

SinkKind names a specific dispatch/edge-level reason a negative call-graph verdict cannot be trusted.

const (
	// SinkInterfaceDispatch is an over-approximated or unresolved interface invoke
	// site that dispatches on the queried method's name. CHA may have failed to
	// bind it to the queried method (the implementer's method was absent from the
	// built SSA set), so the absence of an edge is not proof of absence.
	SinkInterfaceDispatch SinkKind = "interface-dispatch"
	// SinkTypeOnlyCallee is an edge into, or a queried node within, a module
	// analysed below BUILT_WITH_BODIES: method bodies were never built, so call
	// edges out of them are simply absent.
	SinkTypeOnlyCallee SinkKind = "type-only-callee"
	// SinkUnresolvedEdge is an edge the analyser could not resolve to a concrete
	// callee (ConfidenceUnknown), excluding reflect edges, which are reported as
	// SinkReflectDispatch.
	SinkUnresolvedEdge SinkKind = "unresolved-edge"
	// SinkReflectDispatch is an edge resolved through reflection: the callee set is
	// not statically knowable.
	SinkReflectDispatch SinkKind = "reflect-dispatch"
	// SinkUnsafePointerLeaf is a node whose body performs an unsafe.Pointer
	// conversion — a documented soundness sink that can defeat static edge
	// resolution on paths through it.
	SinkUnsafePointerLeaf SinkKind = "unsafe-pointer-leaf"
	// SinkAssemblyOrLinknameLeaf is a node implemented in assembly or via
	// //go:linkname: it has no Go body, so no edges out of it are visible.
	SinkAssemblyOrLinknameLeaf SinkKind = "assembly-or-linkname-leaf"
	// SinkPluginLeaf is a node whose body loads code through the Go plugin
	// package (plugin.Open / (*Plugin).Lookup): the loaded targets are resolved
	// at runtime and are never present in the static graph.
	SinkPluginLeaf SinkKind = "plugin-leaf"
	// SinkTestScopeUnmeasured is a module whose _test.go declarations were not
	// part of the analysis. Test fakes and table-driven callers are a systematic
	// part of what calls a port, so an empty answer over such a module measures
	// production code only — and saying so is worth more than a clean-looking
	// absent that quietly means something narrower than it reads.
	SinkTestScopeUnmeasured SinkKind = "test-scope-unmeasured"
)

type SoundnessSink added in v0.3.2

type SoundnessSink struct {
	// Kind is the category of sink.
	Kind SinkKind
	// Site is the symbol ID the sink was found at — the invoke site, the callee
	// node, or the leaf node.
	Site string
	// Detail is an optional human-readable specific (e.g. the module's
	// completeness level, or the method name an interface site dispatches on).
	Detail string
}

SoundnessSink is a single reason an empty verdict was downgraded to UNRESOLVED, with enough provenance for a reviewer to act on it.

func (SoundnessSink) Describe added in v0.3.2

func (s SoundnessSink) Describe() string

Describe renders the sink as a single deterministic line naming the kind, the site, and any detail.

type SourcePosition

type SourcePosition struct {
	File string // path relative to module root or absolute within the analysis
	Line int
}

SourcePosition identifies a location in a source file.

type SynthesisedGoMod added in v0.7.0

type SynthesisedGoMod struct {
	// ModulePath is the module path written into the synthesised file. It is the
	// coordinate's path VERBATIM, with no major-version suffix added.
	//
	// That rule comes from a measured hazard. A +incompatible version — v2 or
	// above published without a /vN path — names a module whose path carries no
	// suffix at all. Synthesising "github.com/Masterminds/sprig/v2" for
	// sprig@v2.22.0+incompatible would load and build cleanly, and every node ID
	// in the resulting graph would name a module that does not exist. Such a graph
	// joins nothing else in the ledger and says so nowhere: worse than the empty
	// graph it replaces.
	ModulePath string
	// GoDirective is the language version written into the file, pinned and
	// recorded rather than defaulted.
	//
	// It is on the record because it decides the graph. A go directive of 1.22 or
	// later changes loop-variable scoping, which changes the SSA, which changes the
	// call graph — so a graph built under an unstated directive is not reproducible
	// across tool versions. Reading it back is how a consumer knows which language
	// semantics the graph describes.
	GoDirective string
	// VendorTreePresent reports that the extracted module also ships a vendor
	// directory.
	//
	// A go.mod beside a vendor tree makes the toolchain auto-select -mod=vendor,
	// at which point the analysis silently describes vendored copies of
	// dependencies rather than the module graph — and, with a synthesised file
	// that requires nothing, fails consistency checking against vendor/modules.txt
	// instead. The load explicitly disables vendor mode when this is set, and the
	// field is what makes that decision readable off the record rather than
	// inferable from the analyser's source.
	VendorTreePresent bool
}

SynthesisedGoMod records that the tree an analysis read is NOT the tree that was published: the module zip shipped no go.mod, so kanonarion wrote one into the extraction directory before loading.

It exists because the alternative is a record that quietly claims more than it measured. A module published before Go modules carries no go.mod in its zip, so the loader runs outside any module: no package carries the module's import path, nothing is recognised as the target, and the module is recorded with an empty graph. Writing a go.mod fixes that — and makes the analysis one of the published bytes PLUS a file kanonarion invented. A record sealed against an artefact identity while describing a tree that artefact does not contain is making a claim about bytes it did not read, which is precisely the kind of silent divergence the ledger exists to prevent.

The zero value means no file was synthesised. That is unambiguous rather than merely absent: no record written before this field existed could have been synthesised, because nothing synthesised anything. So there is no "unrecorded" third state to ladder against here, and the field is omitted from the sealed shape when zero — which keeps every stored record's content hash verifiable.

func (SynthesisedGoMod) IsZero added in v0.7.0

func (s SynthesisedGoMod) IsZero() bool

IsZero reports whether no go.mod was synthesised for this analysis.

func (SynthesisedGoMod) String added in v0.7.0

func (s SynthesisedGoMod) String() string

String renders the synthesis for a human reading a record's provenance, returning the empty string when nothing was synthesised so a caller can append it unconditionally.

type TestScope added in v0.6.0

type TestScope string

TestScope records whether a module's _test.go declarations were part of the analysis. It exists because the alternative — saying nothing — makes an empty callers answer indistinguishable from an unmeasured one, and test fakes are a large, systematic part of the edit surface of any interface change.

const (
	// TestScopeUnknown is the zero value: the record makes no claim either way,
	// which every consumer must read as "not measured", never as "no test code".
	TestScopeUnknown TestScope = ""
	// TestScopeAnalysed means test files were loaded and their declarations are
	// present as nodes tagged IsTest. An empty test-scope answer over such a
	// record is a measurement.
	TestScopeAnalysed TestScope = "Analysed"
	// TestScopeExcluded means test files were deliberately not analysed for this
	// module — the load could not be performed with tests enabled.
	// TestScopeDetail carries why.
	TestScopeExcluded TestScope = "Excluded"
)

func (TestScope) IsMeasured added in v0.6.0

func (t TestScope) IsMeasured() bool

IsMeasured reports whether the record's test axis was actually analysed. Only TestScopeAnalysed qualifies; both the zero value and an explicit exclusion mean an answer scoped to production code alone.

type Verdict added in v0.3.2

type Verdict struct {
	Outcome VerdictOutcome
	Sinks   []SoundnessSink
}

Verdict bundles the outcome with the sinks that justify it. For a RESOLVED-PRESENT or RESOLVED-ABSENT verdict Sinks is empty.

func ClassifyNegativeVerdict added in v0.3.2

func ClassifyNegativeVerdict(in NegativeVerdictInputs) Verdict

ClassifyNegativeVerdict classifies an empty callers/callees answer into RESOLVED-ABSENT or UNRESOLVED, collecting the sinks that force a downgrade. The sinks are deterministically ordered and de-duplicated.

func (Verdict) Reason added in v0.3.2

func (v Verdict) Reason() string

Reason renders a deterministic, human-readable justification for the verdict: empty for a confident present/absent answer, and the semicolon-joined sink descriptions for an UNRESOLVED one.

type VerdictOutcome added in v0.3.2

type VerdictOutcome string

VerdictOutcome is the three-valued soundness verdict for a callers/callees/ reachability query. It replaces the implicit two-valued "some edges" vs "empty" answer, which silently conflated a proven absence with an absence caused by an unresolved dispatch on the path. An empty answer is a confident negative only when no dispatch/edge-level soundness sink lies on the queried node or an examined path; otherwise the absence is UNRESOLVED — an edge may be missing, not proven absent.

This composes with, and sits above, the module-level resolution gate (ResolveSymbolModule) and the failed-package caveat: a verdict is a confident negative only when the module gate resolves, no failed-package caveat fires, and this dispatch gate reports RESOLVED-ABSENT.

const (
	// VerdictResolvedPresent is a query that found at least one edge: the
	// relationship is present. Presence is never downgraded — an over-approximated
	// edge only ever adds callers/callees, so a non-empty answer stands.
	VerdictResolvedPresent VerdictOutcome = "RESOLVED-PRESENT"
	// VerdictResolvedAbsent is an empty answer across a fully-built path with no
	// soundness sink: a provably-absent edge. This is the only outcome that may be
	// reported as a confident "not reachable" / "no callers".
	VerdictResolvedAbsent VerdictOutcome = "RESOLVED-ABSENT"
	// VerdictUnresolved is an empty answer with a soundness sink on the queried
	// node or an examined path: an unresolved interface dispatch, an edge into a
	// module not built with bodies, an unresolved/reflect edge, or a leaf carrying
	// a documented soundness sink. The absence is not proven — an edge may simply
	// be missing.
	VerdictUnresolved VerdictOutcome = "UNRESOLVED"
)

func (VerdictOutcome) IsConfidentNegative added in v0.3.2

func (o VerdictOutcome) IsConfidentNegative() bool

IsConfidentNegative reports whether the outcome may be presented as a trustworthy "no edge" answer. Only RESOLVED-ABSENT qualifies.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL