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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ the compatibility and migration notes before upgrading.

### Added

- Name-addressed callers, callees and impact radius for v2 (CodeGraph)
generations: `POST /v1/graph/callers`, `/v1/graph/callees` and
`/v1/graph/impact-radius`, and the MCP tools `graph_callers`, `graph_callees`
and `graph_impact_radius`. They follow CodeGraph's `codegraph_callers`,
`codegraph_callees` and `codegraph_impact`: qualified names, per-definition
sections, `file` narrowing and limits. They are compared with the pinned
CodeGraph answers. Differences in definition order and non-exact name fallback
are documented in `docs/graph-exploration.md`.
- Repository-scoped graph publication (CodeGraph parity S1.08).
`POST /v1/graph/uploads` now accepts v2 artifacts
(`application/vnd.graphnest.graph.v2+protobuf`). Administrators can publish
Expand Down
37 changes: 37 additions & 0 deletions docs/execplans/codegraph-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ Implementation, validation, draft publication, and release are separate states.

## Progress

- 2026-09-27: S1.07 symbol tools on `feat/codegraph/s1-07-symbol-tools`, based
on `main`. The pinned harness now also runs the upstream `codegraph_callers`,
`codegraph_callees`, and `codegraph_impact` handlers: eleven new real answers,
52 in all.
- The timing baseline was refreshed because it pins the harness hash; timings
were measured on the same machine class.
- GraphNest adds name-addressed `SymbolCalls`/`SymbolImpact` queries, service
authorization, REST (`/v1/graph/callers`, `/callees`, `/impact-radius`), MCP
(`graph_callers`, `graph_callees`, `graph_impact_radius`), and capability
workflows.
- `TestGraphSymbolToolsMatchCodeGraph` loads the real fixture into PostgreSQL
and renders GraphNest's structured answers in upstream's layout. The eight
callers/callees answers match exactly apart from the order of same-named
definitions. The three impact answers match as sets: depth 1 is equal, and
depth 2 adds only the two documented shortest-depth nodes.
- Reversing the relation order fails five cases.
- REST and MCP return identical answers from PostgreSQL, and an ungranted
repository returns 404.

- 2026-09-27: S1.08 repository-scoped publication on
`feat/codegraph/s1-08-publish-policy`, based on `main`. `POST
/v1/graph/uploads` now accepts v2 artifacts from administrators and from
Expand Down Expand Up @@ -509,6 +528,19 @@ these tests.
transaction if that window matters.
- REST-published generations record no producer capability list. Stage 2
should declare capabilities once it negotiates CodeGraph schema versions.
- Symbol tools resolve exact-name candidates with upstream's `matchesSymbol`
rules instead of `Discover`. `Discover` is exploration ranking: it splits
`MissingFixtureSymbol987` and matches `Fixture`, while upstream `searchNodes`
returns nothing. Two consequences are recorded until `searchNodes` (FTS5 BM25,
LIKE and Levenshtein fallbacks, rescoring) is ported:
- same-named definitions are ordered by generated flag, path, and line instead
of by BM25;
- a name with no exact match returns `not_found` instead of the best
non-exact hit.
- Callers and callees read edges in the producer's row order. Upstream's
unordered `IN (...)` over `idx_edges_{target,source}_kind` yields kind
ascending, then rowid; v2 fixtures keep rowid order as the edge ordinal. Stage
2 importers must export edges in rowid order to keep this.

## Discoveries

