Skip to content

Latest commit

 

History

History
290 lines (214 loc) · 12.4 KB

File metadata and controls

290 lines (214 loc) · 12.4 KB

Plan: ASP.NET Core API Boundary Support

Context

The codeagent index engine currently indexes C# symbols (classes, methods, properties, etc.) and their relationships (calls, inherits, implements), but has no awareness of HTTP API boundaries. When an LLM agent navigates a codebase, knowing which methods are API endpoints — their routes, HTTP verbs, auth requirements, and request/response types — is critical for understanding the public surface area of a service.

This change adds detection and indexing of ASP.NET Core controller-based API endpoints. The architecture is designed to be extensible to Minimal APIs and gRPC services later.

Scope: ASP.NET Core controllers only (classes with [ApiController] or inheriting ControllerBase, action methods with [HttpGet]/[HttpPost]/etc.)

Metadata per endpoint: route template, HTTP method, authorization policy, request body type, response type, status codes, content types.

MCP surface: New search_api_endpoints tool + API metadata attached to existing get_symbol and get_file_outline responses.


Storage: New api_endpoints Table (Migration 004)

File: crates/codeagent-core/src/db/schema.rs

A separate table (not columns on nodes) so that:

  • Only API nodes get rows — no null-bloat for the 95% of symbols that aren't endpoints
  • The 29-column row_to_node() convention is preserved unchanged
  • ON DELETE CASCADE handles cleanup automatically
  • Extensible for Minimal APIs and gRPC via the api_style column
