Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1b60840
Add repository-scoped dead-code read capabilities for #1002
zzet Oct 27, 2026
d4204c7
Apply repository scope inside dead-code analysis for #1002
zzet Oct 27, 2026
3df1b7a
Check scoped dead-code paging plans and generation isolation for #1002
zzet Oct 27, 2026
a5e17f2
Pass resolved dead-code scope and retained ownership through MCP for …
zzet Oct 27, 2026
1f24e89
Preserve adjacency masks in scoped dead-code queries for #1002
zzet Oct 27, 2026
1b294f9
Bound outline membership and entry-point reads for #1002
zzet Oct 27, 2026
7f5c19b
Cache immutable scoped outlines by mutation and analysis identity for…
zzet Oct 27, 2026
df44aeb
Warm the existing outline ranking after current analysis publication …
zzet Oct 27, 2026
9e990a4
Prove production outline routing and six-repository output parity for…
zzet Oct 27, 2026
9db1975
Retain exact scoped reads while bulk indexes are absent for #1002
zzet Oct 27, 2026
ca821f0
Prepare bounded exact outline sections after explicit analysis for #1002
zzet Oct 27, 2026
e0e9e97
Read only scoped process steps for dead-code analysis in #1002
zzet Oct 27, 2026
35781b4
Bound retained outline ownership and skip empty-scope process reads f…
zzet Oct 27, 2026
d22c8dc
Cover scoped analysis capabilities in generation isolation checks for…
zzet Oct 27, 2026
405e37a
Handle absent optional indexes before fatal scan classification in #1002
zzet Oct 27, 2026
e79d7e6
Assert outline field projection and string identity contracts in #1002
zzet Oct 27, 2026
df46d03
Assert retained outline substrings preserve contents in #1002
zzet Oct 27, 2026
bf9150e
Avoid an unused process inverse map in scoped dead-code reads for #1002
zzet Oct 27, 2026
a396692
Keep scoped analysis readers and routing fixtures lint-clean for #1002
zzet Oct 27, 2026
a6b0cf2
Merge main 24ce36c4 into scoped analysis work
zzet Oct 27, 2026
e2bb1f8
Merge main into scoped dead-code and outline work
zzet Oct 29, 2026
0c71da9
fix: cancel outline preparation with its analysis owner
zzet Oct 29, 2026
d57aff4
test: use promoted contextual store method in warmup fixture
zzet Oct 29, 2026
f76d73c
test(mcp): isolate resource and prompt activity ownership
zzet Oct 29, 2026
ebb8723
perf(analysis): keep filtered dead-code queries for unscoped readers
zzet Oct 29, 2026
0ed5d7d
test(analysis): pin unscoped masked and foreign usage semantics
zzet Oct 29, 2026
bfed41f
style(mcp): format dead-code query fixture fields
zzet Oct 29, 2026
aabac80
test(analysis): reproduce legacy dead-code candidate kind drift
zzet Oct 30, 2026
1bd7c4b
fix(analysis): share positive dead-code kind eligibility
zzet Oct 30, 2026
1e0debf
test: expose workspace dead-code candidate overreach
zzet Oct 30, 2026
4e6a648
perf: push exact workspace scope into dead-code reads
zzet Oct 30, 2026
e581160
perf: seek effective workspace buckets for dead-code analysis
zzet Oct 30, 2026
7526518
test: retain scoped analysis through bulk index restoration
zzet Oct 30, 2026
2e4c0b9
test: simplify exact-scope fixture checks
zzet Oct 30, 2026
0912b93
fix: register exact workspace planner and generation contracts
zzet Oct 30, 2026
2068ac0
test: require demand-driven interface evidence for dead methods
zzet Oct 30, 2026
40d4bed
perf: demand interface evidence only for dead method receivers
zzet Oct 30, 2026
5b55afa
test: retain active analysis across an index-only upgrade
zzet Oct 30, 2026
1729e48
fix: preserve analysis for explicitly neutral index migrations
zzet Oct 30, 2026
f679844
test: expose dead-code hydration growth from rejected globals
zzet Oct 30, 2026
9369615
perf: exclude known ineligible dead-code candidates before usage reads
zzet Oct 30, 2026
22381f2
test: measure rejected-global candidate scan growth
zzet Oct 30, 2026
c14bd2a
perf: seek eligible dead-code workspace candidates without rejected g…
zzet Oct 30, 2026
6052e86
test(sqlite): expose dead-code primary-row probes
zzet Oct 30, 2026
18c0278
perf(sqlite): cover proven-workspace dead-code probes
zzet Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 96 additions & 14 deletions internal/analysis/betweenness.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package analysis

