Documentation
¶
Overview ¶
Package graph — algorithms.go implements Pass 4: graph algorithm computation over the merged entity/relationship set.
Six attributes are pre-baked into every entity:
- community_id (Louvain modularity-maximising community)
- centrality (betweenness centrality, weighted)
- pagerank (PageRank, damping=0.85, tol=1e-6)
- is_god_node (top 5% by combined betweenness+pagerank rank)
- is_surprise_endpoint (endpoint of one of the top-K cross-community "surprise" edges)
- is_articulation_point (cut vertex in the undirected graph)
Top-level corpus aggregates are exposed via AlgorithmResults: per-community stats, surprise-edge list, and timing.
Community detection algorithm ¶
We use Louvain modularity maximisation. The implementation is in-repo (louvain.go) rather than gonum's community.Modularize: gonum evaluates a candidate move by iterating every member of the target community, making one local-moving sweep cost Σ_c |c|². On the production corpus that was 3h15m, 99.5% of the whole indexing pipeline. The in-repo version is the SAME algorithm computed the standard way (incremental sigma_tot, neighbour-side k_{i,C}, CSR adjacency) at O(E) per sweep. See louvain.go.
It carries no PRNG at all: node ordering, adjacency ordering and move tie-breaks are fixed, so results are byte-identical across re-runs of the same graph by construction.
Leiden was evaluated for this release (#1382) and deferred because no production-quality Go Leiden library exists: github.com/vsuryav/leiden-go and github.com/k8nstantin/go-leiden are both pre-v1, un-tagged, and lack the weighted-graph + deterministic-seeding APIs required. An in-house Leiden port would require porting the full CPM refinement phase (~500 LOC) and is out-of-scope for this PR. The gonum Louvain implementation already produces stable, well-connected communities with a fixed seed; the main noise problem is addressed by the min-size denoise filter (see CommunityOptions.MinSize).
Layer 1 deterministic naming for Louvain communities.
For each community we collect its member entity names (label, qualified name, source-file basename), tokenise on camelCase / snake_case / kebab / path-separators, and compute TF-IDF where each community is a "document". The top 1-3 distinguishing terms become the community's auto_name (e.g. "order-viewset", "data-sync-task"). The result is written back onto each CommunityResult and serialised as `communities[].auto_name` in graph.json.
Stdlib only — no new dependencies.
Package graph — coverage.go implements the test-coverage graph algorithm.
ComputeCoverage walks all TESTS and CALLS relationships in a graph.Document to build a CoverageReport that answers three questions:
- Which production entities have at least one incoming TESTS edge? (covered = true)
- Which production entities are untested (no TESTS inbound)? (untested list, severity-ranked)
- What is the coverage percentage per directory / module?
Definitions ¶
"Test entity" — an entity whose SourceFile matches a test-file pattern (_test.go, .test.ts, .spec.ts, test_*.py, *_test.py, *_spec.rb, *Test.java, *Tests.java, *Spec.java, *Test.kt).
"Production entity" — any non-test entity whose kind is in the coverage scope: Function, Method, Class, Interface, Struct.
"TESTS edge" — a graph relationship with Kind == "TESTS". The testmap extractor (internal/extractors/cross/testmap) emits these edges directly. When none are present, ComputeCoverage falls back to a synthetic pass: for every test entity it walks its CALLS edges and emits a virtual TESTS link for each call target that is a production entity.
Severity rules for untested entities ¶
"high" — http_endpoint_definition or http_endpoint without TESTS "medium" — exported Function / Method (capitalised name, no leading _) "low" — everything else in scope
OTel span: graph.ComputeCoverage Issue #1323.
Package graph — cycles.go implements Tarjan's strongly-connected-components (SCC) algorithm over IMPORTS edges between entities. Each SCC of size > 1 represents a circular dependency (import cycle).
Beyond raw detection, each cycle is annotated with:
- WeakestLinkID: the edge whose source has the lowest PageRank — severing it causes the least disruption to the rest of the graph.
- SuggestedExtraction: the entity with the highest PageRank inside the cycle (best candidate to be extracted into a shared module that both sides can depend on without the cycle).
Package graph — diff.go computes structural diffs between two graph Documents.
DiffDocs is the entry-point: given two *Documents (graphA and graphB), it returns a DiffResult containing which entities were added/removed/modified and which relationships were added/removed. The algorithm uses canonical entity IDs as the primary key — the same entity appearing in both graphs (same ID) is considered "same entity". Key-field changes (name, source_file hash, start_line, end_line) trigger a "modified" classification.
This is a pure-Go, zero-dependency function. It never mutates the inputs.
Package graph — genpath.go implements the generation-file + pointer layout for the on-disk FlatBuffers graph (issue #5891, "Approach A").
Why this exists ¶
Historically every reindex overwrote a single fixed file, <dir>/graph.fb, via a tmp+rename. On Windows renaming OVER a file that another process still has memory-mapped fails with ERROR_USER_MAPPED_FILE — precisely the case for grafel's serve/MCP zero-copy reader, which keeps graph.fb mmap'd while agents query it. That hazard forced the mmap serve path OFF by default on Windows.
The generation layout removes the rename-over-a-mapped-file entirely:
- Each write emits a BRAND-NEW file graph.<gen>.fb (gen = monotonically increasing integer) and then atomically writes a tiny `current` pointer file naming the active generation. No existing (possibly mapped) file is ever renamed over.
- Readers resolve the active file through CurrentGraphPath: read `current` → return <dir>/graph.<gen>.fb; if the pointer is absent (a legacy repo that has not been reindexed since the migration) fall back to the flat <dir>/graph.fb. This makes the migration LAZY: existing flat-file repos keep working untouched and pick up the gen layout only on their next naturally-triggered reindex. There is NO forced reindex and NO fbversion bump.
The `.fb` suffix is preserved on gen files (graph.<gen>.fb) so existing strings.HasSuffix(path, ".fb") checks keep working unchanged.
Package graph defines the public on-disk schema produced by `grafel index`. The schema is stable and versioned; downstream tools (graph loaders, MCP servers, viewers) consume graph.json files written by this package.
Package graph — load.go provides the shared FB-first graph loader introduced by ADR-0016 flip-day (issue #808).
LoadGraphFromDir tries graph.fb first (via fbreader mmap + decode), then falls back to graph.json. All callers that previously opened graph.json directly should migrate to this helper so the code-base picks up the binary fast-path as graph.fb becomes universally present.
Daemon MCP reads graph.fb via graph_cache.go (zero-copy mmap path, no conversion). This helper is for CLI consumers (audit, quality, links, doctor, dashboard) where eager conversion to *Document is acceptable and simplicity matters more than zero-copy.
Package graph — louvain.go implements Louvain modularity maximisation in-repo, replacing gonum's community.Modularize.
Why ¶
gonum v0.17.0's undirectedLocalMover.deltaQ evaluates a candidate move of node i into community C by iterating EVERY MEMBER of C, reading edge weights out of a map[[2]int]float64. One local-moving sweep therefore costs Σ_c |c|² map lookups. On the 25-repo production corpus (427k entities / 1.85M relationships) the partition has a 109k-node community, Σ|c|² = 2.75e10, and community detection took 3h15m — 99.5% of the whole indexing pipeline. Measured scaling was N^1.78–1.88 where an O(E)-per-sweep Louvain is N^1.0.
What this file does differently ¶
Same algorithm (multi-level local moving → aggregation → repeat), same resolution parameter, computed the standard way:
- sigmaTot[c] (Σ of weighted degrees in c) is a running total updated in O(1) on every move. A community is never rescanned to compute it.
- k_{i,C} is accumulated by walking node i's NEIGHBOURS (degree ~4-10 on code graphs) into a scratch array indexed by community id, reset in O(deg) via a monotone stamp. Community membership is never iterated.
- Adjacency is flat CSR ([]int32 offsets/targets + []float64 weights) rather than map[[2]int]float64, which removes the cache-miss constant on top of the asymptotic win.
Per sweep: Σ|c|² → O(E).
Determinism ¶
There is no PRNG. Nodes are indexed by ascending gonum node id, each node's adjacency list is sorted by target index, sweeps run in ascending index order, and ties in the move decision are broken toward the lowest community index. Same input ⇒ same output, run to run.
Termination, and why louvainMaxSweeps is a REAL bound ¶
The intended argument is that the tie-break "move on equal gain only to a strictly lower community index" guarantees termination: a strict-gain move raises Q, a tie move holds Q and lowers Σ comm-index, so (Q, -Σ idx) is lexicographically monotone. That argument has a hole. The tie branch in localMoving assigns bestGain = gain, so bestGain can ratchet DOWNWARD by up to louvainMinGain per accepted tie; a chain of ties can therefore land a move whose true gain is below staying put by O(deg · 1e-12). Q is consequently not strictly monotone, and the pair is not strictly lexicographically monotone. No cycle has been constructed or observed, so this is theoretical — but it means louvainMaxSweeps is the actual backstop, not a formality.
More importantly, the cap BINDS on realistic input. Sweeps-to-convergence at level 0, measured on Barabási-Albert graphs (scale-free, the shape a real code graph has), grows roughly N^0.65:
barabasi-albert n= 8000 117 sweeps barabasi-albert n= 32000 266 sweeps barabasi-albert n=128000 387 sweeps planted-partition n=16000 10 sweeps <- what the planted fixtures measure
Extrapolated to the 427k-entity corpus that is several hundred sweeps, so the original cap of 100 would have truncated level 0 well short of convergence.
WHAT THAT TRUNCATION ACTUALLY COSTS, MEASURED — this matters, because the intuitive answer is wrong. Q at cap=100 vs cap=1000 on the BA family:
n= 8000 cap100 Q=0.388869 cap1000 Q=0.388219 (-0.000650) n= 32000 cap100 Q=0.380061 cap1000 Q=0.378383 (-0.001678) n=128000 cap100 Q=0.375377 cap1000 Q=0.371322 (-0.004055)
Running to convergence scores slightly WORSE here, not better. That is not a paradox: multi-level Louvain's final Q is not monotone in how far level 0 is converged — a truncated level 0 hands a different (here, slightly luckier) starting partition to aggregation. An independent review measured the opposite sign on its own BA fixtures (-0.001223 at n=128000 for cap=100). Both results are inside ±0.005. The honest reading is that the cap's effect on quality is NOISE, not a mechanism.
So the cap is 1000 for well-definedness, not for a quality win: "Louvain run to convergence" is a specified algorithm whose output depends only on the graph, whereas "Louvain truncated at 100 sweeps" makes the partition a function of an arbitrary constant, drifting with graph size in a way nothing measures — and it was entirely unmeasured at 427k. Choosing 100 because it scored +0.004 on one synthetic family would be tuning to fixtures. Cost of the headroom: BA-128k goes 944ms -> 3.26s; each corpus sweep is O(E)=1.85e6, so this is seconds against 3h15m saved.
louvainMaxLevels is genuinely slack (max observed depth: 6).
Package graph — manifest.go defines the on-disk manifest for a MULTI-SEGMENT generation directory (the #5890 segmented streaming-write epic; this is the DARK read-substrate slice #5901, which defines the format + teaches readers to route to it but writes NOTHING segmented yet).
Layout recap ¶
A graph that fits in ONE segment stays a plain graph.<gen>.fb file — no dir, no manifest, byte-identical to today (the single-file fast path). Only a MULTI-segment graph uses the directory layout:
graph.<gen>/
seg-0000.fb
seg-0001.fb
...
manifest.json
The `current` pointer may then name the gen DIR (graph.<gen>) or its manifest (graph.<gen>/manifest.json) — so `current` is NO LONGER guaranteed to name a *.fb file (see genpath.go's CurrentGraphDescriptor).
What the manifest records (decisions 3+4+5) ¶
- A FormatVersion for the manifest schema itself (independent of fbversion.Version, which versions the FlatBuffers *payload* inside each seg-*.fb — this slice does NOT bump fbversion).
- One SegmentMeta per segment file, carrying its Kind (entity vs relationship — so the two streams can be listed/counted independently), its entity/relationship counts, and — for entity segments — the MIN and MAX entity-ID key it contains. The reader uses that [min,max] range to SKIP segments that cannot hold a looked-up key, avoiding the O(S) per-segment fan-out (decision 4).
Package graph — module_gds.go implements module-LEVEL graph data-science algorithms (issue #1384, part of epic #1380).
Why ¶
algorithms.go runs SCC / PageRank / betweenness over the ENTITY graph (one node per function / class / file). On a real-world codebase that produces tens of thousands of nodes and the high-centrality picture is dominated by widely-shared utility entities. The strategic question the user actually has — "which MODULES form a cycle? which MODULE is the blast-radius hub?" — needs a bird's-eye view at module granularity.
Aggregated module graph ¶
We collapse the entity graph onto modules:
for each entity-level edge A → B with kind ∈ realKinds:
let mA = module(A), mB = module(B)
if mA == mB: skip (intra-module: not a module-edge)
if mA == "" || mB == "": skip (untagged: would pollute results)
accumulate edgeWeight[mA → mB] += 1
"module" is read from Entity.Properties["module"] when present; if the document also carries pre-baked synthetic Module containers (the post-#1383 shape — Kind="Module", DEPENDS_ON edges between them with weight) we use those directly to avoid recomputing. realKinds excludes the synthetic scaffolding kinds (CONTAINS, STEP_IN_PROCESS, ENTRY_POINT_OF, DEPENDS_ON when between module nodes — those are themselves the aggregated edges).
Outputs ¶
- ModuleSCC: strongly-connected components of size >= 2 in the module graph. These are the circular module dependencies. Tarjan iterative.
- PageRank + betweenness: module-level centrality scores. The top-N are the modules everything else depends on (PageRank) and the modules that sit on the most shortest paths (betweenness — bottlenecks).
Determinism ¶
Same input ⇒ same output. Adjacency lists are sorted, Tarjan walks nodes in sorted order, ties in score-ranked output are tiebroken on module ID. Scores are rounded to 4 decimal places (same policy as algorithms.go) so minor solver-tolerance noise does not flip the top-N membership across runs.
Package graph — nplus1.go implements the N+1 query anti-pattern detector.
The detector walks the entity+relationship graph produced by grafel index and flags function/method entities that contain ORM query calls inside an apparent loop body. It operates entirely on the on-disk graph.Document, so it runs after indexing without re-parsing source files.
Algorithm ¶
- Build adjacency maps from Relationship slices (CONTAINS, CALLS, QUERIES).
- For every entity whose Properties["orm"] is set (i.e. emitted by the ORM-query pass, #723), collect the set of query-call entity IDs.
- Walk each Function/Class entity's CONTAINS sub-graph; when we find a node whose subtype indicates a loop ("for_loop", "while_loop", "list_comprehension", etc.) and that node has a CALLS/QUERIES edge to a known query entity, flag the enclosing function.
- Alternatively (and more robustly for graph shapes where loops are not explicit entities), inspect each QUERIES edge: when the caller entity's Properties["loop_context"] is "true" — set by the ORM enricher — it is already a known N+1 site.
- As a belt-and-suspenders pass, any entity with subtype in the loop-subtype set that directly emits a QUERIES edge is itself an N+1 site.
False-positive suppression ¶
Queries inside prefetch/eager-load calls are excluded via an ORM-safe-method allowlist. Developers may also annotate a call site with the comment "grafel:nplus1-safe" in the source (recorded as Properties["nplus1_safe"]="true" by the ORM extractor) to opt out.
Output ¶
Each finding is a Finding value. Callers may attach the property anti_pattern="n_plus_1" to the flagged Relationship or Entity, or emit the findings through the /api/quality/anti-patterns/{group} endpoint.
Package graph — pr_impact.go computes diff/PR-scoped impact analysis and cross-change merge-risk triage (issue #4292).
This file is pure-Go and MCP-free, mirroring AnalyzeOrientation (#4290): the thin MCP handler loads the per-ref graphs and supplies the changed-entity set (reusing the same DiffDocs machinery that backs grafel_diff_refs), then calls into these functions. Keeping the analysis pure makes it unit-testable on an in-memory graph with no daemon, no registry, and no git.
Two analyses:
AnalyzePRImpact — single change. Given the HEAD graph (entities + relationships) and the set of entity IDs the diff touched, it resolves which communities those entities belong to (Entity.CommunityID from the Pass-4 algorithm pass) and walks the INBOUND dependency graph to find the downstream blast radius — every entity that transitively depends on a changed entity. This is the same inbound-BFS reachability used by grafel_impact_radius, generalised to a *set* of seeds.
AnalyzeMergeRisk — multiple changes. Given each change's impacted- community set, it intersects them pairwise; any two changes whose impacted communities overlap are a merge-order / conflict risk. Pairs are ranked by shared-community count (descending), with the shared community ids listed.
All outputs are deterministic with ID/index tiebreaks, per the #481 contract.
pr_impact_infer.go — issue #6042: infer a community for a changed entity the group-algo partition has NEVER SEEN, from the entities around it that it has.
WHY THIS EXISTS. The overlay is computed from the last indexed group union, so an entity that a PR ADDS is absent from it by construction. #6006 made that state an explicit decline rather than a silent "no conflicts" — correct, but it turns the single most ordinary PR shape there is (one that only adds code) into a tool that always refuses to answer.
WHAT IT MUST NOT BECOME. Inference presented as fact is the #6006 defect one layer up: a confident answer the caller cannot tell from a measured one. So every inferred placement is labelled per entity (CommunitySource), carries its own margin (CommunityInference), is tracked per community (ImpactedCommunity. InferredOnly) and per merge-risk pair (MergeRiskPair.InferredOnly), and inference that fails falls back to the existing decline path unchanged.
── Who is a candidate ──────────────────────────────────────────────────────
ONLY entities ABSENT from the overlay — Entity.CommunityID == nil. This is not the same as communityOf(e) < 0, and the difference matters:
nil the partition has never seen this entity: it did not
exist at the last group index. THIS is #6042.
non-nil, negative (-2) the partition DID see it and declined to place it
(groupalgo writes -2 for "not assigned a community";
legacy graph.json can carry -1 directly).
Inferring for the second class would override the group algorithm's own decision with a path-prefix heuristic — a strictly worse answer than the one community detection already gave, wearing a confident label. See TestAnalyzePRImpact_NegativeCommunityIDIsNotInferred.
── The signals, and why these three ────────────────────────────────────────
container (primary) — the placed entities sharing the new entity's
SourceFile, plus any placed CONTAINS parent. A file is the smallest unit
community detection would essentially never split, so a new function in
an already-placed file is in that file's community with high confidence.
module (fallback) — Properties["module"], stamped on both the full
(cmd/grafel/index.go) and incremental (extractors/incremental.go) paths
and round-tripped through graph.fb (load.go restores the FB scalar into
props). It votes ONLY when the container abstains, because it is not an
independent signal: module is module.Derive(SourceFile), a pure function
of the path, so the module histogram is a strict SUPERSET of the file
histogram. Letting both vote would count one measurement twice, and a
file-vs-module disagreement is never "two sources disagree" — it is "my
file leans X while the wider directory leans Y", where the file is
strictly the more specific evidence.
module.Derive is also weak on its own: MarkerFileNames has no per-Go-
package marker, so a single-module Go repo falls through to DefaultDepth
and everything under (say) internal/graph/** shares one label. A bare
plurality over such a bucket (26 of 51) is not a community signal, so the
module vote additionally requires a real sample AND real concentration.
call targets — placed entities the new entity CALLS/USES/EXTENDS/…, over an
ALLOWLIST of edge kinds (inferTargetKinds). This is the only signal not
derived from the file path, and the closest to what community detection
itself would have seen. IMPORTS is excluded: importing two placed
packages is universal, so it would clear the >= 2 threshold for
essentially every new file while carrying almost no community
information. CONTAINS is excluded because it IS the container signal and
must not vote twice under a second name.
── The decision rule: unanimity ────────────────────────────────────────────
At most two votes are ever cast — the container-or-module primary, and the call targets — and BOTH must agree:
primary only -> infer targets only -> infer primary + targets agree -> infer (the strongest case) primary + targets differ -> DECLINE nothing votes -> DECLINE
This is the issue's own rule ("no inference at all when signals disagree"), and it is deliberately arithmetic-free. An earlier cut scored the signals 3/2/1 and required the winner to outweigh all dissent; once module is subordinate to the container that comparison can never actually block anything, so it was a guard held by nothing — exactly the kind of untested branch this feature must not ship. Each signal also abstains internally when its own evidence has no unique winner (a file split 1-1), rather than letting map iteration order decide.
── What is deliberately NOT inferred ───────────────────────────────────────
- Entities the overlay placed (it is ground truth) or explicitly declined to place (see "Who is a candidate").
- Removed entities: absent from the head graph, so they have no neighbours; the diff record is not a community signal.
- Blast-radius entities: only the CHANGED set drives the verdict, and inferring for an unbounded downstream set would cost far more than it is worth.
- Chained inference. Only OVERLAY-placed entities vote. An inferred placement is a guess; letting guesses vote would propagate one weak signal across an entire new package and make the result depend on processing order.
── Cost ────────────────────────────────────────────────────────────────────
Nothing is built unless a changed entity is present in the head graph AND absent from the overlay. Then: one pass over `entities` filtered to the small set of files and modules those entities occupy, plus outbound/CONTAINS edges captured during the adjacency pass AnalyzePRImpact already makes, for changed ids only. No BFS, no transitive walk. See BenchmarkAnalyzePRImpact_Inference, whose fixture ASSERTS that inference actually fires before timing it.
Package graph — reader_open.go is the segment-aware reader-open seam for the #5890 gen-dir read substrate (#5901). It turns a state dir (or a resolved gen dir) into an fbreader.GraphView WITHOUT the caller needing to know whether the graph is a single mmap'd file or N segment mmaps:
- single-file / legacy flat / absent → fbreader.Open (today's *Reader, byte-identical — the common path is completely unchanged).
- segment-set → fbreader.OpenSegmentsWithRanges (a *MultiReader), threading the manifest's per-segment [MinKey,MaxKey] so LookupEntityByID skips out-of-range segments (decision 4).
The reader-open does NOT itself enforce the fbversion gate — callers that need it (load.go, graph_cache.go) compose it with their existing FormatVersionError check, so behaviour for an old-flat-below-fbversion graph is unchanged (no fbversion bump in this slice).
Package graph — service_cycles.go implements strongly-connected-component (SCC) detection over a SERVICE-level (repo-level) graph.
Per-entity import-cycle detection (cycles.go) cannot see cross-service coupling that flows THROUGH synthetic boundary nodes: an HTTP `FETCHES` edge lands on an endpoint-synthetic node, and a Kafka publish/subscribe pair is mediated by a topic node. As a result two services that mutually depend on each other (orders calls payments over REST while payments calls back over a Kafka topic) never share a direct entity→entity edge, so Tarjan over IMPORTS finds nothing.
The cross-repo link passes (internal/links P1-P7) already resolve those boundaries into direct service→service edges (`<repo>::<id>` source/target, relation calls / imports / publishes_to). FindServiceCycles aggregates those resolved links into a directed service graph and runs Tarjan SCC over it, reporting every SCC of size >= 2 as a service-level circular dependency.
Package graph — tests_walkup.go implements the TESTS edge walk-up pass.
Problem: the testmap extractor emits TESTS edges that point at the helper Operation a test directly calls (e.g. `test_create_order → _calculate_total`). The helper is not a meaningful coverage target; the real subject is the viewset / handler / controller that wraps it. Coverage metrics therefore severely undercount TESTS coverage — 0.1% (87 edges / 8 709 production entities) was measured on a real corpus before this pass.
Algorithm (DeriveTestsWalkUp):
Build an inbound-CALLS index: helperID → []callerIDs for ALL production entities (not just helpers — we filter per-entity below).
Scan every TESTS edge. For each target that is a production entity, check whether it has inbound CALLS edges from other production entities (i.e., it is called by viewsets / handlers / controllers and not just directly tested).
If the caller set is non-empty: a. Filter to at most maxCallersPerHelper callers (configurable, default 5). When the caller set is larger than the limit the helper is almost certainly a widely-shared utility — emitting derived edges for every caller would inflate coverage; skip instead. b. For each selected caller, emit a derived TESTS relationship: test_fn → caller (Kind="TESTS") with properties: derived=helper:<helperID> confidence=0.7 (below 1.0 to distinguish from explicit edges) source=tests-walkup
Deduplicate: if an explicit TESTS edge already covers the caller, skip.
The derived edges are appended to doc.Relationships so all downstream consumers (ComputeCoverage, MCP tools, dashboard) pick them up automatically.
OTel span: graph.DeriveTestsWalkUp Issue: tests-edge-walk-up from helpers (follow-up to #1653).
Index ¶
- Constants
- func AnnotateDocument(doc *Document, report *NPlusOneReport) int
- func AssignCommunityNames(results []CommunityResult, entities []Entity, communityOf map[string]int)
- func BetweennessSampleThreshold() int
- func BuildGraph(entities []Entity, rels []Relationship) (*simple.WeightedDirectedGraph, *nodeIndex)
- func CommunityInputHash(entities []Entity, rels []Relationship) string
- func ComputeCentrality(g *simple.WeightedDirectedGraph, idx *nodeIndex) (map[string]float64, map[string]float64)
- func CountImportCyclesFromView(v fbreader.GraphView) int
- func CoverageTotalsFromView(v fbreader.GraphView) (totalProduction, coveredProduction int)
- func CurrentGraphMtime(dir string) (mtime time.Time, ok bool)
- func CurrentGraphPath(dir string) string
- func EntityID(repo, kind, name, sourceFile string) string
- func FormatVersionReason(found, required int) string
- func GCStaleGens(dir string, currentGen uint64) []string
- func GenDirName(gen uint64) string
- func GenFileName(gen uint64) string
- func IdentifyArticulationPoints(g *simple.WeightedDirectedGraph, idx *nodeIndex) map[string]bool
- func IsDirectedServiceRelation(relation string) bool
- func IsGraphFileName(base string) bool
- func LinkStatsPath(stateDir string) string
- func NextGen(dir string) uint64
- func OpenSegmentReader(genDir string) (fbreader.GraphView, error)
- func ReaderForDir(dir string) (fbreader.GraphView, error)
- func ReindexRequiredReason(dir string) (required bool, reason string)
- func RelationshipID(fromID, toID, kind string) string
- func SegmentFileName(i int) string
- func SidecarPath(stateDir string) string
- func SortDocumentForEmission(doc *Document)
- func SummariseFindingsText(r *NPlusOneReport) string
- func WriteAtomic(outPath string, doc *Document, pretty bool) error
- func WriteCurrentPointer(dir, genName string) error
- func WriteCurrentPointerRaw(dir, name string) error
- func WriteGenGraph(dir string, buf []byte) (genPath string, err error)
- func WriteLinkStats(stateDir string, side *LinkStatsSidecar) error
- func WriteManifest(genDir string, m *Manifest) error
- func WriteSidecar(outPath string, side *GraphStatsSidecar, pretty bool) error
- type AlgorithmResults
- type AlgorithmStats
- type BlastEntity
- type ChangeImpact
- type ChangeSet
- type ChangedEntity
- type CommunityInference
- type CommunityOptions
- type CommunityResult
- type CoverageReport
- type CrossCutEdge
- type CycleEdge
- type DiffEntityEntry
- type DiffRelEntry
- type DiffResult
- type DiffSummary
- type DirCoverage
- type Document
- type Entity
- func (e Entity) MarshalJSON() ([]byte, error)
- func (e *Entity) PropDelete(key string)
- func (e Entity) PropGet(key string) string
- func (e Entity) PropLen() int
- func (e Entity) PropLookup(key string) (string, bool)
- func (e Entity) PropRange(f func(k, v string) bool)
- func (e *Entity) PropSet(key, val string)
- func (e Entity) PropsClone() types.Props
- func (e *Entity) PropsReplace(props map[string]string)
- func (e Entity) PropsSnapshot() map[string]string
- func (e *Entity) UnmarshalJSON(data []byte) error
- func (e Entity) WithProperties(props map[string]string) Entity
- type EntityCoverageResult
- type EntityView
- type FileCoverage
- type FormatVersionError
- type GraphDescriptor
- type GraphKind
- type GraphStatsSidecar
- type GraphStream
- func (s *GraphStream) Close() error
- func (s *GraphStream) DocStats() Stats
- func (s *GraphStream) EachEntity(visit func(Entity) bool)
- func (s *GraphStream) EachEntityKind(visit func(kind string) bool)
- func (s *GraphStream) EachRelationship(visit func(Relationship) bool)
- func (s *GraphStream) EntityCount() int
- func (s *GraphStream) Header() fbreader.GraphMeta
- func (s *GraphStream) RelationshipCount() int
- type ImpactedCommunity
- type ImportCycle
- type KeyEntity
- type KindVocabularyState
- type LinkStatsSidecar
- type Manifest
- type MergeRiskPair
- type MergeRiskResult
- type ModuleAlgorithmResults
- type ModuleAlgorithmStats
- type ModuleCentrality
- type ModuleCoverage
- type ModuleEdge
- type ModuleSCC
- type NPlusOneFinding
- type NPlusOneReport
- type OrientQuestion
- type OrientationOptions
- type OrientationResult
- type PRImpactOptions
- type PRImpactResult
- type PersistedStats
- type Relationship
- func (r Relationship) MarshalJSON() ([]byte, error)
- func (r *Relationship) PropDelete(key string)
- func (r Relationship) PropGet(key string) string
- func (r Relationship) PropLen() int
- func (r Relationship) PropLookup(key string) (string, bool)
- func (r Relationship) PropRange(f func(k, v string) bool)
- func (r *Relationship) PropSet(key, val string)
- func (r *Relationship) PropsReplace(props map[string]string)
- func (r Relationship) PropsSnapshot() map[string]string
- func (r *Relationship) UnmarshalJSON(data []byte) error
- func (r Relationship) WithProperties(props map[string]string) Relationship
- type RelationshipView
- type SegmentKind
- type SegmentMeta
- type ServiceCycle
- type ServiceEdge
- type ServiceLink
- type Stats
- type SubtypeConflict
- type SurpriseEdge
- type UncoveredEntity
- type WalkUpStats
Constants ¶
const ( CoverageStateReach = "reach" CoverageStateContractOnly = "contract-only" CoverageStateUncovered = "uncovered" )
Coverage-state string constants (#4662). The full three-state vocabulary is reach | contract-only | uncovered; only the latter two appear in UncoveredEntity.State (reach-covered entities are not listed as uncovered).
const ( // CommunitySourceOverlay — measured. The group-algo overlay placed this // entity directly; it existed at the last group index. CommunitySourceOverlay = "overlay" // CommunitySourceInferred — deduced from placed neighbours. Good enough to // triage merge risk with, not good enough to present as measured. CommunitySourceInferred = "inferred" // CommunitySourceNone — not placed and not inferrable. These entities are // what #6006's decline path exists for. CommunitySourceNone = "none" )
CommunitySource labels HOW a changed entity's community was determined. It is the whole point of #6042: an inferred placement a caller cannot distinguish from a measured one is worse than no placement at all.
const AmbiguousConfidenceThreshold = 0.5
AmbiguousConfidenceThreshold is the effective-confidence ceiling below which an edge is treated as "ambiguous" for question mining. Fallback speculation (0.4) and anything more pessimistic falls below it; regex/AST-grade edges (>=0.7) do not. Edges with a zero/unset confidence read as 1.0 (direct AST) and are never ambiguous (see types.EffectiveConfidence).
const CurrentPointerName = currentPointerName
CurrentPointerName is the exported basename of the `current` generation pointer file. Callers that scan a directory tree for state dirs (e.g. the cross-repo link-pass discovery walk in internal/links) use it as the cheapest marker of a state dir whose active graph is a segment-set (which has no flat graph.fb to match on), then confirm via CurrentGraphDescriptor.
const ManifestFileName = "manifest.json"
ManifestFileName is the fixed basename of the per-generation segment manifest inside a multi-segment gen dir (graph.<gen>/manifest.json).
const ManifestFormatVersion = 1
ManifestFormatVersion is the CURRENT on-disk schema version of manifest.json. It versions the manifest STRUCTURE only; it is deliberately separate from fbversion.Version (which versions the FlatBuffers payload of each seg-*.fb). A reader rejects a manifest whose FormatVersion it does not understand.
const RelationshipIDProperty = "\x00id"
RelationshipIDProperty is the reserved property key under which graph.fb persists Relationship.ID (#6085). graph.fb has no id field, and (from, to, kind) is NOT a unique key for an edge: producers deliberately mint distinct IDs for edges that share a triple — migration ops salted per operation (internal/engine/migration_schema_ops.go), phantom CALLS salted per HTTP method (internal/links/phantom_edges.go), process/event steps salted per step index — so identity cannot be recovered from the endpoints alone.
The leading NUL makes the key unreachable by construction rather than by convention: no producer can collide with it, so identity can never be hijacked by a payload key and a payload key can never be swallowed as identity. A plain "id" WOULD collide — relationships carrying a producer-set "id" property already exist in tree (internal/mcp/mmapview_test.go:70) — and the failure would be silent in both directions: the loader would lift the producer's value into Relationship.ID and delete the property, and the writer would overwrite the producer's value on any salted edge.
fbwriter writes the key only when the ID is not derivable from the triple; the loader strips it back out of Properties after restoring the ID. It must never be read or written as an ordinary property.
const SchemaVersion = 1
SchemaVersion is the integer version of the on-disk graph.json schema. Bump when making a backwards-incompatible change.
Variables ¶
This section is empty.
Functions ¶
func AnnotateDocument ¶
func AnnotateDocument(doc *Document, report *NPlusOneReport) int
AnnotateDocument mutates doc in-place, setting Properties["anti_pattern"] = "n_plus_1" on every Relationship whose FromID is involved in a finding and whose Kind is CALLS or QUERIES. Returns the count of annotated edges.
This is the "stamp the graph" step that makes N+1 findings visible to downstream consumers (MCP agents, the web dashboard) without requiring them to call DetectNPlusOne independently.
func AssignCommunityNames ¶
func AssignCommunityNames(results []CommunityResult, entities []Entity, communityOf map[string]int)
AssignCommunityNames computes a deterministic auto_name for every community in `results` using TF-IDF over the supplied entity corpus. Communities with no member tokens fall back to the legacy "community_<id>" label so callers can rely on auto_name being non-empty.
The function mutates `results` in place. Order of `results` is preserved.
func BetweennessSampleThreshold ¶ added in v0.1.3
func BetweennessSampleThreshold() int
BetweennessSampleThreshold exports the effective betweenness-sampling node threshold (honouring GRAFEL_BETWEENNESS_SAMPLE_THRESHOLD) so out-of-package consumers (e.g. the group-algo differential validator) can report whether a group was large enough to trigger the sampled approximation.
func BuildGraph ¶
func BuildGraph(entities []Entity, rels []Relationship) (*simple.WeightedDirectedGraph, *nodeIndex)
BuildGraph constructs a weighted directed graph plus an index mapping string entity IDs to gonum int64 node IDs. Edge weight follows the spec:
weight = max(1, callsite_count) * confidence
with both properties drawn from Relationship.Properties (string-typed).
The CSR is built FIRST and the gonum edge set is then materialised from it (#5954 S5/S6). The obvious ordering — build g by walking rels, then derive the CSR by walking rels again — reads every relationship's properties twice, and Relationship.PropsSnapshot() materialises a fresh map[string]string on every call. On a 60k-entity fixture whose relationships actually carry callsite_count/confidence (as corpus relationships do) that cost 31.6% of BuildGraph's wall time; it is invisible on a fixture with no properties, which is how it initially escaped review. Building in this order reads them exactly once, and additionally spares gonum the remove-and-reinsert churn that parallel edges used to cause, because the CSR arrives pre-collapsed.
The two representations are therefore no longer independent derivations. legacyBuildGraph in algorithms_csr_legacy_test.go preserves the original walk-rels-into-gonum implementation verbatim, and TestDirectedCSRMatchesGonumAdjacencyBitForBit checks BOTH the CSR and g against it, so the equivalence claim still rests on an independent oracle.
func CommunityInputHash ¶ added in v0.1.6
func CommunityInputHash(entities []Entity, rels []Relationship) string
CommunityInputHash returns a stable content hash over the COMMUNITY-RELEVANT input graph that BuildGraph would construct from (entities, rels): the set of node ids (every entity, including isolated ones — they still receive scores) plus the accumulated directed weighted edge set (endpoints both in the node set, self-loops dropped, parallel edges weight-accumulated — exactly the transformation BuildGraph applies). The hash is the COMPLETE determinant of the deterministic Pass-4 output (community partition + integer labels + PageRank + betweenness): the gonum node-id assignment is a pure function of entity insertion order, and louvainPartition is a deterministic function of the node/edge set with no PRNG at all, so two unions with the same CommunityInputHash produce byte-identical AlgorithmResults.
Scope note: "byte-identical" is a claim about two runs of the SAME grafel build. It is not a claim across builds — a change to the community algorithm itself (as in the #5954 replacement of gonum's community.Modularize with the in-repo Louvain in louvain.go) re-partitions the graph without changing this hash. That is by design: the hash covers the INPUT, not the algorithm version. Any such change therefore requires a full recompute of stored overlays even where the hash matches, and is gated on partition quality (modularity), not on equality with the previously stored partition.
This is the gate for incremental community detection (#5309 layer 4): when a reindex leaves this hash unchanged (docs/comment/config edits, or any change that touches neither a node nor a community-graph edge), the prior overlay is exactly what a full recompute would yield, so the recompute can be SKIPPED while maintaining strict parity with a full rebuild. When the hash changes, a full deterministic recompute runs (CPU-bounded by the daemon-wide ceiling, #5602).
The hash is order-independent (node ids and accumulated edges are both sorted before hashing) so it depends only on the graph CONTENT, not on the order entities/rels happen to arrive in — a re-sort of the same union yields the same hash. Edge weights are rendered at the same determinism rounding the algorithm layer uses so float jitter never spuriously invalidates.
func ComputeCentrality ¶
func CountImportCyclesFromView ¶ added in v0.2.0
CountImportCyclesFromView returns len(FindImportCycles(...)) for the graph behind v, without materialising a Document.
pagerank is not accepted because the count is independent of it: pagerank only annotates each cycle with a weakest link and an extraction candidate, never adds or removes one. (The daemon's caller always passed nil anyway.)
func CoverageTotalsFromView ¶ added in v0.2.0
CoverageTotalsFromView returns the TotalProduction and CoveredProduction counts ComputeCoverage would report for the same graph, without materialising a Document.
func CurrentGraphMtime ¶ added in v0.2.0
CurrentGraphMtime resolves dir's active graph — segment-set aware — and returns its freshness mtime. #5915 J2 slice-3: every existence/freshness gate that used to os.Stat(CurrentGraphPath(dir)) directly only ever sees a flat .fb path, which is ABSENT for a segment-set repo (graph.<gen>/ dir + manifest.json, no flat .fb) — that stat silently reports "never indexed" / mtime-zero for a repo that is in fact freshly indexed. This is the shared, exported resolution those call sites route through instead of duplicating the descriptor-branch inline:
- GraphSingleFile (including the legacy flat fallback): the resolved .fb file's own mtime — byte-identical to the pre-fix os.Stat behavior.
- GraphSegmentSet: the gen dir's manifest.json mtime, the atomic commit point of a segment-set rebuild (verified newer than every segment file it names) — mirrors internal/graph/groupalgo's graphSourceMtime and cmd/grafel/daemon_tier.go's tierReloadCallback, which use the same signal for cold-wake / overlay staleness.
- GraphAbsent, or a resolved path whose stat fails: ok=false.
Lives here (not in internal/graph/groupalgo, which already has an unexported graphSourceMtime) because groupalgo imports internal/daemon, so internal/daemon call sites (deadref.go, algo/cache.go) cannot import groupalgo without a cycle; internal/graph has no such constraint and is already imported by every one of these sites.
func CurrentGraphPath ¶ added in v0.1.9
CurrentGraphPath resolves the active graph file for a state dir. It is the single central resolver every reader routes through:
- If <dir>/current names an existing graph.<gen>.fb, return that path.
- Otherwise fall back to the legacy flat <dir>/graph.fb path.
The flat path is returned even when it does not exist on disk — callers stat or open it and handle absence, exactly as they did before the gen layout. This preserves the pre-existing "graph absent" semantics for a brand-new, never-indexed repo (no pointer, no flat file).
func EntityID ¶
EntityID computes a stable 16-char hex id from a repo tag and an entity's identity fields (kind + name + source file).
func FormatVersionReason ¶ added in v0.1.9
FormatVersionReason renders the shared human-readable "reindex required" explanation naming both the found and required graph.fb format versions. Exported so every caller that detects a *FormatVersionError (the header-only ReindexRequiredReason check above, and internal/mcp's reload loop reacting to a failed LoadGraphFromDir) renders the IDENTICAL wording — one consistent message regardless of which code path first observed the incompatibility.
func GCStaleGens ¶ added in v0.1.9
GCStaleGens best-effort unlinks generation files older than the immediately-previous generation, keeping the current gen and the one before it. The previous gen is retained because serve/MCP may still have it mapped during a reload overlap (it swaps to the new gen on its next mtime-drift poll); deleting it out from under an active mmap would be unsafe on Unix and impossible on Windows.
This is STRICTLY best-effort and MUST NOT affect the caller's success: a still-mapped gen on Windows fails to delete with a sharing violation, which is silently ignored and swept on a later write. A GC error never propagates. Returns the list of gen entries actually removed (for tests/observability).
#5901: a generation may be EITHER a single-file graph.<gen>.fb OR a multi-segment gen DIR graph.<gen>/ (holding seg-NNNN.fb + manifest.json). GC sweeps both: a stale gen file is unlinked; a stale gen dir is removed recursively (os.RemoveAll — every segment plus the manifest). The keep-window (current + immediately-previous) is computed over the UNION of gen files and gen dirs, so a mixed-history dir (some gens single-file, some segmented) still retains exactly the two newest generations regardless of shape.
func GenDirName ¶ added in v0.2.0
GenDirName renders the on-disk MULTI-SEGMENT generation directory name for a given generation: graph.<gen> (no .fb suffix). The future streaming writer (#5902) creates this dir and fills it with seg-NNNN.fb + manifest.json; the dark read substrate only needs the name to recognise + resolve it.
func GenFileName ¶ added in v0.1.9
GenFileName renders the on-disk filename for a given generation.
func IdentifyArticulationPoints ¶
func IdentifyArticulationPoints(g *simple.WeightedDirectedGraph, idx *nodeIndex) map[string]bool
IdentifyArticulationPoints implements Tarjan's articulation-point algorithm on the undirected projection of g. A node u is an articulation point if:
- u is the root of the DFS tree and has at least two children; OR
- u has a child v such that no descendant of v has a back-edge to a proper ancestor of u — i.e. low[v] >= disc[u].
Returns a set of original entity IDs.
func IsDirectedServiceRelation ¶
IsDirectedServiceRelation reports whether a link relation expresses a directed dependency that may participate in a service-level cycle.
func IsGraphFileName ¶ added in v0.1.9
IsGraphFileName reports whether base (a bare filename, not a path) is a FlatBuffers graph file recognised by the gen layout: the legacy flat "graph.fb" or a generation file "graph.<gen>.fb". Enumeration/GC callers use this so a directory walk that previously matched only "graph.fb" also sees the generation files.
func LinkStatsPath ¶ added in v0.1.8
LinkStatsPath returns the link-stats.json path inside stateDir.
func NextGen ¶ added in v0.1.9
NextGen returns the next generation integer to write in dir: one greater than the maximum generation observed among the `current` pointer and every graph.<gen>.fb already present. Returns 1 for a dir that has never held a gen file. Scanning the directory (not merely trusting the pointer) keeps the sequence monotonic even if the pointer was manually deleted while gen files remain, so a stale reader never resolves a freshly-written gen to an older file. #5902: the segmented writer emits a generation as a graph.<gen>/ DIR (not a graph.<gen>.fb file), and points `current` at that dir. NextGen therefore scans the UNION of gen files and gen dirs — and reads the pointer via the segment-aware readPointerRaw so a segment-set `current` value is counted too — keeping the sequence monotonic across a mixed single-file/segment-set history. Trusting only the file-shaped pointer/entries (as before) would let a fresh segmented write collide with the immediately-previous gen dir.
func OpenSegmentReader ¶ added in v0.2.0
OpenSegmentReader opens the multi-segment graph in genDir (which MUST contain a valid manifest.json) as a unified fbreader.GraphView with cross-segment key routing. A malformed/hostile manifest, or any segment failing to mmap, yields a non-nil error and a nil view. Callers own the returned view's Close.
This is the path-based entry point (for callers that already hold the gen dir, e.g. the daemon graph cache keyed by resolved path). ReaderForDir is the state-dir-based entry point that resolves single-file vs segment-set first.
func ReaderForDir ¶ added in v0.2.0
ReaderForDir resolves dir's active graph and opens it as an fbreader.GraphView:
- GraphSingleFile / GraphAbsent → fbreader.Open(desc.Path). This is the UNCHANGED common path — the returned view is the same *Reader over the same file today's fbreader.Open(CurrentGraphPath(dir)) returns, so single-file callers are byte-identical (an absent flat path returns fbreader.Open's usual open error, exactly as before).
- GraphSegmentSet → OpenSegmentReader(desc.GenDir), a *MultiReader with the manifest key ranges attached for LookupEntityByID pruning.
A corrupt segment-set manifest surfaces the descriptor's error. This does NOT apply the fbversion gate; callers compose it (see loadFBDoc / graph_cache).
func ReindexRequiredReason ¶ added in v0.1.9
ReindexRequiredReason performs a cheap, header-only check of dir's graph.fb (no entity/relationship materialization — just an mmap open plus one scalar read) and reports whether it was written by an older grafel build than this binary supports.
required is false — the overwhelmingly common case — when graph.fb is absent, unreadable, or already at/above minSupportedFBFormatVersion; a caller must not treat that as an error, only as "nothing to report". required is true only when the on-disk FormatVersion is strictly below what this binary accepts, in which case reason names both versions and points at the fix, mirroring FormatVersionError's wording so a human sees one consistent message whether they hit it via a failed MCP reload or via the persisted status-plane sidecar.
This is deliberately independent of any single load attempt: it is safe to call on every status-plane heartbeat (see internal/daemon's writeRepoStatusFile) so the persisted ReindexRequired flag is always freshly recomputed from the ACTUAL bytes on disk, never a one-shot flag that could go stale or get clobbered by a later write from a different writer.
func RelationshipID ¶
RelationshipID computes a stable 16-char hex id for an edge. It is the DEFAULT identity scheme, not the only one — see RelationshipIDProperty.
func SegmentFileName ¶ added in v0.2.0
SegmentFileName renders the zero-padded on-disk name for the i-th segment (seg-0000.fb, seg-0001.fb, ...). Decision 2's zero-padding keeps a plain lexicographic dir listing in segment order. Used by the future streaming writer (#5902) and the dark-slice test fixtures.
func SidecarPath ¶ added in v0.1.8
SidecarPath returns the graph-stats.json path inside stateDir.
func SortDocumentForEmission ¶ added in v0.2.0
func SortDocumentForEmission(doc *Document)
SortDocumentForEmission sorts doc in place into the canonical order every on-disk graph must be written in: entities by byte-wise ID (the FlatBuffers `(key)` order the reader binary-searches), relationships by (FromID, ToID, Kind, ID), and the Pass-4 outputs by their own stable keys.
It is the final, post-everything sort applied immediately before the graph is serialized. Even with every fan-in already sorted, intermediate passes that mutate slices in place (external synthesis appends, Pass 4 reads via maps) deserve a defensive belt-and-braces sort. Sorting on the canonical graph IDs keeps diffs minimal and is idempotent, so callers may run it more than once.
func SummariseFindingsText ¶
func SummariseFindingsText(r *NPlusOneReport) string
SummariseFindingsText returns a compact human-readable summary of an NPlusOneReport, suitable for terminal output.
func WriteAtomic ¶
WriteAtomic marshals doc to JSON and writes it to outPath atomically by writing to a sibling .tmp file and renaming on success. When pretty is true, the JSON is indented for human readability; otherwise it is minified (the default — minified output is materially smaller on real repos).
func WriteCurrentPointer ¶ added in v0.1.9
WriteCurrentPointer atomically points <dir>/current at genName (a bare graph.<gen>.fb filename) via a unique sibling temp + rename. The pointer file is tiny and NEVER memory-mapped, so this rename-over is safe on every platform (it is not the ERROR_USER_MAPPED_FILE hazard the gen layout removes); a bounded retry absorbs the transient Windows reader-open race.
func WriteCurrentPointerRaw ¶ added in v0.2.0
WriteCurrentPointerRaw atomically points <dir>/current at name, where name may be EITHER a single-file gen ("graph.<gen>.fb") OR a segment-set gen dir ("graph.<gen>") or its manifest ("graph.<gen>/manifest.json"). It is the segment-aware superset of WriteCurrentPointer (which only accepts the single-file shape); the two share the same atomic write + bounded retry via flipCurrentPointer. This is a format primitive for the FUTURE streaming writer (#5902) and the test fixtures — no existing producer calls it.
func WriteGenGraph ¶ added in v0.1.9
WriteGenGraph writes buf as a NEW generation file in dir, flips the `current` pointer to it, and best-effort GCs stale generations. It is the single producer primitive behind every writer (full-index and incremental): it never renames over an existing (possibly mapped) file — the gen filename is freshly allocated by NextGen, so its tmp+rename lands on a name nothing has open. Returns the absolute path of the gen file written, which callers pass to directory-keyed sidecar writers (WriteSidecar keys on filepath.Dir).
The `current` pointer is flipped only AFTER the gen file is durably renamed into place, so a reader never resolves the pointer to a half-written file. A GC failure after the flip does not fail the write (see GCStaleGens).
func WriteLinkStats ¶ added in v0.1.8
func WriteLinkStats(stateDir string, side *LinkStatsSidecar) error
WriteLinkStats atomically writes the link-stats.json sidecar into stateDir. The link pass is the sole writer of this file, so no read-modify-write / field-preservation is needed. Minified to match the other sidecars.
func WriteManifest ¶ added in v0.2.0
WriteManifest atomically writes m as graph.<gen>/manifest.json inside genDir (tmp + rename, mirroring WriteGenGraph / WriteCurrentPointer). It validates m first so a malformed manifest is never persisted. NOTE: this is the format primitive the FUTURE streaming writer (#5902) will call; the dark read slice only exercises it from tests to hand-build fixtures — no producer calls it.
func WriteSidecar ¶
func WriteSidecar(outPath string, side *GraphStatsSidecar, pretty bool) error
WriteSidecar emits the graph-stats.json sidecar next to the main document. outPath is the same path passed to WriteAtomic; the sidecar is written to the sibling file `graph-stats.json`. When pretty is true, the JSON is indented for human readability; otherwise it is minified (default).
Types ¶
type AlgorithmResults ¶
type AlgorithmResults struct {
CommunityID map[string]int // entity id -> community id
Centrality map[string]float64 // entity id -> betweenness
PageRank map[string]float64 // entity id -> pagerank
GodNodes map[string]bool
ArticulationPoints map[string]bool
SurpriseEndpoints map[string]bool
Communities []CommunityResult
SurpriseEdges []SurpriseEdge
Stats AlgorithmStats
}
AlgorithmResults bundles the per-entity and corpus-level outputs of Pass 4.
func RunAlgorithms ¶
func RunAlgorithms(entities []Entity, rels []Relationship) *AlgorithmResults
RunAlgorithms executes the full Pass 4 sweep with default options (community MinSize=5). It is a convenience wrapper over RunAlgorithmsWithOptions.
func RunAlgorithmsWithOptions ¶
func RunAlgorithmsWithOptions(entities []Entity, rels []Relationship, opts CommunityOptions) *AlgorithmResults
RunAlgorithmsWithOptions executes the full Pass 4 sweep and bundles every result into AlgorithmResults. opts controls community-detection behaviour (e.g. MinSize for denoising). The caller decides how to attach the per-entity attributes onto the on-disk Document and where to emit the corpus aggregate.
type AlgorithmStats ¶
type AlgorithmStats struct {
LouvainModularity float64 `json:"louvain_modularity"`
NumCommunities int `json:"num_communities"`
NumGodNodes int `json:"num_god_nodes"`
NumArticulationPts int `json:"num_articulation_points"`
NumSurpriseEdges int `json:"num_surprise_edges"`
RuntimeMS int64 `json:"runtime_ms"`
// DenoisedCommunities is the number of raw Louvain communities that were
// collapsed into the "ungrouped" bucket (community_id=-1) because they
// fell below CommunityOptions.MinSize. Zero when MinSize <= 1.
DenoisedCommunities int `json:"denoised_communities,omitempty"`
}
AlgorithmStats are the corpus-level metrics exposed both inside graph.json and inside the .grafel/graph-stats.json sidecar.
type BlastEntity ¶
type BlastEntity struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceFile string `json:"source_file,omitempty"`
CommunityID int `json:"community_id"`
HopDistance int `json:"hop_distance"`
}
BlastEntity is a downstream entity that transitively depends on a changed entity, annotated with the BFS hop distance from the nearest changed seed.
type ChangeImpact ¶
type ChangeImpact struct {
Ref string // ref / branch / PR label
Communities []int // impacted community ids (grouped only)
// CommunityDataAvailable carries PRImpactResult.CommunityDataAvailable for
// this ref (#6006). An empty Communities slice means "this change touched no
// community" only when this is true; when it is false the slice is empty
// because nothing was computed, and no conclusion about merge safety can be
// drawn from this ref at all.
CommunityDataAvailable bool
// OverlayEntityCount / InferredEntityCount carry this ref's measured/deduced
// split (#6042), so the merge-risk verdict can say whether it rests on the
// group-algo partition or on inference from placed neighbours.
OverlayEntityCount int
InferredEntityCount int
// InferredOnlyCommunities are the subset of Communities that this ref touches
// ONLY through inferred placements (PRImpactResult.InferredOnlyCommunityIDs).
// A pair overlapping solely on these is a deduced conflict.
InferredOnlyCommunities []int
}
ChangeImpact pairs a ref label with its impacted-community set. The MCP handler builds one per input ref (running AnalyzePRImpact for each), then hands the slice to AnalyzeMergeRisk.
type ChangeSet ¶
type ChangeSet struct {
Added []DiffEntityEntry
Removed []DiffEntityEntry
Modified []DiffEntityEntry
}
ChangeSet is the diff-derived input to AnalyzePRImpact: the entity IDs the diff classified as added / removed / modified between base and head. The MCP handler fills this from DiffDocs (the diff_refs engine), so this package does not duplicate the git-diff logic.
func (ChangeSet) ChangedIDs ¶
ChangedIDs returns the union of changed entity IDs (added+removed+modified), deduplicated and sorted for determinism. This is the seed set for both the community resolution and the downstream BFS.
type ChangedEntity ¶
type ChangedEntity struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceFile string `json:"source_file,omitempty"`
Change string `json:"change"` // added | removed | modified
CommunityID int `json:"community_id"` // -1 when ungrouped/unknown
// CommunitySource says HOW CommunityID was arrived at (#6042):
// "overlay" (measured — the group-algo partition placed it), "inferred"
// (deduced from placed neighbours, see pr_impact_infer.go), or "none" (not
// placed at all, CommunityID is -1).
//
// A caller that ignores this field is reading a guess as a measurement, which
// is the same defect class as #6006 — hence no omitempty: the label is always
// on the wire.
CommunitySource string `json:"community_source"`
// CommunityInference is the provenance of an inferred placement: which signals
// voted and how strong the deciding one was. Present only when
// CommunitySource == "inferred". A 26-of-51 plurality and a 40-of-41 consensus
// are both "inferred"; only this tells them apart.
CommunityInference *CommunityInference `json:"community_inference,omitempty"`
}
ChangedEntity is a slim record of an entity the diff touched, annotated with its community and a change-class (added | removed | modified).
type CommunityInference ¶ added in v0.2.0
type CommunityInference struct {
// Signals that voted, primary first. Two entries means the container-or-module
// primary and the call targets independently agreed — the strongest case.
Signals []string `json:"signals"`
// Support / Sample are the deciding signal's placed neighbours backing the
// chosen community, out of those it considered.
Support int `json:"support"`
Sample int `json:"sample"`
}
CommunityInference is the provenance of ONE inferred placement: which signals voted, and how strong the deciding signal's evidence was. Without the margin a 26-of-51 plurality and a 40-of-41 consensus are indistinguishable on the wire, and an agent cannot weigh them differently.
type CommunityOptions ¶
type CommunityOptions struct {
// MinSize is the minimum number of nodes a community must contain to be
// emitted as a named community. Communities smaller than MinSize have their
// members remapped to community -1 ("ungrouped") and are dropped from the
// CommunityResult slice. This eliminates singleton and micro-community
// noise without affecting the graph structure or any other algorithm pass.
//
// Default: 5 (configurable via ~/.grafel/algorithms.json or
// the GRAFEL_COMMUNITY_MIN_SIZE environment variable).
//
// Set to 1 to disable denoising (all communities emitted, matching the
// pre-#1382 behaviour).
MinSize int `json:"min_size"`
}
CommunityOptions controls community-detection behaviour. It is passed to RunAlgorithmsWithOptions; RunAlgorithms uses DefaultCommunityOptions.
func DefaultCommunityOptions ¶
func DefaultCommunityOptions() CommunityOptions
DefaultCommunityOptions returns the production defaults for community detection. MinSize=5 removes singletons and micro-communities that contribute noise without structural signal.
type CommunityResult ¶
type CommunityResult struct {
ID int `json:"id"`
Size int `json:"size"`
Modularity float64 `json:"modularity"`
TopEntities []string `json:"top_entities"`
AutoName string `json:"auto_name,omitempty"`
AgentName string `json:"agent_name,omitempty"`
}
CommunityResult summarises one Louvain community for the on-disk output.
AutoName is the deterministic Layer-1 label produced by AssignCommunityNames (TF-IDF over member entity names). It is always populated when communities are computed; consumers that previously fell back to "community_<id>" can now display AutoName directly.
AgentName is the Layer-2 label resolved by an MCP agent via submit_enrichment(kind="name_community"). It takes precedence over AutoName when present (issue #426).
func ComputeCommunities ¶
func ComputeCommunities(idx *nodeIndex, entityNames []string, opts CommunityOptions) ([]CommunityResult, map[string]int, float64, int)
ComputeCommunities runs Louvain modularity maximisation on the undirected projection of g. Returns:
- per-community summary (size, modularity contribution, top entity names)
- mapping from entity ID -> community id (community_id=-1 for ungrouped)
- overall modularity score
- number of raw communities that were denoised (below opts.MinSize)
Denoise: communities with fewer than opts.MinSize nodes are removed from the result slice and their members are assigned community_id=-1 ("ungrouped"). This prevents singleton/micro-community noise from reaching the MCP surface and the dashboard. Set opts.MinSize=1 to disable denoising.
type CoverageReport ¶
type CoverageReport struct {
// TotalProduction is the count of in-scope production entities.
TotalProduction int `json:"total_production"`
// CoveredProduction is the count of production entities with ≥1 TESTS edge.
CoveredProduction int `json:"covered_production"`
// CoveragePct is CoveredProduction/TotalProduction*100 (0 when no
// production entities exist).
//
// NOTE (#4662): this is the REACH-coverage % — production entities a test
// actually executes (CALLS the handler / executing TESTS edge / endpoint
// credited via a reached handler). It deliberately does NOT include
// contract-covered-only endpoints (see ContractCoveredOnly), so the headline
// figure stays honest about what is structurally exercised by a test.
CoveragePct float64 `json:"coverage_pct"`
// ContractCoveredOnly is the count of production endpoints that are NOT
// reach-covered but ARE contract-covered: an OFFLINE contract spec references
// the endpoint's route (asserts its shape) without any test executing the
// handler (#4662). These read as "contract-covered" rather than "untested".
//
// This is a SEPARATE, secondary band — it is never added into
// CoveredProduction / CoveragePct (which remain pure reach-coverage).
ContractCoveredOnly int `json:"contract_covered_only"`
// ContractCoveredPct is (CoveredProduction+ContractCoveredOnly)/TotalProduction
// *100 — the union band the UI renders behind the reach-coverage bar so the
// gap between "executed by a test" and "shape-asserted offline" is visible.
ContractCoveredPct float64 `json:"contract_covered_pct"`
// TotalTests is the number of test entities found.
TotalTests int `json:"total_tests"`
// TotalTestsEdges is the number of TESTS relationship edges (real or
// synthetic) that contributed to coverage.
TotalTestsEdges int `json:"total_tests_edges"`
// UncoveredEntities lists production entities without any TESTS edge,
// sorted by severity (high → medium → low) then by file+name.
UncoveredEntities []UncoveredEntity `json:"uncovered_entities"`
// ByDirectory contains per-directory coverage statistics, sorted by
// directory path. Only directories with ≥1 production entity are included.
ByDirectory []DirCoverage `json:"by_directory"`
// ByFile contains per-file coverage statistics, sorted by file path. Only
// files with ≥1 production entity are included. Directory rollups in
// ByDirectory are sums of the files sharing their directory, so the
// frontend can nest files under their directory using the path segments.
ByFile []FileCoverage `json:"by_file"`
// ByModule contains per-module (Properties["module"]) coverage statistics.
// Only populated when entities carry the "module" property.
ByModule []ModuleCoverage `json:"by_module"`
// EntitiesScanned is the total number of entities examined.
EntitiesScanned int `json:"entities_scanned"`
// RelationshipsScanned is the total number of relationships examined.
RelationshipsScanned int `json:"relationships_scanned"`
}
CoverageReport is the output of ComputeCoverage.
func ComputeCoverage ¶
func ComputeCoverage(doc *Document) *CoverageReport
ComputeCoverage analyses doc and returns a CoverageReport.
It runs in two phases:
Collect existing TESTS edges (emitted by the testmap extractor or a previous run of ComputeCoverage). Build a covered-entity set.
Synthetic fallback: for test entities with CALLS edges to production entities that are not yet covered, emit a virtual TESTS edge (recorded in the report totals but NOT written back to the document).
The caller is responsible for writing TESTS edges back to the document if persistence is desired.
type CrossCutEdge ¶
type CrossCutEdge struct {
FromID string `json:"from_id"`
ToID string `json:"to_id"`
FromName string `json:"from_name"`
ToName string `json:"to_name"`
Kind string `json:"kind"`
Score float64 `json:"score"`
Reasons []string `json:"reasons"`
FromCommunity int `json:"from_community"`
ToCommunity int `json:"to_community"`
}
CrossCutEdge is an edge that crosses a structural boundary.
type DiffEntityEntry ¶
type DiffEntityEntry struct {
ID string `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
SourceFile string `json:"source_file"`
ModifiedFields []string `json:"modified_fields,omitempty"`
}
DiffEntityEntry is a slim record describing an entity in the diff output.
type DiffRelEntry ¶
type DiffRelEntry struct {
FromID string `json:"from_id"`
ToID string `json:"to_id"`
Kind string `json:"kind"`
}
DiffRelEntry is a slim record describing a relationship in the diff output.
type DiffResult ¶
type DiffResult struct {
Group string `json:"group,omitempty"`
Repo string `json:"repo,omitempty"`
RefA string `json:"ref_a,omitempty"`
RefB string `json:"ref_b,omitempty"`
Summary DiffSummary `json:"summary"`
Entities struct {
Added []DiffEntityEntry `json:"added"`
Removed []DiffEntityEntry `json:"removed"`
Modified []DiffEntityEntry `json:"modified"`
} `json:"entities"`
Relationships struct {
Added []DiffRelEntry `json:"added"`
Removed []DiffRelEntry `json:"removed"`
} `json:"relationships"`
}
DiffResult is the complete output of DiffDocs.
func DiffDocs ¶
func DiffDocs(docA, docB *Document) DiffResult
DiffDocs computes the diff between docA (the "before" ref) and docB (the "after" ref). It returns an annotated DiffResult ready for JSON encoding.
Algorithm:
- Build an ID-keyed map for entities in each document.
- Walk docA.Entities — anything not in docB is "removed"; anything in docB with changed key fields is "modified".
- Walk docB.Entities — anything not in docA is "added".
- Build (fromID, toID, kind) sets for relationships; set-diff gives added and removed relationships.
- Derive files_changed from the union of changed source files.
type DiffSummary ¶
type DiffSummary struct {
EntitiesAdded int `json:"entities_added"`
EntitiesRemoved int `json:"entities_removed"`
EntitiesModified int `json:"entities_modified"`
RelationshipsAdded int `json:"relationships_added"`
RelationshipsRemoved int `json:"relationships_removed"`
FilesChanged int `json:"files_changed"`
}
DiffSummary aggregates the change counts.
type DirCoverage ¶
type DirCoverage struct {
Dir string `json:"dir"`
Total int `json:"total"`
Covered int `json:"covered"`
// ContractOnly is the count of entities in this directory that are
// contract-covered-only (shape-asserted offline, not reach-covered) (#4662).
ContractOnly int `json:"contract_only"`
CoveragePct float64 `json:"coverage_pct"`
}
DirCoverage is per-directory coverage statistics.
type Document ¶
type Document struct {
Version int `json:"version"`
GeneratedAt time.Time `json:"generated_at"`
Repo string `json:"repo"`
IndexerVersion string `json:"indexer_version"`
Stats Stats `json:"stats"`
Entities []Entity `json:"entities"`
Relationships []Relationship `json:"relationships"`
Communities []CommunityResult `json:"communities,omitempty"`
SurpriseEdges []SurpriseEdge `json:"surprise_edges,omitempty"`
AlgorithmStats *AlgorithmStats `json:"algorithm_stats,omitempty"`
// Phase 0 git metadata (#2088). Populated at index time by
// internal/gitmeta.Capture. Empty/false for non-git repos or when the
// graph was loaded from an older graph.fb written before this field was
// added (FlatBuffers defaults to "" / false for missing fields).
IndexedRef string `json:"indexed_ref,omitempty"`
IndexedSHA string `json:"indexed_sha,omitempty"`
IsWorktree bool `json:"is_worktree,omitempty"`
// CoverageStatus indicates whether the indexed working tree is a full
// or partial checkout (#2181 / M4 of monorepo epic #2175).
//
// Values (see internal/gitmeta constants):
// "" — field absent / legacy graph (treated as "full" by readers).
// "full" — normal full checkout; all tracked files are present.
// "partial" — git sparse-checkout is active; only a subset of paths
// was indexed. Readers should surface a badge in the UI.
CoverageStatus string `json:"coverage_status,omitempty"`
}
Document is the top-level structure written to <repo>/.grafel/graph.json.
func LoadGraphFromDir ¶
LoadGraphFromDir loads a graph.Document from dir, where dir is the .grafel state directory for a repo (the directory that contains graph.fb / graph.json).
Strategy (ADR-0016 flip-day, #808):
- If graph.fb exists and graph.json does NOT, read graph.fb.
- If graph.fb exists and graph.json ALSO exists, prefer the newer file. Log a warning when mtimes disagree (indicates a partial write or a mixed-version install).
- If only graph.json exists, fall back to JSON.
- If neither exists, return a non-nil error.
func LoadGraphHeaderOnlyFromDir ¶ added in v0.1.9
LoadGraphHeaderOnlyFromDir loads a HEADER-ONLY graph.Document from dir: all meta/Stats/community/algo fields are populated but the Entities and Relationships slices are left EMPTY (non-nil). It is the serve-side mmap- cutover load path (ADR-0027 PR7, memory epic #5850, opt-in via GRAFEL_SERVE_FROM_MMAP) — serve pairs the returned Doc with a resident fbreader.Reader and serves every row from the mmap, so the ~608 MB of entity/relationship rows is never materialized on the Go heap.
Header-only is only meaningful for graph.fb (the mmap-backed format). When only graph.json is present there is no mmap Reader to serve from, so this falls back to the full LoadGraphFromDir (JSON) load — correctness over the (absent) memory win. Callers that need a full Document (every CLI/full-Doc consumer) must keep using LoadGraphFromDir.
type Entity ¶
type Entity struct {
ID string `json:"id"`
Name string `json:"name"`
QualifiedName string `json:"qualified_name,omitempty"`
Kind string `json:"kind"`
Subtype string `json:"subtype,omitempty"`
SourceFile string `json:"source_file"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Language string `json:"language"`
Signature string `json:"signature,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
// PH8 (#2100): content-hash pointer into the shared embedding cache.
// When non-empty, readers load the vector from Cache instead of
// computing it inline. Old graphs have this absent; omitempty preserves
// byte-identical output for graphs written before PH8.
EmbeddingRef string `json:"embedding_ref,omitempty"`
// Pass 4 (graph algorithm) attributes. Pointers + omitempty so that
// documents written with --skip-pass=graph-algo stay byte-identical to
// the pre-PORT-4 schema.
CommunityID *int `json:"community_id,omitempty"`
Centrality *float64 `json:"centrality,omitempty"`
PageRank *float64 `json:"pagerank,omitempty"`
IsGodNode bool `json:"is_god_node,omitempty"`
IsSurpriseEndpoint bool `json:"is_surprise_endpoint,omitempty"`
IsArticulationPt bool `json:"is_articulation_point,omitempty"`
// Confidence overlay (Phase 1C, #2769). Value in [0.0, 1.0]; zero/unset
// reads as 1.0 (direct AST extraction). See internal/types/confidence.go
// for the universal taxonomy and propagation rules.
Confidence float64 `json:"confidence,omitempty"`
// contains filtered or unexported fields
}
Entity is a single node in the graph.
func EntityPtr ¶ added in v0.1.9
EntityPtr returns a pointer to a copy of e. Used by call sites that build an Entity value (e.g. via a chained WithProperties call) but need a *Entity, since taking the address of a method-call result is not legal Go.
func MaterializeEntity ¶ added in v0.1.9
MaterializeEntity decodes the i-th entity of the mmap'd Reader into a single heap-safe graph.Entity, on demand. It is a thin exported wrapper over the same fbEntityToGraphEntity conversion loadFBDocument uses per row, so a MaterializeEntity(r, i) result is byte-identical to the Document's Entities[i] for the same graph.fb.
Every string field is COPIED out of the mmap region (see fbEntityToGraphEntity), so the returned Entity is safe to retain after the Reader is closed — no borrow of the underlying mapping is held.
A fresh interner is used per call: interning only deduplicates repeated substrings WITHIN one materialized row, and the copied-out strings are independent of any Document-wide interner. Returns the zero Entity when r is nil or i is out of range.
func (Entity) MarshalJSON ¶ added in v0.1.9
MarshalJSON emits the same wire shape as the original map-backed Properties field (key "properties", omitted when empty).
func (*Entity) PropDelete ¶ added in v0.1.9
PropDelete removes key, if present. No-op if absent or unset.
func (Entity) PropLookup ¶ added in v0.1.9
PropLookup returns the value for key and whether it was present.
func (Entity) PropRange ¶ added in v0.1.9
PropRange calls f for every key/value pair in key-sorted order. Iteration stops early if f returns false. Safe to call on a zero-value Entity (no-op).
func (*Entity) PropSet ¶ added in v0.1.9
PropSet sets key to val, lazily initializing the backing storage.
func (Entity) PropsClone ¶ added in v0.2.0
PropsClone returns an independent copy of the properties as a types.Props, or nil if there are none. It is the map-free counterpart of PropsSnapshot: both graph's internal propKV and types.PropKV are the same sorted key/value pair shape, so this is one exact-sized slice allocation with no hmap header or bucket. Callers that only need keyed reads should prefer this over PropsSnapshot — at group-union scale the map header dominates (#5954).
func (*Entity) PropsReplace ¶ added in v0.1.9
PropsReplace replaces e's entire property set with props, mutating e in place. See WithProperties.
func (Entity) PropsSnapshot ¶ added in v0.1.9
PropsSnapshot returns an independent copy of the properties as a map, or nil if there are none. Callers must not assume the returned map aliases internal storage.
func (*Entity) UnmarshalJSON ¶ added in v0.1.9
UnmarshalJSON decodes the wire shape written by MarshalJSON.
func (Entity) WithProperties ¶ added in v0.1.9
WithProperties returns a copy of e with its properties replaced by props, converted into the sorted []propKV backing (a fresh copy — the input map is not aliased, unlike the old field-assignment semantics).
type EntityCoverageResult ¶
type EntityCoverageResult struct {
EntityID string `json:"entity_id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceFile string `json:"source_file"`
StartLine int `json:"start_line"`
Severity string `json:"severity"`
Tested bool `json:"tested"`
CoveringTests []string `json:"covering_tests"`
CoverageFraction float64 `json:"coverage_fraction"`
// State is the three-state coverage classification (#4662):
// "reach" (a test executes it), "contract-only" (an offline contract spec
// asserts its shape but nothing executes it), or "uncovered".
State string `json:"state"`
// ContractCovered is true when a contract spec references this entity's
// route (shape-asserted) regardless of reach state.
ContractCovered bool `json:"contract_covered"`
}
EntityCoverageResult is the single-entity output of ComputeEntityCoverage.
func ComputeEntityCoverage ¶
func ComputeEntityCoverage(doc *Document, entityID string) (*EntityCoverageResult, bool)
ComputeEntityCoverage returns coverage details for a single entity ID within doc. It applies the same two-phase algorithm as ComputeCoverage (real TESTS edges, then synthetic fallback via CALLS) but only for the requested entity, so it avoids iterating the entire graph for the output rendering step.
Returns (result, true) when the entity is found and is a production entity. Returns (nil, false) when the entity ID is not present in the document. Returns a result with Tested=false and empty CoveringTests when the entity exists but is a test entity or out-of-scope kind.
type EntityView ¶ added in v0.1.9
type EntityView interface {
ID() string
Kind() string
Name() string
QualifiedName() string
Subtype() string
SourceFile() string
Language() string
Signature() string
// The read-only property surface below is NAME- AND SIGNATURE-IDENTICAL to
// graph.Entity's own methods. That identity is the whole point of W2
// (ADR-0027): *graph.Entity satisfies these for free, so migrating a
// property-reading consumer to EntityView is a pure TYPE change — its
// PropGet/PropLookup/PropLen/PropRange/PropsSnapshot calls compile unchanged.
// The WRITE methods (PropSet/PropDelete) are deliberately EXCLUDED: writes are
// cut-set and stay on the concrete type. Storage-agnostic by contract
// (ADR-0027 §Interaction): compatible with the map-backed and []propKV
// backings and with values aliased straight out of the mmap.
//
// PropGet returns the value for key, or "" if absent.
PropGet(key string) string
// PropLookup returns the value for key and whether it was present.
PropLookup(key string) (string, bool)
// PropLen returns the number of properties.
PropLen() int
// PropRange calls f for every key/value pair in key-sorted order, stopping
// early if f returns false.
PropRange(f func(k, v string) bool)
// PropsSnapshot returns an independent copy of all properties as a map, or nil
// if there are none. The returned map (and its strings) are safe to retain
// past any borrow — an mmap-backed impl heap-copies the values.
PropsSnapshot() map[string]string
}
EntityView is the read-only view of a graph entity. See package doc above for the HOT/COLD rationale and why the concrete type is adapted via a wrapper.
func EntityViewOf ¶ added in v0.1.9
func EntityViewOf(e *Entity) EntityView
EntityViewOf wraps a concrete *Entity as an EntityView. Returns a nil interface for a nil entity so callers can nil-check the result.
type FileCoverage ¶
type FileCoverage struct {
File string `json:"file"`
Dir string `json:"dir"`
Total int `json:"total"`
Covered int `json:"covered"`
// ContractOnly is the count of entities in this file that are
// contract-covered-only (shape-asserted offline, not reach-covered) (#4662).
ContractOnly int `json:"contract_only"`
CoveragePct float64 `json:"coverage_pct"`
}
FileCoverage is per-file coverage statistics. File is the full source path (forward-slash normalised); Dir is its parent directory (matching the keys in ByDirectory) so the frontend can nest files under directories.
type FormatVersionError ¶ added in v0.1.9
type FormatVersionError struct {
// Found is the FormatVersion actually stamped on the rejected graph.fb.
Found int
// Required is the minimum FormatVersion this binary accepts
// (fbversion.Version at build time).
Required int
}
FormatVersionError is returned (wrapped, via %w) by loadFBDocument when an on-disk graph.fb was stamped with a FormatVersion older than this binary requires. It exists as a typed error — rather than forcing callers to string-match the human-readable message below — so a caller like internal/mcp's reload loop can detect "this specific repo's on-disk graph is version-incompatible" via errors.As and record durable state (e.g. a statusfile ReindexRequired flag), instead of only stashing the opaque error string and silently continuing (the "silent-green lie" this type exists to let callers fix).
func (*FormatVersionError) Error ¶ added in v0.1.9
func (e *FormatVersionError) Error() string
Error implements the error interface. The wording is part of the loader's user-facing contract (internal/graph/fbwriter/writer_test.go asserts on substrings of it verbatim) and must keep pointing the user at `grafel index <repo>`.
type GraphDescriptor ¶ added in v0.2.0
type GraphDescriptor struct {
Kind GraphKind
// Dir is the state dir this descriptor was resolved for.
Dir string
// Path is the single .fb file for GraphSingleFile (and, for GraphAbsent,
// the flat graph.fb path that does not exist — mirroring CurrentGraphPath's
// "return the flat path even when absent" contract). Empty for a segment-set.
Path string
// GenDir is the absolute graph.<gen>/ directory for GraphSegmentSet; empty
// otherwise.
GenDir string
// Manifest is the parsed, validated manifest for GraphSegmentSet; nil
// otherwise.
Manifest *Manifest
// Segments are the absolute seg-NNNN.fb paths (in manifest order) for
// GraphSegmentSet; nil otherwise. Handed straight to OpenSegmentsWithRanges.
Segments []string
}
GraphDescriptor is the resolved shape of a state dir's active graph. It is the segment-aware superset of CurrentGraphPath: where CurrentGraphPath only ever hands back a single .fb path (and is preserved unchanged for the ~11 #5891 single-file/legacy callers), CurrentGraphDescriptor additionally recognises the segment-set layout so a reader can route to it.
func CurrentGraphDescriptor ¶ added in v0.2.0
func CurrentGraphDescriptor(dir string) (GraphDescriptor, error)
CurrentGraphDescriptor resolves dir's active graph into a GraphDescriptor, classifying it as single-file, segment-set, or absent. Resolution order mirrors CurrentGraphPath, extended for the segment-set layout:
- `current` names graph.<gen>.fb (an existing file) → GraphSingleFile.
- `current` names graph.<gen> (a dir) or graph.<gen>/manifest.json → read + validate that dir's manifest → GraphSegmentSet. A malformed/hostile manifest returns a non-nil error (the reader MUST NOT proceed).
- Otherwise fall back to the legacy flat graph.fb: GraphSingleFile when it exists on disk, else GraphAbsent (Path still set to the flat path, for parity with CurrentGraphPath's absent semantics).
A missing pointer, a pointer to a missing gen file, or a hostile pointer all fall through to step 3 (never an error) — only a pointer that DOES resolve to a gen dir whose manifest is corrupt surfaces an error, so a genuinely broken segment-set is loud rather than silently mis-read as "absent".
type GraphKind ¶ added in v0.2.0
type GraphKind int
GraphKind classifies what the `current` pointer (or the legacy flat file) resolves to for a state dir.
const ( // GraphAbsent — no graph on disk (never-indexed repo): neither a resolvable // gen file/dir nor the legacy flat graph.fb exists. GraphAbsent GraphKind = iota // GraphSingleFile — the single-segment fast path: graph.<gen>.fb (or the // legacy flat graph.fb). Byte-identical to the pre-#5901 world; the // overwhelming common case. GraphSingleFile // GraphSegmentSet — the multi-segment layout: a graph.<gen>/ dir holding // seg-NNNN.fb files + manifest.json. GraphSegmentSet )
type GraphStatsSidecar ¶
type GraphStatsSidecar struct {
Version int `json:"version"`
ComputedAt time.Time `json:"computed_at"`
TotalFiles int `json:"total_files,omitempty"`
TotalEntities int `json:"total_entities"`
TotalRelationships int `json:"total_relationships"`
Communities int `json:"communities"`
Modularity float64 `json:"modularity"`
GodNodes int `json:"god_nodes"`
ArticulationPoints int `json:"articulation_points"`
// KindVocabularyVersion is types.KindVocabularyVersion as of the build
// that produced the accompanying graph (#6779) — the version of the
// entity-kind VOCABULARY the stored entities are spelled in.
//
// This sidecar is the only place it lives TODAY — not the only place it
// could: the graph.fb header or graph.json could carry it, at the cost of
// an .fbs slot (and an fbversion bump, which would auto-reindex every
// group — precisely what this mechanism exists to avoid doing behind the
// user's back). What is genuinely forced is that it must be stamped
// SOMEWHERE outside the entities: nothing in the graph's own rows records
// which vocabulary their kind strings belong to, so after a rename the old
// graph still loads, still validates, and still answers a query for the
// new kind with an empty result — indistinguishable from "there is nothing
// of that kind here".
//
// Zero / absent means the graph was written before this field existed and
// is therefore on an OLDER vocabulary — deliberately NOT "unknown, assume
// fine". omitempty is safe here because 0 is never a valid stamp:
// KindVocabularyVersion starts at 1.
KindVocabularyVersion int `json:"kind_vocabulary_version,omitempty"`
// RuntimeMS is the wall-clock duration of the graph-algorithm pass
// (Pass 4: Louvain / PageRank / articulation). Its meaning is unchanged
// since it was introduced; the extract/link phase timings below are
// tracked separately.
RuntimeMS int64 `json:"runtime_ms"`
// ExtractMS is the wall-clock duration (milliseconds) of the extraction
// phase for the index pass that produced this sidecar (#5692). Recorded so
// `grafel feedback` and future tooling can report where indexing time goes.
// A zero value means "unknown": the phase was not measured, or this sidecar
// was written before the field existed (back-compat).
//
// The measured span differs slightly by write path: on the full-index path
// it is scoped to the extraction pipeline (idx.Run) only; on the
// incremental reindex path it is the whole incremental pass wall-clock
// (manifest load + re-extract + scoped resolve + graph.fb write). Both are
// written in-band by the same goroutine that writes this sidecar, so the
// value is always consistent with the counts alongside it.
//
// It is written IN-BAND (by the reindex writer, sole owner of this file);
// the cross-repo link timing lives in a SEPARATE link-stats.json (see
// LinkStatsSidecar) so an unserialized link goroutine never races the
// reindex writer on this file's count fields (#5692).
ExtractMS int64 `json:"extract_ms,omitempty"`
// ParseErrorCanary is the A4 per-language parse-error-node canary report
// (#5414, epic #5359): per-language aggregate ERROR-node rates plus a
// baseline comparison and a spike flag. The shape is the JSON marshalling
// of treesitter.CanaryReport; kept as raw JSON here so the graph package
// does not depend on the treesitter package. Omitted when no parse stats
// were collected (e.g. a heuristic-only index).
ParseErrorCanary json.RawMessage `json:"parse_error_canary,omitempty"`
// ParseErrorSpike mirrors ParseErrorCanary.spiked at the top level so
// dashboards / crons can read the alarm without decoding the full report.
ParseErrorSpike bool `json:"parse_error_spike,omitempty"`
// RenameDetectTruncated reports that the rename-detection pass (#6087) hit
// its work budget and did NOT examine every candidate entity, so the
// RENAMED_FROM edges in the accompanying graph are a PARTIAL result.
//
// This exists because the stderr warning is invisible to every
// programmatic consumer: MCP, the dashboard and `grafel doctor` read the
// graph, not the indexer's log. A consumer that reports "no renames" off a
// truncated index is stating a confident wrong answer; this flag is how it
// can tell the difference. Absent/false means the pass ran to completion
// (or was skipped, or there was no prior graph to compare against — in all
// of which cases no rename data was dropped).
RenameDetectTruncated bool `json:"rename_detect_truncated,omitempty"`
// RenameDetectAddedSkipped is how many added entities the truncated pass
// never examined. Only meaningful when RenameDetectTruncated is true.
RenameDetectAddedSkipped int `json:"rename_detect_added_skipped,omitempty"`
// RenameDetectPairsSkipped is how many candidate pairs those skipped
// entities would have contributed. Only meaningful when
// RenameDetectTruncated is true.
RenameDetectPairsSkipped int `json:"rename_detect_pairs_skipped,omitempty"`
// UnsupportedExtensions counts the files this index pass SAW and dropped
// because no extractor claims their extension, keyed by lowercased
// extension (#6338).
//
// This is the one skip reason that means grafel silently indexed nothing:
// the file is not vendored, not ignored, not binary, not quarantined and
// did not error — no extractor was ever reached, so `doctor` had nothing
// to report and a 670-file VB.NET codebase looked exactly like a healthy
// index. Aggregated by extension, never per file: the originating report
// was 672 files.
//
// omitempty, and never written with zero-valued entries, so a repo with
// full extractor coverage carries no key at all and its consumers print
// nothing.
UnsupportedExtensions map[string]int `json:"unsupported_extensions,omitempty"`
// RelationshipKindsScanned records that the write path that produced this
// sidecar actually tallied the relationship kinds it serialized (#6757
// arm C). It is the difference between "counted, found none" and "never
// counted": with omitempty alone those two are the same bytes, and a
// consumer reading "no problem" off a graph nothing looked at is the
// #6534 failure exactly. A sidecar written before this field existed, or
// by a pass whose graph write FAILED, carries false.
//
// The three fields below are meaningless unless this is true.
RelationshipKindsScanned bool `json:"relationship_kinds_scanned,omitempty"`
// RelationshipEdgesKindNotInEnum is how many relationships this index
// WROTE whose kind is absent from BOTH relationship-kind vocabularies —
// types.AllRelationshipKinds() and, since #6773,
// types.AllDerivedRelationshipKinds(). Derived kinds are counted under
// RelationshipEdgesDerivedKind below instead, never here.
//
// The relationship vocabulary is enforced by nothing —
// types.IsValidRelationshipKind had zero non-test callers — so the enum
// described an intention, not a constraint. Only the write path can see
// the kinds that resolve at RUNTIME, and only if it says so: the same
// reasoning as RenameDetectTruncated above — the stderr line is invisible
// to MCP, the dashboard and `grafel doctor`, which read the graph.
//
// "Not in the enum" is NOT the same as "undeclared": an engine rule YAML
// can declare a kind the Go enum omits (DECORATES, in
// internal/engine/rules/python/frameworks/fastapi.yaml), and such a kind
// is counted here. The field names say enum because the enum is what was
// checked.
//
// Nothing is dropped and no index fails because of this. It is a
// measurement, taken so a later decision about declaring or rejecting
// these kinds rests on evidence.
RelationshipEdgesKindNotInEnum int `json:"relationship_edges_kind_not_in_enum,omitempty"`
// RelationshipDistinctKindsNotInEnum is the number of DISTINCT such
// kinds. Never capped, so it may exceed len(RelationshipKindsNotInEnum).
RelationshipDistinctKindsNotInEnum int `json:"relationship_distinct_kinds_not_in_enum,omitempty"`
// RelationshipKindsNotInEnum maps each distinct kind to the number of
// edges that carried it. A bare count would only say something is wrong;
// the names say what. Truncated to the busiest
// fbwriter.NonEnumKindListCap kinds so a pathological graph cannot bloat
// this file — the two counts above stay exact.
RelationshipKindsNotInEnum map[string]int `json:"relationship_kinds_not_in_enum,omitempty"`
// RelationshipEdgesDerivedKind is how many relationships this index wrote
// whose kind is in types.AllDerivedRelationshipKinds() — the DERIVED
// (statistical) vocabulary added by #6773.
//
// These are declared kinds, so they are NOT counted in
// RelationshipEdgesKindNotInEnum above. They get their own count because
// COMMIT_COUPLED alone was 27,407 of the 27,645 edges arm C reported as
// absent from the vocabulary — 99.1%. Declaring it removes it from that
// count, and if nothing else recorded it, the effect of the decision
// would be indistinguishable from deleting the measurement. Nothing about
// these edges is wrong: a derived edge is an inference from git history
// rather than a structural fact an extractor observed, and this is the
// number that says how much of the graph is that.
RelationshipEdgesDerivedKind int `json:"relationship_edges_derived_kind,omitempty"`
// RelationshipDistinctDerivedKinds is the number of DISTINCT such kinds.
// Never capped, so it may exceed len(RelationshipDerivedKinds).
RelationshipDistinctDerivedKinds int `json:"relationship_distinct_derived_kinds,omitempty"`
// RelationshipDerivedKinds maps each distinct derived kind to the number
// of edges that carried it, truncated to the busiest
// fbwriter.NonEnumKindListCap kinds while the two counts above stay exact.
RelationshipDerivedKinds map[string]int `json:"relationship_derived_kinds,omitempty"`
// EntityKindsScanned records that the write path that produced this
// sidecar actually tallied the ENTITY kinds it serialized (#6776 arm A).
// Same job as RelationshipKindsScanned above and for the same reason:
// with omitempty alone, "counted, found none" and "never counted" are the
// same bytes, and reporting a clean vocabulary off a graph nothing looked
// at is the #6534 failure exactly. A sidecar written before this field
// existed, or by a pass whose graph write FAILED, carries false.
//
// The three fields below are meaningless unless this is true.
EntityKindsScanned bool `json:"entity_kinds_scanned,omitempty"`
// EntitiesKindNotInEnum is how many entities this index WROTE whose kind
// is absent from types.AllEntityKinds().
//
// The entity vocabulary is not enforced on the rule path at all:
// internal/engine/detector.go copies SourcePattern.EntityType straight
// into types.EntityRecord.Kind, so types.IsValidEntityKind is never
// consulted and a rule YAML can mint any string it likes. #6744 ledgered
// 532 declaration sites over 25 invalid values; this field is the other
// half of that picture, because a declaration site that produces no
// entity is an edit that buys nothing (#6776 arm A).
//
// "Not in the enum" is NOT the same as "undeclared": `Route` is declared
// by rule YAML and counted here, because the enum is what was checked.
//
// Nothing is dropped and no index fails because of this. It is a
// measurement, taken so the migration is ranked on evidence.
EntitiesKindNotInEnum int `json:"entity_entities_kind_not_in_enum,omitempty"`
// EntityDistinctKindsNotInEnum is the number of DISTINCT such kinds.
// Never capped, so it may exceed len(EntityKindsNotInEnum).
EntityDistinctKindsNotInEnum int `json:"entity_distinct_kinds_not_in_enum,omitempty"`
// EntityKindsNotInEnum maps each distinct kind to the number of entities
// that carried it. A bare count would only say something is wrong; the
// names say what, and the names are what a migration is ranked by.
// Truncated to the busiest fbwriter.NonEnumKindListCap kinds so a
// pathological graph cannot bloat this file — the two counts above stay
// exact.
EntityKindsNotInEnum map[string]int `json:"entity_kinds_not_in_enum,omitempty"`
}
GraphStatsSidecar is the corpus-level summary written to <repo>/.grafel/graph-stats.json. Consumed by `grafel doctor` and the future MCP `graph_stats` tool.
func LoadSidecar ¶ added in v0.1.8
func LoadSidecar(stateDir string) (*GraphStatsSidecar, error)
LoadSidecar reads and decodes the graph-stats.json sidecar in stateDir. Returns an error if the sidecar is absent or malformed.
type GraphStream ¶ added in v0.2.1
type GraphStream struct {
// contains filtered or unexported fields
}
GraphStream is a re-iterable, non-materialising read over a persisted graph.
It holds the mmap open for its lifetime, so Close MUST be called. Every string handed to a visitor is COPIED out of the mapping (the shared converters do that), so records outlive the stream safely.
The zero value is not usable; construct with OpenGraphStream.
func OpenGraphStream ¶ added in v0.2.1
func OpenGraphStream(dir string) (*GraphStream, error)
OpenGraphStream opens dir's active graph for streaming.
Resolution mirrors LoadGraphFromDir exactly — segment-set, then single-file graph.fb, then graph.json — so a dir that loads today streams today, and a dir that fails to load still fails (the incremental path depends on that error to fall back to a full reindex rather than persisting a truncated graph).
func (*GraphStream) Close ¶ added in v0.2.1
func (s *GraphStream) Close() error
Close releases the mmap. Safe on a nil receiver and safe to call twice.
func (*GraphStream) DocStats ¶ added in v0.2.1
func (s *GraphStream) DocStats() Stats
DocStats returns the Stats block LoadGraphFromDir would have put on the Document, without materialising it.
Read the Files field carefully: the .fb formats do NOT encode a file count, so materializeGraphView leaves Stats.Files zero for every .fb-backed graph and only the graph.json fallback carries a real value. This method reproduces that exactly rather than papering over it — a caller that wants the true indexed-file count should read graph-stats.json's TotalFiles instead.
Treat a zero Files here as UNKNOWN, never as "this repo has no files": it is the value the .fb layouts are structurally incapable of carrying, not a measurement. `grafel status` assigned it over a real sidecar count and printed "0 files" for every repo in the shipping format (#6115); it now takes this field only when it is non-zero.
func (*GraphStream) EachEntity ¶ added in v0.2.1
func (s *GraphStream) EachEntity(visit func(Entity) bool)
EachEntity decodes and visits every entity row in file order. Return false from visit to stop early; a later walk still starts from the beginning.
The decoded Entity is byte-identical to the one LoadGraphFromDir would have placed at the same index — same converter, same interner discipline.
func (*GraphStream) EachEntityKind ¶ added in v0.2.1
func (s *GraphStream) EachEntityKind(visit func(kind string) bool)
EachEntityKind visits the Kind field of every entity row, decoding NOTHING else. Return false from visit to stop early.
This exists for the kind-tally callers (`grafel status` counts http endpoints and process flows, #5995): EachEntity converts a whole row — name, qualified name, signature, source file, properties — of which a tally reads one string. The kind strings are interned by the same interner the full walk uses, so a tally over a corpus-sized graph allocates a bounded handful of strings rather than one object graph per row.
func (*GraphStream) EachRelationship ¶ added in v0.2.1
func (s *GraphStream) EachRelationship(visit func(Relationship) bool)
EachRelationship decodes and visits every relationship row in file order. Return false from visit to stop early.
func (*GraphStream) EntityCount ¶ added in v0.2.1
func (s *GraphStream) EntityCount() int
EntityCount reports the number of entity rows the stream will visit.
func (*GraphStream) Header ¶ added in v0.2.1
func (s *GraphStream) Header() fbreader.GraphMeta
Header returns the graph's header fields without decoding a single row.
It is the streaming equivalent of reading Document.IndexedRef / IndexedSHA / IsWorktree / Repo / CoverageStatus off a LoadGraphFromDir result: the same values from the same place (the Graph root table), for callers that want the header and NOT the hundreds of megabytes of rows behind it (#5995).
On the graph.json fallback the fields are reconstituted from the already- materialised Document so both paths answer identically.
func (*GraphStream) RelationshipCount ¶ added in v0.2.1
func (s *GraphStream) RelationshipCount() int
RelationshipCount reports the number of relationship rows the stream will visit.
type ImpactedCommunity ¶
type ImpactedCommunity struct {
CommunityID int `json:"community_id"`
ChangedCount int `json:"changed_count"` // changed entities in this community
BlastRadiusHit int `json:"blast_radius_hit"` // downstream entities in this community
// InferredChangedCount is how many of ChangedCount were placed here by
// INFERENCE rather than by the overlay (#6042).
InferredChangedCount int `json:"inferred_changed_count,omitempty"`
// InferredOnly is true when this community appears in the impact set SOLELY
// because of inferred placements: no overlay-placed changed entity and no
// blast-radius entity puts it here. Such a community is a deduction, and any
// merge-risk overlap resting on it is a deduced conflict, not a measured one.
InferredOnly bool `json:"inferred_only,omitempty"`
}
ImpactedCommunity is a community touched by the change, with how many changed entities and how many blast-radius entities fall inside it.
type ImportCycle ¶
type ImportCycle struct {
// Members lists entity IDs that form the cycle, sorted for determinism.
Members []string `json:"members"`
// Edges lists the IMPORTS relationships (FromID → ToID) inside the cycle,
// sorted for determinism.
Edges []CycleEdge `json:"edges"`
// WeakestLinkFromID / WeakestLinkToID identify the edge whose source has
// the lowest PageRank — the best edge to sever to break the cycle cheaply.
WeakestLinkFromID string `json:"weakest_link_from_id"`
WeakestLinkToID string `json:"weakest_link_to_id"`
// SuggestedExtractionID is the entity inside the cycle with the highest
// PageRank — the best candidate for extraction into a common module.
SuggestedExtractionID string `json:"suggested_extraction_id"`
// Size is len(Members) for fast filtering.
Size int `json:"size"`
}
ImportCycle describes one strongly-connected component of size > 1 in the IMPORTS sub-graph.
func FindImportCycles ¶
func FindImportCycles(entities []Entity, rels []Relationship, pagerank map[string]float64) []ImportCycle
FindImportCycles runs Tarjan SCC on the IMPORTS sub-graph of rels, using pagerank scores to annotate each cycle with a weakest link and suggested extraction target.
pagerank may be nil or empty — in that case all nodes are treated as equally weighted (weakest-link = first alphabetical source).
Returns cycles sorted by descending size, then ascending first member ID.
type KeyEntity ¶
type KeyEntity struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceFile string `json:"source_file,omitempty"`
CommunityID int `json:"community_id"`
Degree int `json:"degree"` // in+out degree (cheap centrality)
Betweenness float64 `json:"betweenness"` // precomputed approx. betweenness
PageRank float64 `json:"pagerank"` // precomputed pagerank
Score float64 `json:"score"` // composite ranking score
Reason string `json:"reason"` // why this entity is a key entity
}
KeyEntity is a structural hub/bridge in the graph.
type KindVocabularyState ¶ added in v0.3.2
type KindVocabularyState string
KindVocabularyState is the answer to "what vocabulary does the graph stored in this state dir speak?".
It has THREE values on purpose. "This graph is current" and "there is no graph here at all" are different facts about a repo, and a check that collapses them re-creates the very failure #6779 exists to fix: a report that cannot distinguish a healthy answer from an absent one. (The same collapse #6757 arm C had to undo between `Scanned` and `Clean`.)
const ( // KindVocabularyNoGraph — this state dir holds no graph. Nothing has been // indexed here (or a `reset` removed it), so there is no vocabulary to be // current or stale, and telling the user to reindex would be advice about // a graph that does not exist. KindVocabularyNoGraph KindVocabularyState = "no-graph" // KindVocabularyCurrent — a graph is stored here and it was written under // this build's kind vocabulary (or a newer one). Its kind strings mean // what this binary thinks they mean. KindVocabularyCurrent KindVocabularyState = "current" // KindVocabularyOlder — a graph is stored here and it was written under an // OLDER kind vocabulary. Its entities may still be spelled with kinds this // build has renamed or retired, so queries filtering on the new spellings // can silently return nothing. The fix is a reindex, which the USER // chooses to run: nothing in grafel migrates or reindexes on this signal. KindVocabularyOlder KindVocabularyState = "older" )
func KindVocabularyStateForDir ¶ added in v0.3.2
func KindVocabularyStateForDir(dir string) (state KindVocabularyState, stored int)
KindVocabularyStateForDir reports which entity-kind vocabulary the graph stored in dir (a repo's .grafel state directory) was written under, together with the version actually stamped on disk (0 when the graph predates the stamp, and meaningless when the state is KindVocabularyNoGraph).
The two inputs are read INDEPENDENTLY, and that independence is the whole design:
- whether a graph EXISTS is answered by the same resolution the loader uses (CurrentGraphDescriptor for graph.fb / segment sets, plus the graph.json fallback), never by the sidecar. A stale graph-stats.json left behind by a removed graph must not be reported as a stale- vocabulary graph.
- which VOCABULARY it speaks is answered by the sidecar stamp, never by the graph, because the graph does not record it and cannot.
Deriving both from one signal is what would collapse the three states into two. It is cheap enough to call on every doctor run: a stat plus a small JSON read, with no entity materialization.
type LinkStatsSidecar ¶ added in v0.1.8
type LinkStatsSidecar struct {
Version int `json:"version"`
ComputedAt time.Time `json:"computed_at"`
// LinkMS is the wall-clock duration (milliseconds) of the most recent
// cross-repo link pass for the group this repo belongs to. Zero (or an
// absent file) means "unknown".
LinkMS int64 `json:"link_ms"`
}
LinkStatsSidecar is the per-repo cross-repo-link timing sidecar written to <repo-state>/link-stats.json (#5692). It is kept SEPARATE from graph-stats.json on purpose: the cross-repo link pass runs on its own per-group goroutine (scheduler AfterFunc) which is NOT serialized against the reindex worker pool. Were link timing stamped into graph-stats.json, a read-modify-write from the link goroutine could land between a reindex's ReadFile and Rename and clobber the freshly written entity/relationship counts. Giving the link pass its own file makes it the SOLE writer here, so there is no cross-writer lost-update: link passes for a single group are themselves serialized by the scheduler's per-group debounce.
func LoadLinkStats ¶ added in v0.1.8
func LoadLinkStats(stateDir string) (*LinkStatsSidecar, error)
LoadLinkStats reads and decodes the link-stats.json sidecar in stateDir. Returns an error if the file is absent (os.IsNotExist) or malformed; callers treating absence as "link timing unknown" should check IsNotExist.
type Manifest ¶ added in v0.2.0
type Manifest struct {
// FormatVersion is the manifest schema version (ManifestFormatVersion).
FormatVersion int `json:"format_version"`
// Segments lists every segment file in the gen dir, in read/open order.
Segments []SegmentMeta `json:"segments"`
}
Manifest is the parsed graph.<gen>/manifest.json.
func ReadManifest ¶ added in v0.2.0
ReadManifest reads and validates graph.<gen>/manifest.json from genDir. A missing, unreadable, malformed, or hostile manifest yields a non-nil error and a nil manifest — callers MUST NOT open any segment on error.
func (*Manifest) EntitySegments ¶ added in v0.2.0
func (m *Manifest) EntitySegments() []SegmentMeta
EntitySegments returns the subset of segments that carry entities (EntityCount>0), preserving manifest order. Lets a caller enumerate the entity stream independently of the relationship stream (decision 5).
func (*Manifest) RelationshipSegments ¶ added in v0.2.0
func (m *Manifest) RelationshipSegments() []SegmentMeta
RelationshipSegments returns the subset of segments that carry relationships (RelCount>0), preserving manifest order.
func (*Manifest) TotalEntityCount ¶ added in v0.2.0
TotalEntityCount / TotalRelationshipCount sum the per-segment counts. Cheap header-level totals that avoid opening any segment file.
func (*Manifest) TotalRelationshipCount ¶ added in v0.2.0
type MergeRiskPair ¶
type MergeRiskPair struct {
RefA string `json:"ref_a"`
RefB string `json:"ref_b"`
// touches ONLY through inference (#6042 D1). The overlap on such a community
// was not observed — it was deduced on one or both sides.
InferredSharedCommunities []int `json:"inferred_shared_communities,omitempty"`
// InferredOnly is true when EVERY shared community is in the list above, i.e.
// this reported conflict is entirely a product of inference.
//
// The verdict-level CommunityDataInferredOnly cannot express this: a pair of
// refs can each have measured communities (so the verdict looks measured) and
// still overlap ONLY on an inferred one, making the reported conflict
// manufactured while every aggregate flag reads "measured". That is the #6006
// defect class at pair granularity, which is the granularity a merge decision
// is actually made at.
InferredOnly bool `json:"inferred_only"`
}
MergeRiskPair is two refs whose impacted-community sets overlap.
type MergeRiskResult ¶
type MergeRiskResult struct {
Pairs []MergeRiskPair `json:"risk_pairs"`
RefCount int `json:"ref_count"`
RiskyPairs int `json:"risky_pair_count"`
// CommunityDataAvailable is false when ANY input ref lacked community data
// (#6006). A zero risky_pair_count is only a "these refs do not conflict"
// answer when this is true; when it is false the analysis did not run and the
// empty pair list carries no safety information whatsoever.
//
// Deliberately conservative: one uncovered ref taints the whole result,
// because merge risk is a property of the pair set, not of a single ref.
CommunityDataAvailable bool `json:"community_data_available"`
// RefsWithoutCommunityData names the refs that had no community data, sorted.
RefsWithoutCommunityData []string `json:"refs_without_community_data,omitempty"`
// InferredEntityCount totals the changed entities across all refs that were
// placed by INFERENCE rather than by the group-algo overlay.
InferredEntityCount int `json:"inferred_entity_count"`
// CommunityDataInferredOnly is true when the analysis ran but NO ref
// contributed a single overlay-measured entity — the add-only PR shape #6042
// exists for. The pairs below are then a reasoned guess at what Louvain would
// have said, not a reading of what it did say. False when the data was
// unavailable altogether: that is a decline, not a low-confidence answer.
CommunityDataInferredOnly bool `json:"community_data_inferred_only"`
// RefsWithInferredCommunityData names the refs that contributed at least one
// inferred placement, sorted.
RefsWithInferredCommunityData []string `json:"refs_with_inferred_community_data,omitempty"`
// InferredOnlyPairCount is how many of the reported risk pairs overlap SOLELY
// on communities that at least one side reached by inference. Non-zero means
// at least one reported conflict is deduced rather than observed — even when
// CommunityDataInferredOnly is false, which it will be whenever the refs also
// touch measured communities that happen not to overlap.
InferredOnlyPairCount int `json:"inferred_only_pair_count"`
}
MergeRiskResult is the ranked triage output of AnalyzeMergeRisk.
func AnalyzeMergeRisk ¶
func AnalyzeMergeRisk(impacts []ChangeImpact) MergeRiskResult
AnalyzeMergeRisk intersects every change's impacted-community set pairwise and returns the pairs that overlap, ranked by shared-community count descending. Refs with disjoint community sets are safe to merge in any order and are omitted from the result.
Determinism: input refs are sorted by label; pairs are emitted with RefA<RefB and ranked by (sharedCount desc, RefA asc, RefB asc).
type ModuleAlgorithmResults ¶
type ModuleAlgorithmResults struct {
// ModuleIDs is the deterministic sorted list of all module IDs present
// in the aggregated graph (including singletons with no inbound/outbound
// edges, when they're present in the input as Module entities).
ModuleIDs []string `json:"module_ids"`
// ModuleNames maps module ID → human-readable name. Always populated.
ModuleNames map[string]string `json:"module_names"`
// Edges is the full set of aggregated module→module edges, sorted.
Edges []ModuleEdge `json:"edges"`
// SCCs are the strongly-connected components of size >= 2.
SCCs []ModuleSCC `json:"sccs"`
// SCCOf maps module ID → SCC ID; -1 when the module is not in any
// non-trivial SCC. Always populated for every module ID.
SCCOf map[string]int `json:"scc_of"`
// Centrality is per-module PageRank + betweenness, sorted by ID.
Centrality []ModuleCentrality `json:"centrality"`
// Stats are corpus-level summaries.
Stats ModuleAlgorithmStats `json:"stats"`
}
ModuleAlgorithmResults bundles every output of RunModuleAlgorithms.
func RunModuleAlgorithms ¶
func RunModuleAlgorithms(entities []Entity, rels []Relationship) *ModuleAlgorithmResults
RunModuleAlgorithms is the top-level entry point. It builds the aggregated module graph from (entities, rels) and runs SCC + PageRank + betweenness over it.
Inputs:
- entities: the full Document.Entities slice. Both Module container entities (Kind="Module") and regular entities (carrying Properties["module"]) are accepted; the function reconciles them.
- rels: the full Document.Relationships slice. CONTAINS, STEP_IN_PROCESS and ENTRY_POINT_OF edges are dropped (they are scaffolding, not real dependencies). DEPENDS_ON edges BETWEEN two Module nodes are taken as pre-aggregated module-level edges (with their "weight" property when present); all other edges are aggregated by collapsing their endpoints onto their respective modules.
Returns a populated *ModuleAlgorithmResults. Never returns nil — an empty graph produces a zero-stats result with empty slices.
type ModuleAlgorithmStats ¶
type ModuleAlgorithmStats struct {
NumModules int `json:"num_modules"`
NumModuleEdges int `json:"num_module_edges"`
NumSCCs int `json:"num_sccs"`
LargestSCCSize int `json:"largest_scc_size"`
NumModulesInCycle int `json:"num_modules_in_cycle"`
}
ModuleAlgorithmStats are the corpus-level summary numbers.
type ModuleCentrality ¶
type ModuleCentrality struct {
ModuleID string `json:"module_id"`
ModuleName string `json:"module_name"`
PageRank float64 `json:"pagerank"`
Betweenness float64 `json:"betweenness"`
// InDegree / OutDegree are the unweighted node-degrees in the module
// graph — cheap sanity-check metrics that often correlate with PageRank
// but with very different units, useful for hub vs. fan-out distinction.
InDegree int `json:"in_degree"`
OutDegree int `json:"out_degree"`
}
ModuleCentrality is one module's score pair.
func TopByBetweenness ¶
func TopByBetweenness(cents []ModuleCentrality, n int) []ModuleCentrality
TopByBetweenness returns the top-N centrality entries by betweenness, ties broken by ascending module ID. n<=0 returns the full list.
func TopByPageRank ¶
func TopByPageRank(cents []ModuleCentrality, n int) []ModuleCentrality
TopByPageRank returns the top-N centrality entries by PageRank, ties broken by ascending module ID. n<=0 returns the full list.
type ModuleCoverage ¶
type ModuleCoverage struct {
Module string `json:"module"`
Total int `json:"total"`
Covered int `json:"covered"`
CoveragePct float64 `json:"coverage_pct"`
}
ModuleCoverage is per-module coverage statistics.
type ModuleEdge ¶
type ModuleEdge struct {
FromModule string `json:"from_module"`
ToModule string `json:"to_module"`
// Weight is the count of underlying entity-edges that collapsed onto
// this module-pair (or, when DEPENDS_ON edges already carry a weight
// property, that pre-baked count).
Weight int `json:"weight"`
}
ModuleEdge is a directed aggregated edge between two module IDs.
type ModuleSCC ¶
type ModuleSCC struct {
// ID is a deterministic integer assigned to the SCC for cross-reference.
// Stable across runs of the same input (Tarjan-discovery order over the
// sorted node set).
ID int `json:"id"`
// Members lists module IDs participating in the cycle, sorted.
Members []string `json:"members"`
// MemberNames mirrors Members but carries the human-readable module name
// (Module entity.Name when available; falls back to the ID).
MemberNames []string `json:"member_names"`
// Edges are the directed module→module edges INTERNAL to the SCC, sorted.
Edges []ModuleEdge `json:"edges"`
// Size is len(Members), for fast filtering.
Size int `json:"size"`
}
ModuleSCC is one strongly-connected component of size >= 2 in the aggregated module graph.
type NPlusOneFinding ¶
type NPlusOneFinding struct {
// CallerEntityID is the Function/Method entity that contains the loop.
CallerEntityID string `json:"caller_entity_id"`
// CallerName is the human-readable name of that entity.
CallerName string `json:"caller_name"`
// CallerFile is the source file of the caller entity.
CallerFile string `json:"caller_file"`
// CallerStartLine is the start line of the enclosing function.
CallerStartLine int `json:"caller_start_line"`
// QueryEntityID is the entity ID of the ORM query call site.
QueryEntityID string `json:"query_entity_id"`
// QueryName is the name of the query call (e.g. "User.objects.get").
QueryName string `json:"query_name"`
// QueryFile is the source file of the query call site.
QueryFile string `json:"query_file"`
// QueryLine is the line of the query call site.
QueryLine int `json:"query_line"`
// ORM is the ORM framework (e.g. "django", "sqlalchemy", "activerecord").
ORM string `json:"orm"`
// Language is the programming language.
Language string `json:"language"`
// LoopEntityID is the entity ID of the intermediate loop entity, if any.
// Empty when the loop context was inferred from Properties["loop_context"].
LoopEntityID string `json:"loop_entity_id,omitempty"`
// LoopSubtype is the loop kind ("for_loop", "while_loop", etc.).
LoopSubtype string `json:"loop_subtype,omitempty"`
// Suggestion is the recommended remediation.
Suggestion string `json:"suggestion"`
}
NPlusOneFinding describes a single N+1 query instance.
type NPlusOneReport ¶
type NPlusOneReport struct {
// Findings is the list of detected N+1 sites, sorted by file+line.
Findings []NPlusOneFinding `json:"findings"`
// EntitiesScanned is the number of entities examined.
EntitiesScanned int `json:"entities_scanned"`
// RelationshipsScanned is the number of relationships examined.
RelationshipsScanned int `json:"relationships_scanned"`
}
NPlusOneReport is the complete output of DetectNPlusOne.
func DetectNPlusOne ¶
func DetectNPlusOne(doc *Document) *NPlusOneReport
DetectNPlusOne scans doc for N+1 query anti-patterns and returns a report. It runs in O(E + R) time where E = entity count and R = relationship count.
type OrientQuestion ¶
type OrientQuestion struct {
Question string `json:"question"`
Source string `json:"source"` // ambiguous_edge | bridge_node | isolated_node
Entities []string `json:"entities,omitempty"`
}
OrientQuestion is a templated orientation question with provenance.
type OrientationOptions ¶
type OrientationOptions struct {
TopEntities int // max KeyEntities returned (default 15)
TopEdges int // max CrossCutEdges returned (default 15)
MaxQuestions int // max OrientQuestions returned (default 12)
}
OrientationOptions bounds the analysis output.
func DefaultOrientationOptions ¶
func DefaultOrientationOptions() OrientationOptions
DefaultOrientationOptions returns the production caps.
type OrientationResult ¶
type OrientationResult struct {
KeyEntities []KeyEntity `json:"key_entities"`
CrossCutEdges []CrossCutEdge `json:"cross_cutting_edges"`
Questions []OrientQuestion `json:"orientation_questions"`
}
OrientationResult bundles the three analysis parts.
func AnalyzeOrientation ¶
func AnalyzeOrientation(entities []Entity, rels []Relationship, opts OrientationOptions) OrientationResult
AnalyzeOrientation computes the three-part orientation view over a single graph document's entities and relationships. It reads the precomputed Pass-4 attributes (Centrality/PageRank/CommunityID) and computes only cheap O(V+E) quantities (degree, boundary checks) inline.
type PRImpactOptions ¶
type PRImpactOptions struct {
Hops int // downstream BFS depth (default 3, clamped [1,6])
MaxBlastNodes int // cap on blast_radius entries returned (default 500)
}
PRImpactOptions bounds the analysis.
func DefaultPRImpactOptions ¶
func DefaultPRImpactOptions() PRImpactOptions
DefaultPRImpactOptions returns the production caps.
type PRImpactResult ¶
type PRImpactResult struct {
ChangedEntities []ChangedEntity `json:"changed_entities"`
ImpactedCommunities []ImpactedCommunity `json:"impacted_communities"`
BlastRadius []BlastEntity `json:"blast_radius"`
// Aggregate counts for quick triage.
ChangedCount int `json:"changed_count"`
CommunityCount int `json:"impacted_community_count"`
BlastRadiusCount int `json:"blast_radius_count"`
Truncated bool `json:"truncated,omitempty"`
// CommunityDataAvailable is the validity flag for every community-derived
// field in this struct (ImpactedCommunities, CommunityCount,
// ImpactedCommunityIDs, and therefore the whole merge-risk analysis built on
// top of them). Issue #6006.
//
// It exists because the per-repo Pass-4 algorithm pass was removed when the
// group-scope algo pass replaced it: entities loaded straight out of graph.fb
// carry the -2/nil sentinel, so every community-derived output collapses to
// empty. Without this flag "no impacted communities" is indistinguishable
// from "communities were never computed" — and on a merge-risk question that
// reads as "safe to merge", which is the worst direction to be wrong in.
//
// MEASURED OVER THE CHANGED SET, NOT THE ENTITY SET. The first cut of this
// flag asked "does any entity anywhere in the graph carry a community?", and
// that is the wrong question: the group-algo overlay is computed from the
// INDEXED GROUP UNION, so entities that exist only on a feature ref — every
// entity a PR ADDS — are absent from it by construction. A pure add-only PR
// therefore has a fully-covered repo and a completely uncovered change set,
// and the graph-wide flag reported `community_data_available: true` next to
// `risky_pair_count: 0` — an affirmative all-clear on a change nothing was
// known about. Coverage of the entities we are actually reasoning about is
// the only coverage that makes the answer valid.
//
// An EMPTY change set is vacuously available: nothing changed, so nothing can
// conflict, and that is a real answer rather than a missing one. (This is the
// live default-base case — conflicts mode diffs refs[0] against itself.)
CommunityDataAvailable bool `json:"community_data_available"`
// ChangedWithoutCommunity counts changed entities carrying no community, i.e.
// the entities this analysis could not place — neither by the overlay nor by
// inference. Non-zero with CommunityDataAvailable=true is PARTIAL coverage:
// the verdict stands on the entities that were placed, but this many were
// invisible to it.
ChangedWithoutCommunity int `json:"changed_entities_without_community"`
// ChangedWithOverlayCommunity counts changed entities the group-algo overlay
// placed directly. These are MEASURED.
ChangedWithOverlayCommunity int `json:"changed_entities_with_overlay_community"`
// ChangedWithInferredCommunity counts changed entities placed by inference
// from their placed neighbours (pr_impact_infer.go). These are DEDUCED — good
// enough to triage with, not good enough to present as measured.
ChangedWithInferredCommunity int `json:"changed_entities_with_inferred_community"`
// CommunityDataInferredOnly is the verdict-level confidence marker: the
// analysis ran, but EVERY placement behind it was inferred. An agent should
// weigh such a verdict differently from one the overlay measured. False when
// nothing was placed at all — that case is a decline
// (CommunityDataAvailable=false), not a low-confidence answer.
CommunityDataInferredOnly bool `json:"community_data_inferred_only"`
}
PRImpactResult is the structured output of AnalyzePRImpact.
func AnalyzePRImpact ¶
func AnalyzePRImpact(entities []Entity, rels []Relationship, change ChangeSet, opts PRImpactOptions) PRImpactResult
AnalyzePRImpact computes the single-change impact view: changed entities -> impacted communities -> downstream blast radius.
`entities`/`rels` are the HEAD graph (added & modified entities live here; removed entities won't, which is fine — they have no downstream in HEAD). `change` is the diff-derived change set. The function:
- annotates each changed entity with its community,
- walks the INBOUND graph from the changed seeds (callers-of-callers, bounded by opts.Hops) to find every downstream dependent, and
- rolls up per-community changed/blast counts.
func (PRImpactResult) ImpactedCommunityIDs ¶
func (r PRImpactResult) ImpactedCommunityIDs() []int
ImpactedCommunityIDs returns the sorted set of (grouped) community ids a PRImpactResult touches — used as the merge-risk overlap key. Ungrouped (-1) is excluded: "everything not in a community" is not a meaningful conflict signal, and including it would make every unrelated pair appear to overlap.
func (PRImpactResult) InferredOnlyCommunityIDs ¶ added in v0.2.0
func (r PRImpactResult) InferredOnlyCommunityIDs() []int
InferredOnlyCommunityIDs returns the sorted subset of ImpactedCommunityIDs whose presence rests ENTIRELY on inferred placements (#6042 D1).
This is what makes per-PAIR provenance possible. CommunityDataInferredOnly is a whole-verdict flag and says nothing about an individual overlap: a ref can have two measured communities and one inferred one, and if the inferred one is the ONLY community it shares with another ref, the reported conflict is entirely manufactured while the verdict-level flag reads false.
type PersistedStats ¶ added in v0.1.4
type PersistedStats struct {
Entities int
Relationships int
// ComputedAt is the index timestamp stamped into the graph.fb header
// (the same value the sidecar's computed_at would carry). Zero when the
// header carries no/unparseable timestamp.
ComputedAt time.Time
// IndexedRef is the git ref name (branch/tag) at index time, read
// directly off the graph.fb header (#5729-W1 status plane). Empty for
// legacy graphs written before Phase 0 git metadata (#2088) or a
// detached HEAD / non-git repo.
IndexedRef string
// IndexedSHA is the abbreviated (short) HEAD commit hash at index time,
// read directly off the graph.fb header. Empty under the same
// conditions as IndexedRef.
IndexedSHA string
}
PersistedStats is a cheap, on-disk view of a repo's index size and freshness, read from the graph.fb header WITHOUT materializing the entity/relationship vectors. Used by the dashboard group overview and `grafel status` as a fallback when the graph-stats.json sidecar is absent (e.g. a graph written by the daemon's incremental reindex path, which does not emit the sidecar) so a cold-but-indexed group reports its real counts instead of "0 entities / never indexed".
func PersistedStatsFromDir ¶ added in v0.1.4
func PersistedStatsFromDir(dir string) (PersistedStats, bool)
PersistedStatsFromDir reads cheap persisted stats from <dir>/graph.fb.
It mmaps the file and reads the entity/relationship vector lengths and the header timestamp — it does NOT decode any entity or relationship — then closes the mapping immediately. This is the inexpensive way to learn a cold group's true size without paying for a full LoadGraphFromDir.
Returns ok=false when graph.fb is absent or cannot be opened (in which case callers should treat the repo as genuinely never-indexed via graph.fb).
type Relationship ¶
type Relationship struct {
ID string `json:"id"`
FromID string `json:"from_id"`
ToID string `json:"to_id"`
Kind string `json:"kind"`
// Confidence overlay (Phase 1C, #2769). Value in [0.0, 1.0]; zero reads
// as 1.0. See internal/types/confidence.go.
Confidence float64 `json:"confidence,omitempty"`
// contains filtered or unexported fields
}
Relationship is a directed edge between entities.
func DropHierarchySelfLoops ¶ added in v0.3.2
func DropHierarchySelfLoops(rels []Relationship) ([]Relationship, int)
DropHierarchySelfLoops removes every relationship whose two endpoints resolved to the SAME entity id and whose kind asserts a type-hierarchy relation. It returns the filtered slice (filtered in place; the input must not be reused) and the number of rows dropped.
Issue #6805. The self-loop this closes is NOT produced by a single bad rule; it is produced by the RESOLVER collapsing two differently-typed stubs onto one entity. The Kotlin Multiplatform rule pack (internal/engine/rules/kotlin/frameworks/kmp.yaml) emits `Implementation:<name> IMPLEMENTS Interface:<name>` for every `actual fun` / `actual class`. When the paired `expect` declaration IS in the indexed set, those are two distinct entities and the edge is correct — that is the expect/actual pairing, and it is the reason those rules are kept rather than deleted. When it is NOT — the commonest case being a member function of an `expect class`, which carries no `expect` keyword of its own, so `actual fun nowMillis` inside `actual class Clock` has no `Interface` counterpart to bind to — the unmatched `Interface:<name>` stub falls through to the resolver's bare-name lookup and binds back to the `Implementation:<name>` entity. Both endpoints then carry one id.
A self-loop is worse than a missing edge for a graph consumer: "what implements Clock.nowMillis?" answers "Clock.nowMillis", a confident wrong answer rather than an absence. Downstream ANALYSIS already discards self-loops (orientation.go, module_gds.go, algorithms.go, pr_impact.go), but the persisted document is what the MCP expand/traces surface reads, so the drop has to happen before the Document is built, not inside each consumer.
This is a graph-level invariant, not a Kotlin fix: any producer, in any language, that lands a hierarchy edge on one entity is expressing nothing.
HONEST LIMIT — "no type is its own supertype" is true of the SOURCE LANGUAGE and not always true of what this function removes. The predicate is equality of grafel entity IDs, and the resolver can collapse two genuinely distinct types onto one id. Measured: `base.py: class Handler` + `impl.py: from base import Handler; class Handler(Handler)` — legal, idiomatic Python — resolves parent and child to the same id, and this filter deletes the edge, leaving the subclass with no recorded superclass at all. (F-bounded polymorphism and CRTP were also probed and are NOT affected: `class C : Base<C>` resolves to two distinct ids and is kept.) Trading a confidently-wrong self-loop for a silent absence is judged an improvement, because a wrong answer to "what implements X" is worse than no answer — but it IS a trade, not a pure win.
The dropped-count printed to stderr therefore conflates two causes: edges that never meant anything (the KMP stub collapse above) and edges that meant something the resolver could not keep apart. The counter cannot separate them; do not read it as "N pieces of noise removed".
func MaterializeRelationship ¶ added in v0.1.9
func MaterializeRelationship(r fbreader.GraphView, i int) Relationship
MaterializeRelationship decodes the i-th relationship of the mmap'd Reader into a single heap-safe graph.Relationship, on demand. Thin exported wrapper over fbRelToGraphRel — byte-identical to the Document's Relationships[i] for the same graph.fb. All strings are copied out of the mmap region, so the result outlives the Reader. Returns the zero Relationship when r is nil or i is out of range.
func RelationshipPtr ¶ added in v0.1.9
func RelationshipPtr(r Relationship) *Relationship
RelationshipPtr returns a pointer to a copy of r. See EntityPtr.
func (Relationship) MarshalJSON ¶ added in v0.1.9
func (r Relationship) MarshalJSON() ([]byte, error)
MarshalJSON emits the same wire shape as the original map-backed Properties field (key "properties", omitted when empty).
func (*Relationship) PropDelete ¶ added in v0.1.9
func (r *Relationship) PropDelete(key string)
PropDelete removes key, if present. No-op if absent or unset.
func (Relationship) PropGet ¶ added in v0.1.9
func (r Relationship) PropGet(key string) string
PropGet returns the value for key, or "" if key is absent.
func (Relationship) PropLen ¶ added in v0.1.9
func (r Relationship) PropLen() int
PropLen returns the number of properties.
func (Relationship) PropLookup ¶ added in v0.1.9
func (r Relationship) PropLookup(key string) (string, bool)
PropLookup returns the value for key and whether it was present.
func (Relationship) PropRange ¶ added in v0.1.9
func (r Relationship) PropRange(f func(k, v string) bool)
PropRange calls f for every key/value pair in key-sorted order. Iteration stops early if f returns false. Safe to call on a zero-value Relationship (no-op).
func (*Relationship) PropSet ¶ added in v0.1.9
func (r *Relationship) PropSet(key, val string)
PropSet sets key to val, lazily initializing the backing storage.
func (*Relationship) PropsReplace ¶ added in v0.1.9
func (r *Relationship) PropsReplace(props map[string]string)
PropsReplace replaces r's entire property set with props, mutating r in place. See WithProperties.
func (Relationship) PropsSnapshot ¶ added in v0.1.9
func (r Relationship) PropsSnapshot() map[string]string
PropsSnapshot returns an independent copy of the properties as a map, or nil if there are none. Callers must not assume the returned map aliases internal storage.
func (*Relationship) UnmarshalJSON ¶ added in v0.1.9
func (r *Relationship) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the wire shape written by MarshalJSON.
func (Relationship) WithProperties ¶ added in v0.1.9
func (r Relationship) WithProperties(props map[string]string) Relationship
WithProperties returns a copy of r with its properties replaced by props, converted into the sorted []propKV backing (a fresh copy — the input map is not aliased, unlike the old field-assignment semantics).
type RelationshipView ¶ added in v0.1.9
type RelationshipView interface {
ID() string
FromID() string
ToID() string
Kind() string
PropGet(key string) string
PropLookup(key string) (string, bool)
PropLen() int
PropRange(f func(k, v string) bool)
PropsSnapshot() map[string]string
}
RelationshipView is the read-only view of a graph relationship. Its property read surface is identical to graph.Relationship's, for the same reason as EntityView's (see there).
func RelationshipViewOf ¶ added in v0.1.9
func RelationshipViewOf(r *Relationship) RelationshipView
RelationshipViewOf wraps a concrete *Relationship as a RelationshipView. Returns a nil interface for a nil relationship.
type SegmentKind ¶ added in v0.2.0
type SegmentKind string
SegmentKind classifies a segment file as carrying the entity stream or the relationship stream. The streams are conceptually independent (decision 5): the writer (a later slice) may emit entity-segments and relationship-segments separately, and the manifest lists/counts them independently. A single segment file MAY in practice carry both (a small graph split only by size), in which case Kind names its PRIMARY stream and both counts are populated.
const ( // SegmentEntities marks a segment whose primary payload is entities. SegmentEntities SegmentKind = "entity" // SegmentRelationships marks a segment whose primary payload is relationships. SegmentRelationships SegmentKind = "relationship" )
type SegmentMeta ¶ added in v0.2.0
type SegmentMeta struct {
// File is the BARE filename (seg-NNNN.fb) of the segment, relative to the
// gen dir. It is never a path — read-side validation (segFileRe) rejects
// any value containing a separator or "..".
File string `json:"file"`
// Kind is the primary stream carried by this segment (entity|relationship).
Kind SegmentKind `json:"kind"`
// EntityCount / RelCount are the number of entities / relationships this
// segment contributes. Either may be zero (a pure-relationship segment has
// EntityCount==0, and vice-versa).
EntityCount int `json:"entity_count"`
RelCount int `json:"rel_count"`
// MinKey / MaxKey are the smallest and largest entity-ID keys present in
// this segment (lexicographic, matching the FlatBuffers `(key)` sort on
// Entity.id). Populated only for segments that carry entities
// (EntityCount>0); empty for pure-relationship segments. The reader uses
// [MinKey,MaxKey] to skip segments during LookupEntityByID (decision 4).
MinKey string `json:"min_key,omitempty"`
MaxKey string `json:"max_key,omitempty"`
}
SegmentMeta describes one segment file within a gen dir's manifest.
type ServiceCycle ¶
type ServiceCycle struct {
// Members lists service (repo) names that form the cycle, sorted.
Members []string `json:"members"`
// Edges lists the directed service→service edges internal to the cycle,
// sorted for determinism.
Edges []ServiceEdge `json:"edges"`
// Size is len(Members), for fast filtering.
Size int `json:"size"`
}
ServiceCycle describes one strongly-connected component of size >= 2 in the service-level graph.
func FindServiceCycles ¶
func FindServiceCycles(links []ServiceLink) []ServiceCycle
FindServiceCycles aggregates directed cross-service links into a service graph and returns every strongly-connected component of size >= 2.
Only links whose relation is a directed dependency (see directedRelations) are considered; undirected co-occurrence relations (shared_label, string_match) are ignored so they cannot fabricate cycles. Self-edges (FromService == ToService) are dropped.
Results are sorted by descending size, then by first member name.
type ServiceEdge ¶
type ServiceEdge struct {
From string `json:"from"`
To string `json:"to"`
Relations []string `json:"relations,omitempty"`
}
ServiceEdge is a directed service→service edge, aggregated from one or more cross-repo links. Relations records the distinct relation labels (calls, imports, publishes_to, ...) that contributed to this edge, for explainability.
type ServiceLink ¶
ServiceLink is the minimal directed cross-service edge FindServiceCycles consumes. It is decoupled from the MCP CrossRepoLink / links.Link shapes so the graph package stays dependency-free; callers map their link type onto it.
type Stats ¶
type Stats struct {
Files int `json:"files"`
Entities int `json:"entities"`
Relationships int `json:"relationships"`
}
Stats summarises a Document.
type SubtypeConflict ¶ added in v0.3.4
SubtypeConflict records one entity whose pre-existing Properties["subtype"] disagrees with its canonical Entity.Subtype. Measured disagreement across the 38 golden fixtures is 0/211, so this is empty on every known input; it exists so that a future producer stamping a second, different answer is visible rather than silently resolved.
func DeriveSubtypeProperties ¶ added in v0.3.4
func DeriveSubtypeProperties(doc *Document) []SubtypeConflict
DeriveSubtypeProperties stamps Properties["subtype"] from the canonical Entity.Subtype on every entity that has a non-empty Subtype and no existing `subtype` property, and returns the entities whose existing property DISAGREES with the canonical field.
It never overwrites an existing `subtype` property — the pre-existing value is the one Properties-only consumers see today, so preserving it is what makes this a derivation rather than a merge — never allocates a property map for an entity whose Subtype is empty, and is idempotent: a second call over the same document stamps nothing, which is what lets it sit in a funnel the producers invoke more than once per write.
The returned conflicts are reported by the caller rather than counted and dropped. A disagreement resolved in silence is the shape that would be hardest to find later, and the report is the only artefact that can be observed; see SortDocumentForEmission.
type SurpriseEdge ¶
type SurpriseEdge struct {
FromID string `json:"from_id"`
ToID string `json:"to_id"`
Score float64 `json:"score"`
Reason string `json:"reason"`
}
SurpriseEdge is a cross-community edge whose pair frequency is rare.
func ComputeSurpriseEdges ¶
func ComputeSurpriseEdges(rels []Relationship, communityOf map[string]int) []SurpriseEdge
ComputeSurpriseEdges scans every relationship; an edge whose endpoints sit in different communities is a "cross-community" edge. We score surprise as 1/frequency of the (commA, commB) pair: a once-only cross is maximally surprising. Top 20 by score are returned.
type UncoveredEntity ¶
type UncoveredEntity struct {
EntityID string `json:"entity_id"`
Name string `json:"name"`
Kind string `json:"kind"`
SourceFile string `json:"source_file"`
StartLine int `json:"start_line"`
Language string `json:"language"`
Module string `json:"module,omitempty"`
// Repo is the slug of the owning repository. ComputeCoverage runs on a
// single document and leaves this empty; the group-level aggregator stamps
// it so the UI can resolve source through the correct repo root in a
// multi-repo group (#4551). Without it, a source-peek ref like
// "src/app.controller.ts:9" cannot be mapped to the repo it lives in.
Repo string `json:"repo,omitempty"`
// Severity is "high" | "medium" | "low".
Severity string `json:"severity"`
// State is the coverage state of this entity (#4662). For entities in this
// (not-reach-covered) list it is either:
//
// "contract-only" — an offline contract spec asserts the endpoint's shape
// but no test executes the handler. NOT dangerously
// untested; severity is downgraded accordingly.
// "uncovered" — neither reach-covered nor contract-covered.
//
// (Reach-covered entities never appear in this list, so the value is never
// "reach" here; the three-state vocabulary is reach | contract-only |
// uncovered, and "reach" lives on the covered side of the report.)
State string `json:"state"`
}
UncoveredEntity is one production entity that has no TESTS inbound.
type WalkUpStats ¶
type WalkUpStats struct {
// HelperTargets is the number of TESTS-edge targets that have inbound CALLS.
HelperTargets int
// DerivedEdges is the number of new derived TESTS edges emitted.
DerivedEdges int
// SkippedHighFanIn is the number of helpers skipped because their caller
// count exceeded maxCallersPerHelper.
SkippedHighFanIn int
// DuplicatesSuppressed is the number of derived edges skipped because an
// explicit TESTS edge already covered the caller.
DuplicatesSuppressed int
}
WalkUpStats records the output of a DeriveTestsWalkUp run.
func DeriveTestsWalkUp ¶
func DeriveTestsWalkUp(doc *Document) WalkUpStats
DeriveTestsWalkUp walks inbound CALLS edges from each TESTS-edge helper target and emits derived TESTS edges to the callers. It mutates doc.Relationships in-place and returns diagnostic statistics.
Source Files
¶
- algorithms.go
- community_names.go
- coverage.go
- coverage_view.go
- cycles.go
- cycles_view.go
- diff.go
- emission_order.go
- genpath.go
- graph.go
- hierarchy_selfloop_6805.go
- kindvocab.go
- load.go
- louvain.go
- manifest.go
- module_gds.go
- nplus1.go
- orientation.go
- pr_impact.go
- pr_impact_infer.go
- reader_open.go
- service_cycles.go
- stream.go
- subtype_property.go
- tests_walkup.go
- view.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package descriptions implements the per-repo DESCRIPTION side-table (#5904 PR-a).
|
Package descriptions implements the per-repo DESCRIPTION side-table (#5904 PR-a). |
|
Package export provides pure, streaming serializers that turn an in-memory graph.Document into static interchange formats for offline analysis and visualization.
|
Package export provides pure, streaming serializers that turn an in-memory graph.Document into static interchange formats for offline analysis and visualization. |
|
Package fbreader — iterator.go provides push-style streaming iterators over the mmap'd graph (S8 of #2149, issue #2159).
|
Package fbreader — iterator.go provides push-style streaming iterators over the mmap'd graph (S8 of #2149, issue #2159). |
|
Package fbversion holds the single source of truth for the on-disk FlatBuffers format version written by internal/graph/fbwriter and read (with a minimum-version gate) by internal/graph/load.go.
|
Package fbversion holds the single source of truth for the on-disk FlatBuffers format version written by internal/graph/fbwriter and read (with a minimum-version gate) by internal/graph/load.go. |
|
Package fbwriter — segmented.go implements the bounded-memory SegmentedWriter that produces the multi-segment gen-dir graph format the #5901 read substrate already understands.
|
Package fbwriter — segmented.go implements the bounded-memory SegmentedWriter that produces the multi-segment gen-dir graph format the #5901 read substrate already understands. |
|
Package flows implements the per-repo FLOW side-table (#5904 PR-b, the #5915 companion to the description side-table in internal/graph/descriptions).
|
Package flows implements the per-repo FLOW side-table (#5904 PR-b, the #5915 companion to the description side-table in internal/graph/descriptions). |
|
diff.go — differential validator (#5349 A4, epic #5350).
|
diff.go — differential validator (#5349 A4, epic #5350). |
|
Package parity provides a strict, order-independent structural-equivalence comparator for two graph Documents — the safety net for grafel's incremental reindex work (epic #5376, layer 2 #5309).
|
Package parity provides a strict, order-independent structural-equivalence comparator for two graph Documents — the safety net for grafel's incremental reindex work (epic #5376, layer 2 #5309). |