Expand Down Expand Up @@ -683,6 +715,10 @@ The rebased sessions layer also passes the exact two-call restoration comparison
implemented. Production query parity, browser parity, CLI import, and
local-engine work remains pending. Publication has no MCP tool; publishers
use REST.
- `searchNodes` has no GraphNest port; symbol tools use exact-name candidates
(see Decisions). `codegraph_node` (file and symbol modes) and the remaining
S1.07 transports are still to do. The Nix option-path branch is covered by
code review only; the pinned fixture has no Nix sources.
- Full Stage 1 validation (including authorization, database, browser, deployment,
and real-producer conformance) has not run and is not claimed as passing.
- The proposed warm-query p95 budgets remain unchanged: existing GraphNest within
Expand Down Expand Up @@ -712,6 +748,7 @@ The rebased sessions layer also passes the exact two-call restoration comparison
| S1.06a3 entity impact | `feat/codegraph/s1-06a3-entity-impact` | Implemented, independently approved and signed (`931e7d9`); depends on PR #81 | Draft [PR #82](https://github.com/balcsida/graphnest/pull/82); native stack #66, position 17; CI and CodeQL passed |
| S1.06b1 type relations and hierarchy | `feat/codegraph/type-hierarchy` | Implemented; focused unit, service and PostgreSQL checks pass; based on `main` | [PR #119](https://github.com/balcsida/graphnest/pull/119) |
| S1.08 publication policy | `feat/codegraph/s1-08-publish-policy` | Implemented; unit race, PostgreSQL integration race (apart from clock-skewed supply-chain claims that fail on `main` too), vet, staticcheck and OpenAPI checks pass; based on `main` | [PR #123](https://github.com/balcsida/graphnest/pull/123) |
| S1.07 symbol tools | `feat/codegraph/s1-07-symbol-tools` | Implemented; oracle, unit race, PostgreSQL integration race, vet, staticcheck, OpenAPI and parity-reference checks pass; based on `main` | [PR #124](https://github.com/balcsida/graphnest/pull/124) |

The first one-branch submission created a draft PR without a remote stack.
Submitting the second real dependent layer created native stack #66
Expand Down
43 changes: 41 additions & 2 deletions docs/graph-exploration.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,46 @@ changes before delivery.
Discovery and exploration require an artifact v2 generation with its discovery
projection. Missing or stale data returns `graph_not_ready`; an empty ready
result is never used to hide unavailable data. Capabilities report the selected
generation and producer coverage separately from server workflows. Public graph
upload remains v1 only: there is no public v2 upload endpoint in this milestone.
generation and producer coverage separately from server workflows. v2
generations are published through `POST /v1/graph/uploads` by administrators or
repository publication grantees; see [operations](operations.md#publishing-v2-graph-generations).
Session history, entity selectors, and qualified wildcard methods are not part
of these public requests.

## Symbol callers, callees and impact

`POST /v1/graph/callers`, `/v1/graph/callees`, and `/v1/graph/impact-radius`, and
the MCP tools `graph_callers`, `graph_callees`, and `graph_impact_radius`, address
a v2 generation by symbol name, like CodeGraph's `codegraph_callers`,
`codegraph_callees`, and `codegraph_impact`. They follow the pinned handlers:

- A name matches a node's name, a file's name without its extension, or a
qualified form (`Class.method`, `module::fn`, `dir/module`). The qualified form
matches the qualified-name suffix, or the containing directories and file for
Rust modules and Python packages. `crate::`, `super::`, and `self::` are
ignored, and an Erlang arity (`fn/3`) must match. A dotted Nix option resolves
its `options.` declaration and writes first.
- Matches sharing a path and qualified name form one definition, so same-file
overloads stay together and same-named classes in different apps stay apart.
`file` narrows by path or path suffix. When it matches nothing, every
definition is returned with `file_filter: unmatched`.
- Callers and callees follow `calls`, `imports`, `instantiates`, `navigates`, and
`references` one hop, in the producer's edge order. Each neighbor appears once
with the first edge that reached it; the edge kind tells an instantiation or
import apart from a call. `limit` bounds each definition (default 20, 1-100)
and `truncated` says more exist.
- Impact merges each definition's impact radius (`depth` default 2, 1-10).

`TestGraphSymbolToolsMatchCodeGraph` compares all eleven captured `mcp-callers-*`,
`mcp-callees-*`, and `mcp-impact-*` answers. Known differences:

- CodeGraph orders same-named definitions by SQLite FTS5 BM25 score. GraphNest
orders them by generated file last, then path and line.
- CodeGraph falls back to its best non-exact search hit when no definition
matches exactly. GraphNest returns `not_found` until a `searchNodes` port lands.
At most 50 matches are considered, as upstream does; `candidate_limit` marks
more.
- Impact visits dependency levels by shortest depth (see
[graph analysis](graph-analysis.md#entity-impact-and-public-graph-projections)).
It can therefore list nodes that CodeGraph's depth-first walk omits, in a
different order within a file.
98 changes: 97 additions & 1 deletion docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,54 @@ paths:
'500': {description: Response exceeded the configured byte limit}
'503': {$ref: '#/components/responses/Unavailable'}
'504': {$ref: '#/components/responses/Timeout'}
/v1/graph/callers:
post:
description: Everything that calls, imports, instantiates, navigates to, or references each definition of a symbol name, one entry per distinct definition (same path and qualified name). Each neighbor appears once with the first edge that reached it, in the producer's edge order.
security: [{bearerAuth: []}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolCallsRequest'}}}}
responses:
'200': {description: Definitions of the symbol with their bounded results; status not_found when no definition matches exactly, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolResponse'}}}}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthenticated'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/GraphConflict'}
'413': {$ref: '#/components/responses/InvalidRequest'}
'415': {$ref: '#/components/responses/InvalidRequest'}
'500': {description: Response exceeded the configured byte limit}
'503': {$ref: '#/components/responses/Unavailable'}
'504': {$ref: '#/components/responses/Timeout'}
/v1/graph/callees:
post:
description: The outgoing counterpart of /v1/graph/callers.
security: [{bearerAuth: []}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolCallsRequest'}}}}
responses:
'200': {description: Definitions of the symbol with their bounded results; status not_found when no definition matches exactly, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolResponse'}}}}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthenticated'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/GraphConflict'}
'413': {$ref: '#/components/responses/InvalidRequest'}
'415': {$ref: '#/components/responses/InvalidRequest'}
'500': {description: Response exceeded the configured byte limit}
'503': {$ref: '#/components/responses/Unavailable'}
'504': {$ref: '#/components/responses/Timeout'}
/v1/graph/impact-radius:
post:
description: Each distinct definition's merged impact radius. Unlike /v1/graph/impact it addresses v2 generations by symbol name.
security: [{bearerAuth: []}]
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolImpactRequest'}}}}
responses:
'200': {description: Definitions of the symbol with their bounded results; status not_found when no definition matches exactly, content: {application/json: {schema: {$ref: '#/components/schemas/GraphSymbolResponse'}}}}
'400': {$ref: '#/components/responses/InvalidRequest'}
'401': {$ref: '#/components/responses/Unauthenticated'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/GraphConflict'}
'413': {$ref: '#/components/responses/InvalidRequest'}
'415': {$ref: '#/components/responses/InvalidRequest'}
'500': {description: Response exceeded the configured byte limit}
'503': {$ref: '#/components/responses/Unavailable'}
'504': {$ref: '#/components/responses/Timeout'}
/v1/graph/explore:
post:
security: [{bearerAuth: []}]
Expand Down Expand Up @@ -2107,7 +2155,7 @@ components:
version: {type: integer, const: 1}
query_artifact_versions: {type: array, items: {type: integer, enum: [1, 2]}}
upload_artifact_versions: {type: array, items: {type: integer, enum: [1, 2]}, description: POST /v1/graph/uploads accepts v1 from administrators and v2 from administrators or publication grantees.}
workflows: {type: array, items: {type: object, additionalProperties: false, required: [name, artifact_version], properties: {name: {type: string, enum: [context, impact, trace, discover, explore, files, capabilities]}, artifact_version: {type: integer, enum: [1, 2]}}}}
workflows: {type: array, items: {type: object, additionalProperties: false, required: [name, artifact_version], properties: {name: {type: string, enum: [context, impact, trace, discover, explore, files, capabilities, callers, callees, impact_radius]}, artifact_version: {type: integer, enum: [1, 2]}}}}
status: {type: string, enum: [ready]}
freshness: {type: string, enum: [current]}
repository_id: {type: integer, format: int64, minimum: 1}
Expand Down Expand Up @@ -2209,6 +2257,54 @@ components:
range: {$ref: '#/components/schemas/GraphPosition'}
confidence: {type: number}
resolution_reason: {type: string}
GraphSymbolCallsRequest:
type: object
additionalProperties: false
required: [symbol]
properties:
repo: {$ref: '#/components/schemas/GraphRepositorySelector'}
branch: {type: string, minLength: 1}
symbol: {type: string, minLength: 1, maxLength: 16384, description: 'Name, or qualified name such as Class.method, module::fn or dir/module'}
file: {type: string, maxLength: 16384, description: Narrow same-named definitions to this path or path suffix; when none matches every definition is returned and file_filter is unmatched.}
limit: {type: integer, description: Per definition; default 20; clamped to 1-100.}
GraphSymbolImpactRequest:
type: object
additionalProperties: false
required: [symbol]
properties:
repo: {$ref: '#/components/schemas/GraphRepositorySelector'}
branch: {type: string, minLength: 1}
symbol: {type: string, minLength: 1, maxLength: 16384}
file: {type: string, maxLength: 16384}
depth: {type: integer, description: Default 2; clamped to 1-10.}
GraphSymbolRelated:
type: object
additionalProperties: false
required: [entity, edge]
properties:
entity: {$ref: '#/components/schemas/GraphEntityV2'}
edge: {$ref: '#/components/schemas/GraphEvidenceV2'}
GraphSymbolDefinition:
type: object
additionalProperties: false
required: [definitions]
properties:
definitions: {type: array, minItems: 1, items: {$ref: '#/components/schemas/GraphEntityV2'}, description: Every match sharing one path and qualified name.}
related: {type: array, items: {$ref: '#/components/schemas/GraphSymbolRelated'}, description: Callers or callees.}
truncated: {type: boolean, description: More callers or callees exist than limit.}
entities: {type: array, items: {$ref: '#/components/schemas/GraphEntityV2'}, description: Impact radius, definitions included.}
edges: {type: array, items: {$ref: '#/components/schemas/GraphEvidenceV2'}}
GraphSymbolResponse:
type: object
additionalProperties: false
required: [status, definitions, generations, partial]
properties:
status: {type: string, enum: [ok, not_found]}
file_filter: {type: string, enum: [matched, unmatched]}
definitions: {type: array, items: {$ref: '#/components/schemas/GraphSymbolDefinition'}}
generations: {type: array, minItems: 1, items: {$ref: '#/components/schemas/GraphGeneration'}}
boundaries: {type: array, items: {$ref: '#/components/schemas/GraphBoundary'}}
partial: {type: boolean}
GraphBoundary:
type: object
additionalProperties: false
Expand Down
Loading
Loading