import (
"context"
"math/rand"
"sort"

Expand Down Expand Up @@ -73,8 +74,21 @@ const (
// Pivot sampling is seeded with a fixed seed, so results are
// reproducible run to run.
func ComputeBetweenness(g graph.Reader) *BetweennessResult {
result, _ := ComputeBetweennessContext(context.Background(), g)
return result
}

// ComputeBetweennessContext retains the same deterministic pivots and scores,
// but never returns a partial ranking when its owner is cancelled.
func ComputeBetweennessContext(ctx context.Context, g graph.Reader) (*BetweennessResult, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if g == nil {
return &BetweennessResult{Scores: map[string]float64{}}
return &BetweennessResult{Scores: map[string]float64{}}, nil
}
if ctx.Done() != nil {
g = graph.BindReadContext(g, ctx)
}
// Betweenness measures shortest-path centrality across the
// call / reference subgraph; only function and method nodes carry
Expand Down Expand Up @@ -107,16 +121,22 @@ func ComputeBetweenness(g graph.Reader) *BetweennessResult {
// Federation proxy nodes carry real kinds, so the kind filter above
// keeps them — drop them here so a remote stub is never a pivot.
ids = excludeProxyIDs(ids)
if err := ctx.Err(); err != nil {
return nil, err
}
n := len(ids)
if n == 0 {
return &BetweennessResult{Scores: map[string]float64{}}
return &BetweennessResult{Scores: map[string]float64{}}, nil
}

// Stable node ordering: betweenness itself is order-independent,
// but a deterministic order makes the sampled pivot pick
// reproducible regardless of the iteration order
// NodeIDsByKinds happens to return.
sort.Strings(ids)
if err := ctx.Err(); err != nil {
return nil, err
}

// Forward adjacency over the call / reference subgraph.
// EdgeAdjacencyForKinds returns only the (from, to) projection of
Expand All @@ -132,37 +152,57 @@ func ComputeBetweenness(g graph.Reader) *BetweennessResult {
// wakeup on the production graph.
idIndex := make(map[string]int, n)
for i, id := range ids {
if i&1023 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
idIndex[id] = i
}
adjacency := make([][]int, n)
appendPair := func(from, to string) {
scanned := 0
appendPair := func(from, to string) bool {
scanned++
if scanned&1023 == 0 && ctx.Err() != nil {
return false
}
fromIdx, fromOK := idIndex[from]
toIdx, toOK := idIndex[to]
if !fromOK || !toOK {
return
return true
}
adjacency[fromIdx] = append(adjacency[fromIdx], toIdx)
return true
}
if adjScan, ok := g.(graph.EdgeAdjacencyForKinds); ok {
for pair := range adjScan.EdgeAdjacencyForKinds(betweennessKinds, bcNodeKinds) {
appendPair(pair[0], pair[1])
if !appendPair(pair[0], pair[1]) {
return nil, ctx.Err()
}
}
} else if es, ok := g.(graph.EdgesByKindsScanner); ok {
for e := range es.EdgesByKinds(betweennessKinds) {
if e != nil {
appendPair(e.From, e.To)
if !appendPair(e.From, e.To) {
return nil, ctx.Err()
}
}
}
} else {
for _, kind := range betweennessKinds {
for e := range g.EdgesByKind(kind) {
if e != nil {
appendPair(e.From, e.To)
if !appendPair(e.From, e.To) {
return nil, ctx.Err()
}
}
}
}
}

if err := ctx.Err(); err != nil {
return nil, err
}
// Adaptive fast path: exact for small graphs, sampled otherwise.
sources := ids
sampled := false
Expand All @@ -171,10 +211,15 @@ func ComputeBetweenness(g graph.Reader) *BetweennessResult {
sources = samplePivots(ids, betweennessPivots)
}

if err := ctx.Err(); err != nil {
return nil, err
}
scoreDense := make([]float64, n)
scratch := newBrandesScratch(n)
for _, src := range sources {
brandesAccumulate(idIndex[src], adjacency, scoreDense, &scratch)
if err := brandesAccumulateContext(ctx, idIndex[src], adjacency, scoreDense, &scratch); err != nil {
return nil, err
}
}

// Rescale the sampled estimate up to a full-source equivalent and convert
Expand All @@ -186,18 +231,26 @@ func ComputeBetweenness(g graph.Reader) *BetweennessResult {
score := make(map[string]float64, n)
var max float64
for i, id := range ids {
if i&1023 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
v := scoreDense[i] * scale
score[id] = v
if v > max {
max = v
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return &BetweennessResult{
Scores: score,
Max: max,
Sampled: sampled,
Pivots: len(sources),
}
}, nil
}

// samplePivots picks k distinct source nodes from ids using a
Expand Down Expand Up @@ -250,30 +303,48 @@ func newBrandesScratch(nodes int) brandesScratch {
}
}

func (s *brandesScratch) reset() {
func (s *brandesScratch) resetContext(ctx context.Context) error {
for i := range s.dist {
if i&1023 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
s.sigma[i] = 0
s.dist[i] = -1
s.delta[i] = 0
s.preds[i] = s.preds[i][:0]
}
s.queue = s.queue[:0]
s.order = s.order[:0]
return ctx.Err()
}

// The graph is unweighted, so a plain BFS yields shortest paths. Node IDs have
// already been lowered to dense integer slots by ComputeBetweenness; no maps
// are allocated in the per-pivot loop.
func brandesAccumulate(src int, adjacency [][]int, score []float64, scratch *brandesScratch) {
scratch.reset()
func brandesAccumulateContext(ctx context.Context, src int, adjacency [][]int, score []float64, scratch *brandesScratch) error {
if err := scratch.resetContext(ctx); err != nil {
return err
}
scratch.sigma[src] = 1
scratch.dist[src] = 0
scratch.queue = append(scratch.queue, src)

for head := 0; head < len(scratch.queue); head++ {
if head&255 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
v := scratch.queue[head]
scratch.order = append(scratch.order, v)
for _, w := range adjacency[v] {
for edge, w := range adjacency[v] {
if edge&1023 == 1023 {
if err := ctx.Err(); err != nil {
return err
}
}
if scratch.dist[w] < 0 {
scratch.dist[w] = scratch.dist[v] + 1
scratch.queue = append(scratch.queue, w)
Expand All @@ -286,14 +357,25 @@ func brandesAccumulate(src int, adjacency [][]int, score []float64, scratch *bra
}

for i := len(scratch.order) - 1; i >= 0; i-- {
if i&255 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
w := scratch.order[i]
if scratch.sigma[w] != 0 {
for _, v := range scratch.preds[w] {
for edge, v := range scratch.preds[w] {
if edge&1023 == 1023 {
if err := ctx.Err(); err != nil {
return err
}
}
scratch.delta[v] += (scratch.sigma[v] / scratch.sigma[w]) * (1 + scratch.delta[w])
}
}
if w != src {
score[w] += scratch.delta[w]
}
}
return ctx.Err()
}
Loading
Loading