CREATE TABLE IF NOT EXISTS api_endpoints (
    endpoint_id       INTEGER  PRIMARY KEY AUTOINCREMENT,
    node_id           BLOB(16) NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
    controller_id     BLOB(16)          REFERENCES nodes(node_id) ON DELETE SET NULL,
    api_style         TEXT     NOT NULL DEFAULT 'controller',
    http_method       TEXT,
    route_template    TEXT,
    auth_policy       TEXT,
    request_body_type TEXT,
    response_type     TEXT,
    status_codes      TEXT,
    consumes          TEXT,
    extractor_version TEXT     NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_node_id    ON api_endpoints(node_id);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_controller ON api_endpoints(controller_id);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_http_method ON api_endpoints(http_method);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_route      ON api_endpoints(route_template);

Update CURRENT_SCHEMA_VERSION from 3 → 4.


Rust Core: New graph/api_endpoints.rs Module

File: crates/codeagent-core/src/graph/api_endpoints.rs (NEW)

Structs:

  • ApiStyle enum: Controller, MinimalApi, Grpc (extensible, serde snake_case)
  • ApiEndpointUpsert — write struct with all fields
  • ApiEndpoint — read-back struct (adds endpoint_id)

Functions:

  • upsert_api_endpoint(conn, &ApiEndpointUpsert) -> Result<()>
  • delete_api_endpoints_for_file(conn, file_path) -> Result<usize> — deletes by joining on nodes.file_path
  • get_api_endpoint(conn, node_id) -> Result<Option<ApiEndpoint>> — single lookup
  • get_api_endpoints_for_file(conn, file_path) -> Result<Vec<ApiEndpoint>> — batch lookup for outline
  • search_api_endpoints(conn, http_method?, route_pattern?, controller_id?, limit) -> Result<Vec<ApiEndpoint>> — filtered search (LIKE on route_template)

Re-export from graph/mod.rs.


Tree-Sitter Extraction (Phase 1 — Syntactic)

File: crates/codeagent-core/src/adapters/csharp.rs

New helper struct: AspNetControllerContext

struct AspNetControllerContext {
    is_api_controller: bool,
    class_route_template: Option<String>,
    class_auth_policy: Option<String>,
    controller_node_id: Option<NodeId>,
    controller_name: String,
}

Attribute extraction from tree-sitter AST

Add extract_aspnet_attributes(node, source) that walks attribute_list > attribute children on a declaration node and returns parsed data. Handles:

  • [ApiController] → marks class as controller
  • [Route("api/[controller]")] → class-level route template
  • [HttpGet], [HttpPost], [HttpPut], [HttpDelete], [HttpPatch] → HTTP method + optional route fragment
  • [Authorize], [Authorize(Policy = "...")] → auth policy
  • [AllowAnonymous]auth_policy = "anonymous"
  • [ProducesResponseType(typeof(T), statusCode)] → response type + status codes
  • [Consumes("...")] → request content type

Route resolution

resolve_route_template(class_route, method_route, controller_name):

  • Replace [controller] token with controller name minus "Controller" suffix, lowercased
  • Combine class + method route segments with /

Integration into ParseContext

  • Add controller_ctx: Option<AspNetControllerContext> field to ParseContext
  • In handle_class: extract attributes; if [ApiController] is present (or class inherits from ControllerBase), populate controller_ctx before walking children. Clear it after.
  • In handle_method: if controller_ctx.is_some(), extract method-level attributes, resolve the full route, detect [FromBody] parameter type, extract response type from ActionResult<T> / Task<ActionResult<T>>, then call delete + upsert_api_endpoint() with extractor_version = "treesitter-cs".
  • Also detect ControllerBase inheritance in handle_class's emit_base_type_edges — check if any base type name contains "ControllerBase" or "Controller" as a heuristic.

IPC Protocol Extension

File: crates/codeagent-core/src/ipc/protocol.rs

Add:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiEndpointInfo {
    pub http_method: Option<String>,
    pub route_template: Option<String>,
    pub auth_policy: Option<String>,
    pub request_body_type: Option<String>,
    pub response_type: Option<String>,
    pub status_codes: Option<String>,
    pub consumes: Option<String>,
}

Add optional field to SemanticNode:

#[serde(skip_serializing_if = "Option::is_none")]
pub api_endpoint: Option<ApiEndpointInfo>,

Backward-compatible: older extractors that omit this field deserialize as None.


Roslyn Extractor (Semantic — Phase 2a)

File: extractors/csharp/src/Protocol.cs

  • Add ApiEndpointInfo class with matching JSON properties
  • Add ApiEndpoint property to SemanticNode

File: extractors/csharp/src/RoslynExtractor.cs

  • Add ExtractApiEndpoint(IMethodSymbol, SemanticModel) that uses symbol.GetAttributes() to authoritatively extract:
    • HTTP method attributes (HttpGetAttribute, etc.)
    • Route resolution from RouteAttribute on both class and method
    • AuthorizeAttribute / AllowAnonymousAttribute with policy names
    • ProducesResponseTypeAttribute for response types and status codes
    • ConsumesAttribute for content types
    • FromBodyAttribute on parameters for request body type
  • Attach ApiEndpointInfo to the corresponding SemanticNode for each controller action

Semantic Enrichment

File: crates/codeagent-core/src/ingest/semantic.rs

In enrich_file(), after identity reconciliation, add a step that processes SemanticNode.api_endpoint:

  1. Look up the method's node_id by location (existing pattern)
  2. Find the controller node_id via the Contains edge (parent class)
  3. Delete existing api_endpoints rows for this node_id
  4. Insert authoritative Roslyn-derived endpoint data with extractor_version = "roslyn"

This overwrites the tree-sitter approximation when Roslyn is available.


MCP: New search_api_endpoints Tool

File: crates/codeagent-mcp/src/tools/search.rs

Params:

struct SearchApiEndpointsParams {
    http_method: Option<String>,      // "GET", "POST", etc.
    route_pattern: Option<String>,    // Substring match on route_template
    controller_id: Option<String>,    // UUID filter
    limit: Option<usize>,            // default 20, max 50
}

Response: array of objects, each with endpoint (API metadata) + method (node metadata: name, qualified_name, file_path, line_start, parameter_signature, return_type).

File: crates/codeagent-mcp/src/state.rs

  • Register the new tool with #[tool(description = "Search API endpoints by HTTP method, route, or controller")]
  • Total tool count: 18 → 19

File: crates/codeagent-mcp/src/serialization.rs

  • Add api_endpoint_to_json(ep, node) function

Extend Existing MCP Tools

File: crates/codeagent-mcp/src/tools/navigation.rs

get_symbol

After fetching the node, also load get_api_endpoint(conn, node_id). If present, attach an api_endpoint key to the JSON response.

get_file_outline

After fetching outline nodes, batch-load get_api_endpoints_for_file(conn, file_path) into a HashMap. When serializing each outline node, attach api_endpoint (http_method + route_template) if present.


Implementation Order

Phase A: Foundation (schema + graph module)

  1. db/schema.rs — add MIGRATION_004, bump CURRENT_SCHEMA_VERSION to 4
  2. graph/api_endpoints.rs — new file with structs + all CRUD functions
  3. graph/mod.rs — add pub mod api_endpoints and re-exports
  4. Write unit tests for CRUD operations + ON DELETE CASCADE
  5. cargo test -p codeagent-core — verify 0 regressions

Phase B: Tree-sitter extraction

  1. adapters/csharp.rs — add attribute extraction helpers + AspNetControllerContext
  2. adapters/csharp.rs — wire into handle_class and handle_method
  3. Write adapter tests with sample ASP.NET Core controller source
  4. cargo test -p codeagent-core — verify all tests pass

Phase C: IPC protocol + Roslyn extractor + semantic enrichment

  1. ipc/protocol.rs — add ApiEndpointInfo struct + field on SemanticNode
  2. extractors/csharp/src/Protocol.cs — add ApiEndpointInfo class
  3. extractors/csharp/src/RoslynExtractor.cs — add ExtractApiEndpoint logic
  4. ingest/semantic.rs — process api_endpoint from semantic nodes
  5. Write IPC serde backward-compatibility tests
  6. cargo test -p codeagent-core

Phase D: MCP surface

  1. serialization.rs — add api_endpoint_to_json
  2. tools/search.rs — add search_api_endpoints handler
  3. tools/navigation.rs — extend get_symbol and get_file_outline
  4. state.rs — register new tool
  5. Write MCP integration tests
  6. cargo test -p codeagent-mcp

Phase E: Documentation

  1. Update MEMORY.md with new conventions
  2. Update TESTS_IMPLEMENTATION_PLAN.md with new test entries
  3. Run Sync-TestCoverage.ps1
  4. Full cargo test to confirm

Test Plan

Core tests (graph/api_endpoints.rs)

  • Migration 004 creates table successfully
  • upsert_api_endpoint + get_api_endpoint roundtrip
  • search_api_endpoints filters by http_method, route_pattern, controller_id
  • delete_api_endpoints_for_file removes correct rows
  • ON DELETE CASCADE removes endpoints when method node is hard-deleted

Adapter tests (adapters/csharp.rs)

  • Basic [ApiController] + [HttpGet] detection
  • Route resolution: [Route("api/[controller]")] + [HttpGet("{id}")]api/users/{id}
  • Auth extraction: [Authorize(Policy = "Admin")], [AllowAnonymous]
  • [FromBody] parameter type extraction
  • Response type from ActionResult<T> and Task<ActionResult<T>>
  • Non-controller classes produce no api_endpoints rows
  • Class without [ApiController] attribute is ignored

IPC tests (ipc/protocol.rs)

  • SemanticNode with api_endpoint deserializes correctly
  • Backward compat: missing api_endpoint field deserializes as None

MCP tests (crates/codeagent-mcp)

  • search_api_endpoints returns filtered results
  • get_symbol includes api_endpoint for action methods
  • get_symbol omits api_endpoint for non-endpoint nodes
  • get_file_outline includes api_endpoint for action methods

Key Risks & Mitigations

  1. Tree-sitter attribute accuracy: Tree-sitter extraction is best-effort (textual matching). Roslyn provides authoritative data that overwrites tree-sitter on semantic enrichment. Both paths produce valid data; Roslyn is just more precise.

  2. 29-column row_to_node() convention: Preserved — no changes to nodes table. API data is in a separate table with its own read functions.

  3. Deletion safety: ON DELETE CASCADE on api_endpoints.node_id — no manual cleanup needed when nodes are deleted.

  4. IPC backward compat: The api_endpoint field on SemanticNode is Option with skip_serializing_if. Older extractors that don't produce it deserialize as None.

  5. Schema migration: Additive only (new table, no ALTER TABLE). Existing databases upgrade cleanly.