Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions cmd/gortex/wire_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ func wireContractGolden(name string) string {
// ProjectID, were added.
return "3b8920ab88d05028e215d68d5917445e2e6d05bdad23aef6dcdf6c9920647823"
case "graph.Edge":
// Bumped when Context was added — the per-reference role label
// (parameter_type / return_type / field / …) populated on demand by
// find_usages via RefContextOf. Additive: gob decodes older
// snapshots with Context blank, and it is recomputed at query time.
// (Previously bumped when Tier was added.)
return "ed897cce4720cd1482d8c217ba5ffb72d7f19d5d1c2d4015b9a98e9daa9d4b63"
// Bumped when ReturnUsage was added — the per-call-site return-value
// consumption label (discarded / assigned / returned / …) populated on
// demand at extraction and query time. Additive: gob decodes older
// snapshots with ReturnUsage blank, and it is recomputed.
// (Previously bumped when Context, then Tier, were added.)
return "f537793b5542de95a9a4f383e6ed02317ac416c529b187e02fa60dccef1112d0"
case "snapshotHeader":
// Bumped when the VectorIndex / VectorDims / VectorCount fields
// were added (additive — gob decodes unknown fields as zero).
Expand Down
26 changes: 16 additions & 10 deletions docs/wire-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,22 @@ Exactly one row.

### `find_usages`

| field | type | description |
|------------|--------|-------------|
| from | string | caller symbol ID |
| to | string | called symbol ID (the query subject) |
| edge_kind | string | `calls`, `references`, `implements`, ... |
| origin | string | tier: `lsp_resolved`, `lsp_dispatch`, `ast_resolved`, `ast_inferred`, `text_matched` |
| confidence | float | 0..1 |
| from_name | string | caller short name |
| from_path | string | caller file path |
| from_line | int | caller start line |
| field | type | description |
|------------------|--------|-------------|
| from | string | caller symbol ID |
| to | string | called symbol ID (the query subject) |
| edge_kind | string | `calls`, `references`, `implements`, ... |
| context | string | reference role at the usage site: `parameter_type`, `return_type`, `field`, `value`, `type`, `attribute`, `generic_arg`, `call` |
| return_usage | string | how a call site consumes the return value: `discarded`, `assigned`, `partially_ignored`, `returned`, `goroutine`, `deferred`, `argument`, `condition`; empty when unclassified |
| origin | string | provenance: `lsp_resolved`, `lsp_dispatch`, `ast_resolved`, `ast_inferred`, `text_matched` |
| tier | string | coarse provenance label derived from origin |
| confidence | float | 0..1 |
| from_name | string | caller short name |
| from_path | string | usage-site file path |
| from_line | int | call-site line (falls back to the caller's start line) |
| from_is_test | bool | caller is a test symbol |
| from_test_role | string | `test`, `benchmark`, `fuzz`, `example` when applicable |
| from_test_runner | string | detected JS/TS test runner when applicable |

### `get_file_summary`

Expand Down
5 changes: 3 additions & 2 deletions internal/agents/claudecode/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ These wrap the discovery + impact + memory surfaces into ordered playbooks so po
|------|-------------------|
| flow_between | Ranked dataflow paths between two symbol IDs. Walks ` + "`value_flow`" + ` (intra-procedural) ∪ ` + "`arg_of`" + ` (caller arg → callee param) ∪ ` + "`returns_to`" + ` (callee → assignment). Pass ` + "`max_depth`" + ` (default 8) and ` + "`max_paths`" + ` (default 10). |
| taint_paths | Pattern-driven dataflow sweep — every flow from a matching source to a matching sink. Patterns: bare token = name substring; ` + "`exact:Foo`" + `; ` + "`path:dir/`" + `; ` + "`kind:method`" + ` (clauses combine with AND). Sinks expand functions to their params automatically. |
| get_cfg | Per-function control-flow graph for one function/method ID — basic blocks, labeled edges (seq/true/false/loop_back/break/continue/return/case/exception/finally), per-statement def/use sets, and statement-granular reaching-definition chains. Optional ` + "`mermaid`" + ` rendering. Go / Python / JS / TS / Java / Rust / Ruby. |

### Structural Code Search
| Tool | What it gives you |
Expand Down Expand Up @@ -392,7 +393,7 @@ These wrap the discovery + impact + memory surfaces into ordered playbooks so po
### Code Quality
| Tool | What it gives you |
|------|-------------------|
| analyze | Unified graph-analysis dispatcher (60 kinds). Structural: dead_code, hotspots, cycles, would_create_cycle, clusters, concepts, role, connectivity_health, edge_audit, constructors_missing_fields. Quality / security: health_score, impact, sast, hygiene, unsafe_patterns, named, review (idiomatic / correctness rulepack — NPE, thread-safety check-then-act, N+1, logic-error; Go + Python — with a graph-grounded false-positive-reduction pass). Churn / ownership: todos, stale_code, ownership, fixes_history, blame. Coverage / releases: coverage, coverage_gaps, coverage_summary, releases. Schema / SQL: orphan_tables, unreferenced_tables, sql_call_sites, sql_rebuild, dbt_models, models. Flags / interop: stale_flags, cgo_users, wasm_users. Edge-driven: channel_ops, race_writes, unclosed_channels, goroutine_spawns, field_writers, annotation_users, config_readers, env_var_users, event_emitters, log_events, string_emitters, error_surface, external_calls, tests_as_edges. Web / infra: routes, components, k8s_resources, images, kustomize, pubsub. Cross-repo: cross_repo. Provenance / resolution: synthesizers (framework-dispatch edges grouped by pass), resolution_outcomes (why a call/ref edge stayed unresolved). Extensible: domain |
| analyze | Unified graph-analysis dispatcher (60 kinds). Structural: dead_code, hotspots, cycles, would_create_cycle, clusters, concepts, role, connectivity_health, edge_audit, constructors_missing_fields. Quality / security: health_score, impact, sast, hygiene, unsafe_patterns, named, review (idiomatic / correctness rulepack — NPE, thread-safety check-then-act, N+1, logic-error; Go + Python — with a graph-grounded false-positive-reduction pass). Churn / ownership: todos, stale_code, ownership, fixes_history, blame. Coverage / releases: coverage, coverage_gaps, coverage_summary, releases. Schema / SQL: orphan_tables, unreferenced_tables, sql_call_sites, sql_rebuild, dbt_models, models. Flags / interop: stale_flags, cgo_users, wasm_users. Edge-driven: channel_ops, race_writes, unclosed_channels, goroutine_spawns, field_writers, annotation_users, config_readers, env_var_users, event_emitters, log_events, string_emitters, error_surface, external_calls, tests_as_edges. Web / infra: routes, components, k8s_resources, images, kustomize, pubsub. Cross-repo: cross_repo. Dataflow: def_use (per-function reaching-definition def→use chains over the on-demand CFG; pairs with the get_cfg tool). Provenance / resolution: synthesizers (framework-dispatch edges grouped by pass), resolution_outcomes (why a call/ref edge stayed unresolved). Extensible: domain |
| analyze kind=dead_code | Symbols with zero incoming edges (excludes entry points, tests, exports) |
| analyze kind=hotspots | Over-coupled symbols ranked by fan-in, fan-out, and community crossings |
| analyze kind=cycles | Tarjan's SCC with severity classification |
Expand Down Expand Up @@ -460,7 +461,7 @@ These wrap the discovery + impact + memory surfaces into ordered playbooks so po
### API Contracts
| Tool | What it gives you |
|------|-------------------|
| contracts | API contracts: action=list (default) lists detected contracts; action=check matches providers/consumers and reports orphans across repos. Scope either action with ` + "`repo`" + `, ` + "`project`" + `, or ` + "`ref`" + ` |
| contracts | API contracts: action=list (default) lists detected contracts; action=check matches providers/consumers and reports orphans across repos; action=bridge ranks matched provider↔consumer groups (RRF over text / path-repo / adjacency / degree signals; mode=impact for a symbol's cross-service blast radius). Scope any action with ` + "`repo`" + `, ` + "`project`" + `, or ` + "`ref`" + ` |

### Config Hygiene
| Tool | What it gives you |
Expand Down
73 changes: 67 additions & 6 deletions internal/analysis/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,61 @@ type ContractViolation struct {

// VerifyResult is the output of contract violation verification.
type VerifyResult struct {
Violations []ContractViolation `json:"violations"`
CheckedCallers int `json:"checked_callers"`
CheckedImpls int `json:"checked_impls"`
Clean bool `json:"clean"`
Errors []string `json:"errors,omitempty"`
CrossRepoViolations bool `json:"cross_repo_violations,omitempty"`
Violations []ContractViolation `json:"violations"`
CheckedCallers int `json:"checked_callers"`
CheckedImpls int `json:"checked_impls"`
Clean bool `json:"clean"`
Errors []string `json:"errors,omitempty"`
CrossRepoViolations bool `json:"cross_repo_violations,omitempty"`
ReturnUsage []ReturnUsageSummary `json:"return_usage,omitempty"`
}

// ReturnUsageSummary aggregates how the call sites of one changed
// function or method consume its return value — the "who actually uses
// the return?" answer an agent needs before changing a return
// signature. Counts come from the extractor-stamped return-usage label
// on each incoming call edge; call sites the classifier could not
// place are reported as unclassified rather than guessed.
type ReturnUsageSummary struct {
SymbolID string `json:"symbol_id"`
CallSites int `json:"call_sites"`
Counts map[string]int `json:"counts,omitempty"`
Unclassified int `json:"unclassified,omitempty"`
}

// summarizeReturnUsage builds the return-usage distribution for one
// function/method's incoming call edges. Returns nil when the symbol
// has no call sites at all (nothing to report).
func summarizeReturnUsage(g graph.Store, node *graph.Node) *ReturnUsageSummary {
if node == nil || (node.Kind != graph.KindFunction && node.Kind != graph.KindMethod) {
return nil
}
summary := &ReturnUsageSummary{SymbolID: node.ID}
for _, e := range g.GetInEdges(node.ID) {
if e.Kind != graph.EdgeCalls {
continue
}
// Skip speculative dispatch edges: the read surfaces (find_usages
// and the rest) hide them by default, so counting them here would
// make this distribution disagree with the call sites a user
// actually sees.
if e.IsSpeculative() {
continue
}
summary.CallSites++
if usage := graph.ReturnUsageOf(e); usage != "" {
if summary.Counts == nil {
summary.Counts = map[string]int{}
}
summary.Counts[usage]++
} else {
summary.Unclassified++
}
}
if summary.CallSites == 0 {
return nil
}
return summary
}

// parsedSignature holds the extracted parameter and return type info from a signature string.
Expand Down Expand Up @@ -64,6 +113,18 @@ func VerifyChanges(g graph.Store, engine *query.Engine, changes []SignatureChang
}
}

// For a function/method, summarise how its call sites consume
// the return value — the sites that bind / return / branch on the
// result are the ones a return-type change touches. The discarded
// count is not a blanket all-clear: a discarded label also folds
// every-sink-blank multi-assignment (Go `_, _ = f()`), which still
// breaks on a return-arity change because the blank list must
// match the result count. Read it as "the value is unused here",
// not "this site is safe to change".
if summary := summarizeReturnUsage(g, node); summary != nil {
result.ReturnUsage = append(result.ReturnUsage, *summary)
}

// Check callers for parameter mismatches
callerSG := engine.GetCallers(change.SymbolID, query.QueryOptions{Depth: 2, Limit: 500})
for _, callerNode := range callerSG.Nodes {
Expand Down
149 changes: 149 additions & 0 deletions internal/analysis/return_usage_summary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package analysis

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/query"
)

// buildReturnUsageGraph wires a target function with three classified
// call sites and one the extractor left unstamped.
func buildReturnUsageGraph(t *testing.T) (*graph.Graph, string) {
t.Helper()
g := graph.New()
targetID := "pkg/t.go::Target"
g.AddNode(&graph.Node{
ID: targetID, Kind: graph.KindFunction, Name: "Target",
FilePath: "pkg/t.go", StartLine: 1,
Meta: map[string]any{"signature": "func() error"},
})
for i, usage := range []string{
graph.ReturnUsageDiscarded,
graph.ReturnUsageDiscarded,
graph.ReturnUsageAssigned,
"", // unclassified site
} {
callerID := "pkg/c.go::caller" + string(rune('A'+i))
g.AddNode(&graph.Node{
ID: callerID, Kind: graph.KindFunction, Name: "caller",
FilePath: "pkg/c.go", StartLine: 10 * (i + 1),
})
e := &graph.Edge{
From: callerID, To: targetID, Kind: graph.EdgeCalls,
FilePath: "pkg/c.go", Line: 10*(i+1) + 1,
}
if usage != "" {
e.Meta = map[string]any{graph.MetaReturnUsage: usage}
}
g.AddEdge(e)
}
return g, targetID
}

func TestVerifyChanges_ReturnUsageSummary(t *testing.T) {
g, targetID := buildReturnUsageGraph(t)
engine := query.NewEngine(g)

result := VerifyChanges(g, engine, []SignatureChange{
{SymbolID: targetID, NewSignature: "func() (int, error)"},
})

require.Len(t, result.ReturnUsage, 1)
ru := result.ReturnUsage[0]
assert.Equal(t, targetID, ru.SymbolID)
assert.Equal(t, 4, ru.CallSites)
assert.Equal(t, 2, ru.Counts[graph.ReturnUsageDiscarded])
assert.Equal(t, 1, ru.Counts[graph.ReturnUsageAssigned])
assert.Equal(t, 1, ru.Unclassified)
}

// Speculative dispatch edges are hidden by default on every read
// surface (find_usages and the rest), so the return-usage distribution
// must not count them either — otherwise verify_change disagrees with
// the call sites a user actually sees.
func TestVerifyChanges_ReturnUsageSkipsSpeculative(t *testing.T) {
g := graph.New()
targetID := "pkg/t.go::Target"
g.AddNode(&graph.Node{
ID: targetID, Kind: graph.KindFunction, Name: "Target",
FilePath: "pkg/t.go", StartLine: 1,
Meta: map[string]any{"signature": "func() error"},
})
// One concrete (visible) assigned call site.
g.AddNode(&graph.Node{
ID: "pkg/c.go::real", Kind: graph.KindFunction, Name: "real", FilePath: "pkg/c.go", StartLine: 10,
})
g.AddEdge(&graph.Edge{
From: "pkg/c.go::real", To: targetID, Kind: graph.EdgeCalls,
FilePath: "pkg/c.go", Line: 11,
Meta: map[string]any{graph.MetaReturnUsage: graph.ReturnUsageAssigned},
})
// One speculative dispatch call site — hidden from read surfaces by
// default, so it must not appear in the distribution.
g.AddNode(&graph.Node{
ID: "pkg/c.go::spec", Kind: graph.KindFunction, Name: "spec", FilePath: "pkg/c.go", StartLine: 20,
})
g.AddEdge(&graph.Edge{
From: "pkg/c.go::spec", To: targetID, Kind: graph.EdgeCalls,
FilePath: "pkg/c.go", Line: 21, Origin: graph.OriginSpeculative,
Meta: map[string]any{
graph.MetaReturnUsage: graph.ReturnUsageDiscarded,
graph.MetaSpeculative: true,
},
})
engine := query.NewEngine(g)

result := VerifyChanges(g, engine, []SignatureChange{
{SymbolID: targetID, NewSignature: "func() (int, error)"},
})

require.Len(t, result.ReturnUsage, 1)
ru := result.ReturnUsage[0]
assert.Equal(t, 1, ru.CallSites, "speculative call site must not be counted")
assert.Equal(t, 1, ru.Counts[graph.ReturnUsageAssigned])
assert.Zero(t, ru.Counts[graph.ReturnUsageDiscarded], "speculative discarded site must be excluded")
assert.Zero(t, ru.Unclassified)
}

// A non-callable symbol must not produce a distribution: return-usage
// only means something for function/method return values.
func TestVerifyChanges_ReturnUsageSkipsNonFunctions(t *testing.T) {
g := graph.New()
typeID := "pkg/t.go::Config"
g.AddNode(&graph.Node{
ID: typeID, Kind: graph.KindType, Name: "Config", FilePath: "pkg/t.go",
})
g.AddNode(&graph.Node{
ID: "pkg/c.go::user", Kind: graph.KindFunction, Name: "user", FilePath: "pkg/c.go",
})
g.AddEdge(&graph.Edge{
From: "pkg/c.go::user", To: typeID, Kind: graph.EdgeReferences,
FilePath: "pkg/c.go", Line: 4,
})
engine := query.NewEngine(g)

result := VerifyChanges(g, engine, []SignatureChange{
{SymbolID: typeID, NewSignature: "struct{}"},
})
assert.Empty(t, result.ReturnUsage)
}

// A function nobody calls reports no distribution rather than an empty
// one — there is nothing to break.
func TestVerifyChanges_ReturnUsageSkipsUncalled(t *testing.T) {
g := graph.New()
fnID := "pkg/t.go::Lonely"
g.AddNode(&graph.Node{
ID: fnID, Kind: graph.KindFunction, Name: "Lonely", FilePath: "pkg/t.go",
Meta: map[string]any{"signature": "func()"},
})
engine := query.NewEngine(g)

result := VerifyChanges(g, engine, []SignatureChange{
{SymbolID: fnID, NewSignature: "func() error"},
})
assert.Empty(t, result.ReturnUsage)
}
Loading
Loading