feat(mcp): trace_path run mode; the container layer documented and guarded

trace_path mode=run walks what a Service or workload runs: traffic to
the workload (INFRA_MAPS), the Image its container runs (USAGE), the
Dockerfile that builds it and what that builds on (DEPENDS_ON), the
program its ENTRYPOINT starts (SPAWNS) and the environment each
container is given (CONFIGURES) -- Service -> Deployment -> Image ->
Dockerfile -> main in one call.

The README lists the Image label, the container edges (a Dockerfile's
bases and entrypoint, image references, the build vias, compose
services, container environment) and the new mode.

The complexity guard gets the container layer: a fixture with
multi-stage Dockerfiles, an ENTRYPOINT script and a compose project
building two components of one repository, its nodes and edges gated
linear per copy, and the Image edges the component split examines
gated linear too -- every copy's builds join one repository Image by
design, so that join must stay linear.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-11 19:26:10 +02:00
1 parent 231bcbf6f5
commit 4587fd2848
8 files changed
+118 -11

No files matched your search

+8 -6
View File
@@ -36,7 +36,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si
- **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search. One graph query replaces dozens of grep/read cycles.
- **45 supported automatic/conditional client surfaces** — `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries.
- **Built-in graph visualization** — 3D interactive UI at `localhost:9749`, served from the binary itself.
- **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources.
- **Infrastructure-as-code indexing** — Dockerfiles, container images, docker compose, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds and compose services, `Image` nodes joining the images that are built and run, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources — a Service walks to the program its container runs.
- **17 MCP tools** — search, trace, architecture, impact analysis, targeted index-coverage checks, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more.
## Quick Start
@@ -233,10 +233,12 @@ The install script placed beside the binary is **reported, not deleted** — uni
- `USAGE` — an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service)
- `SPAWNS` — the caller starts another program (`subprocess.run`, `exec.Command`, `posix_spawn`, `Process.Start`, a shell command or Make recipe running a program by path, ...); the target is a `Process` node named after the program, or `<dynamic>` when the program is not a literal. When a shell script, Make recipe, GitHub Actions step or Prow job runs a script of the repository (`./hack/x.sh`, `bash hack/x.sh`), the target is that script's `Module` (else its `File`); `make TARGET` (also `$(MAKE)`, `-C`, `-f`) targets the rule of that target, followed through included makefiles; edges from CI carry `ci` and `job`
- `SPAWNS` — the caller starts another program (`subprocess.run`, `exec.Command`, `posix_spawn`, `Process.Start`, a shell command or Make recipe running a program by path, ...); the target is a `Process` node named after the program, or `<dynamic>` when the program is not a literal. When a shell script, Make recipe, GitHub Actions step or Prow job runs a script of the repository (`./hack/x.sh`, `bash hack/x.sh`), the target is that script's `Module` (else its `File`); `make TARGET` (also `$(MAKE)`, `-C`, `-f`) targets the rule of that target, followed through included makefiles; edges from CI carry `ci` and `job`. A Dockerfile's last stage `ENTRYPOINT` (else `CMD`) spawns what the container runs (`api: dockerfile`): the repository script or source copied into the image, the Go `main` of a binary a stage builds with `go build -o`, else a `Process`
- `INFRA_MAPS` — a Kubernetes Service to each workload whose pod labels carry every `key: value` of the Service's selector, within one namespace
- `USAGE` with `ref: image` — a Kubernetes workload (any pod template: containers, initContainers, CronJobs, custom resources) or a docker compose service to the `Image` it runs
- `DEPLOYS` — what puts manifests, kustomizations or charts on a cluster, to what it puts there: a Flux Kustomization or Argo Application to the kustomizations / manifests / chart of its path, a Flux HelmRelease to its chart (`spec.chart` or `chartRef` → OCIRepository), a shell command or Make recipe (`kubectl apply -f/-k`, `kustomize build | kubectl apply`, `helm install/upgrade/template`), a Go installer loading a chart with the Helm SDK, a Terraform `helm_release`. A chart outside the repository is a shared external `Chart`; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, ...)
- `DEPENDS_ON` — a Helm chart to the charts it declares (the local chart for a resolvable `file://` repository, otherwise an external `Chart`; the edge keeps `repository`), and a `go.mod` / `requirements.txt` file to an external `Package`
- `DEPENDS_ON` — a Helm chart to the charts it declares (the local chart for a resolvable `file://` repository, otherwise an external `Chart`; the edge keeps `repository`), and a `go.mod` / `requirements.txt` file to an external `Package`; a Dockerfile to the `Image`s it builds on (`via: from`, `copy-from`); an `Image` to the Dockerfile it is built from (`via: build`; `tool`: `skaffold`, `compose`, `docker-build` for `docker` / `podman` / `nerdctl` / `buildah` build commands in scripts, Make and CI); a compose service to the services its `depends_on` / `links` name
- `CONFIGURES` — code reading an environment variable to the shared `EnvVar` of that name (`strategy: env_access`); a Kubernetes container, compose service or Dockerfile (`ENV` of the image) setting one to that container's own `EnvVar` (`strategy: env_set`, with `source`: `value`, `configMapKeyRef`, `secretKeyRef`, `fieldRef`, ..., `compose`, `dockerfile` — values are never stored)
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
@@ -245,7 +247,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
### Indexing pipeline
- **158 vendored tree-sitter grammars** compiled into the binary
- **Generic package / module resolution** — bare specifiers like `@myorg/pkg`, `github.com/foo/bar`, `use my_crate::foo` resolved via manifest scanning (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `composer.json`, `pubspec.yaml`, `pom.xml`, `build.gradle`, `mix.exs`, `*.gemspec`)
- **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests (`Resource`), Kustomize overlays (`IMPORTS` to their entries), Helm charts (`Chart`, `DEPENDS_ON`), Terraform / OpenTofu (`.tf`, `.tofu`, `.tfvars`) as graph nodes
- **Infrastructure-as-code indexing** — Dockerfiles (`Dockerfile.*`, `*.Dockerfile`, `Containerfile`: bases, what the container runs, `ENV`), container images (`Image`: the normalised repository, tag and digest on the edges; a repository built from several Dockerfiles, one per component tag, gets an `Image` per component), docker compose services (`Resource`), Kubernetes manifests (`Resource`), Kustomize overlays (`IMPORTS` to their entries), Helm charts (`Chart`, `DEPENDS_ON`), Terraform / OpenTofu (`.tf`, `.tofu`, `.tfvars`) as graph nodes. A Service walks to the code it runs: Service → workload → `Image` → Dockerfile → `main`
- **[Hybrid LSP semantic type resolution](#hybrid-lsp)** for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl — a lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers including tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer (parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files, namespace + trait + late-static-binding resolution for PHP, file-scoped namespaces + records + LINQ method syntax for C#, class-hierarchy + overload + lambda resolution for Java, extension-function + scope-function resolution for Kotlin, trait-method + UFCS resolution for Rust)
- **RAM-first pipeline**: LZ4 compression, in-memory SQLite, single dump at end. Memory released after.
@@ -724,7 +726,7 @@ mode. `index_status` keeps describing the published graph and its freshness.
| Tool | Description |
|------|-------------|
| `search_graph` | Structural, BM25, and semantic search. Page structural rows with `offset`/`limit` and ranked semantic rows independently with `semantic_offset`/`semantic_limit`. |
| `trace_path` | BFS traversal — who calls a function and what it calls (alias: `trace_call_path`). Depth 1-5. `mode`: `calls` (default), `data_flow`, `cross_service`, `topology` (Kubernetes objects, routes, controllers and what they deploy — start at a Resource or Route), `deploy` (what a CI job, script or Make rule runs and deploys — outbound from the job, inbound from a chart). |
| `trace_path` | BFS traversal — who calls a function and what it calls (alias: `trace_call_path`). Depth 1-5. `mode`: `calls` (default), `data_flow`, `cross_service`, `topology` (Kubernetes objects, routes, controllers and what they deploy — start at a Resource or Route), `deploy` (what a CI job, script or Make rule runs and deploys — outbound from the job, inbound from a chart), `run` (what a Service or workload runs: workload → `Image` → Dockerfile → the program its container starts, with the environment each container is given — outbound from a Service, workload or compose service). |
| `detect_changes` | Map git diff to affected symbols + blast radius with risk classification. |
| `query_graph` | Execute Cypher-like graph queries (read-only). |
| `get_graph_schema` | Node/edge counts, relationship patterns, property definitions per label. Run this first. |
@@ -744,7 +746,7 @@ mode. `index_status` keeps describing the published graph and its freshness.
### Node Labels
`Project`, `Package`, `Folder`, `File`, `Module`, `Section`, `Class`, `Struct`, `Interface`, `Trait`, `Enum`, `Type`, `Function`, `Method`, `Field`, `Variable`, `Constant`, `Macro`, `Decorator`, `Route`, `Channel`, `Table`, `Process`, `EnvVar`, `Resource`, `Chart`, `Branch`
`Project`, `Package`, `Folder`, `File`, `Module`, `Section`, `Class`, `Struct`, `Interface`, `Trait`, `Enum`, `Type`, `Function`, `Method`, `Field`, `Variable`, `Constant`, `Macro`, `Decorator`, `Route`, `Channel`, `Table`, `Process`, `EnvVar`, `Resource`, `Chart`, `Image`, `Branch`
### Edge Types
+14 -3
View File
@@ -623,10 +623,12 @@ static const tool_def_t TOOLS[] = {
"args unchanged; budget may increase.\"},"
"\"mode\":{"
"\"type\":\"string\",\"enum\":[\"calls\",\"data_flow\",\"cross_service\",\"topology\","
"\"deploy\"],"
"\"deploy\",\"run\"],"
"\"default\":\"calls\",\"description\":\"calls, argument-aware data_flow, service edges, "
"topology: infra objects, routes and controllers (start at a Resource or Route), or "
"deploy: scripts, make targets and charts a CI job, script or rule runs and deploys.\"},"
"topology: infra objects, routes and controllers (start at a Resource or Route), "
"deploy: scripts, make targets and charts a CI job, script or rule runs and deploys, or "
"run: what a Service or workload runs (image, Dockerfile, the program and its "
"environment).\"},"
"\"parameter_name\":{\"type\":\"string\",\"description\":\"data_flow parameter filter.\"},"
"\"edge_types\":{\"type\":\"array\",\"items\":{"
"\"type\":\"string\"}},\"risk_labels\":{\"type\":\"boolean\",\"default\":false,"
@@ -8671,6 +8673,12 @@ static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode,
* make targets, programs), calls (CALLS: shell functions) and puts on a
* cluster (DEPLOYS) -- outbound from the job, inbound from a chart */
static const char *mode_deploy[] = {"DEPLOYS", "SPAWNS", "CALLS"};
/* run: what a Service or workload runs -- traffic to the workload
* (INFRA_MAPS), the Image its container runs (USAGE), the Dockerfile
* that builds it and what that builds on (DEPENDS_ON), the program the
* ENTRYPOINT starts (SPAWNS) and the environment each container is
* given (CONFIGURES) */
static const char *mode_run[] = {"INFRA_MAPS", "USAGE", "DEPENDS_ON", "SPAWNS", "CONFIGURES"};
*out_count = 0;
@@ -8709,6 +8717,9 @@ static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode,
} else if (mode && strcmp(mode, "deploy") == 0) {
defaults = mode_deploy;
n_defaults = (int)(sizeof(mode_deploy) / sizeof(mode_deploy[0]));
} else if (mode && strcmp(mode, "run") == 0) {
defaults = mode_run;
n_defaults = (int)(sizeof(mode_run) / sizeof(mode_run[0]));
}
for (int i = 0; i < n_defaults; i++) {
out_types[i] = defaults[i];
+8
View File
@@ -0,0 +1,8 @@
FROM alpine:3.20 AS base
ENV TZ=UTC
FROM base
ENV APP_HOME=/app \
# where the script lives
APP_MODE=web
COPY run.sh /usr/local/bin/run.sh
ENTRYPOINT ["/usr/local/bin/run.sh"]
@@ -0,0 +1,4 @@
FROM alpine:3.20
ENV APP_MODE=worker
COPY run.sh /usr/local/bin/run.sh
CMD ["/usr/local/bin/run.sh"]
+20
View File
@@ -0,0 +1,20 @@
services:
web:
image: acme/cx-app:1-web
build: .
depends_on:
- db
environment:
DB_HOST: db
LOG_LEVEL: info
worker:
image: acme/cx-app:1-worker
build:
context: .
dockerfile: Dockerfile.worker
depends_on:
- db
environment:
- QUEUE
db:
image: postgres:16
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
echo "running $APP_MODE"
+17 -2
View File
@@ -73,6 +73,7 @@ extern _Atomic uint64_t g_k8s_candidates;
extern _Atomic uint64_t g_op_candidates;
extern _Atomic uint64_t g_sh_candidates;
extern _Atomic uint64_t g_tf_candidates;
extern _Atomic uint64_t g_dk_hub_edges;
/* ── Corpus scale ──────────────────────────────────────────────────────
* Small on purpose: the gate reads exponents from ratios, not magnitudes.
@@ -382,8 +383,13 @@ static void cx_pass_sink(const char *line) {
* group/kind is one identity across a repository: copies declaring the same
* one ARE joined (a CRD links to every package marking its group), so the
* fixtures' CRDs use groups no copy's Go code declares -- the guard measures
* accidental growth, not that by-design join. */
static const char *const CX_INFRA_DIRS[] = {"fx_Kubernetes", "fx_Go", "fx_Bash", "fx_HCL"};
* accidental growth, not that by-design join. The container layer (Docker)
* has Dockerfiles with stage ENV, an ENTRYPOINT script and a compose project
* building two components of one repository; an image repository is one
* identity too, so every copy's builds of it join one Image -- that join
* must stay linear (the component split examines its edges). */
static const char *const CX_INFRA_DIRS[] = {"fx_Kubernetes", "fx_Go", "fx_Bash", "fx_HCL",
"fx_Dockerfile"};
enum { CX_INFRA_COUNT = sizeof(CX_INFRA_DIRS) / sizeof(CX_INFRA_DIRS[0]) };
typedef struct {
@@ -405,6 +411,7 @@ typedef struct {
uint64_t op_candidates; /* Go types / dirs the operator pass examined */
uint64_t sh_candidates; /* paths / make rules the shell layer examined */
uint64_t tf_candidates; /* blocks the Terraform layer examined */
uint64_t dk_hub_edges; /* Image edges the component split examined */
int infra_nodes[CX_INFRA_COUNT];
int infra_edges[CX_INFRA_COUNT];
double wall_s;
@@ -433,6 +440,7 @@ static int cx_run(const char *root, const char *db_path, CxMetrics *out) {
uint64_t oc0 = atomic_load_explicit(&g_op_candidates, memory_order_relaxed);
uint64_t sc0 = atomic_load_explicit(&g_sh_candidates, memory_order_relaxed);
uint64_t fc0 = atomic_load_explicit(&g_tf_candidates, memory_order_relaxed);
uint64_t dh0 = atomic_load_explicit(&g_dk_hub_edges, memory_order_relaxed);
atomic_store_explicit(&g_cx_pass_count, 0, memory_order_relaxed);
cbm_log_set_sink_ex(cx_pass_sink, CBM_LOG_SINK_TEE);
@@ -474,6 +482,7 @@ static int cx_run(const char *root, const char *db_path, CxMetrics *out) {
out->op_candidates = atomic_load_explicit(&g_op_candidates, memory_order_relaxed) - oc0;
out->sh_candidates = atomic_load_explicit(&g_sh_candidates, memory_order_relaxed) - sc0;
out->tf_candidates = atomic_load_explicit(&g_tf_candidates, memory_order_relaxed) - fc0;
out->dk_hub_edges = atomic_load_explicit(&g_dk_hub_edges, memory_order_relaxed) - dh0;
cbm_store_t *s = cbm_store_open_path(db_path);
if (!s) {
@@ -625,6 +634,12 @@ TEST(complexity_infra_resolution_is_linear) {
printf(" sh_candidates ratio %.2f tf_candidates ratio %.2f\n", sr, fr);
ASSERT_TRUE(sr >= CX_RATIO_LO && sr <= CX_RATIO_HI);
ASSERT_TRUE(fr >= CX_RATIO_LO && fr <= CX_RATIO_HI);
printf(" dk_hub_edges %llu -> %llu\n", (unsigned long long)a->dk_hub_edges,
(unsigned long long)b->dk_hub_edges);
ASSERT_GT((long long)a->dk_hub_edges, 0);
double dr = cx_ratio((double)b->dk_hub_edges, (double)a->dk_hub_edges);
printf(" dk_hub_edges ratio %.2f\n", dr);
ASSERT_TRUE(dr >= CX_RATIO_LO && dr <= CX_RATIO_HI);
for (int i = 0; i < CX_INFRA_COUNT; i++) {
ASSERT_GT(a->infra_nodes[i], 0);
ASSERT_GT(a->infra_edges[i], 0);
+45
View File
@@ -1675,6 +1675,50 @@ TEST(probe_mcp_trace_topology_follows_deploys) {
PASS();
}
/* trace_path mode=run walks what a Service runs: out to its workload, the
* Image the workload runs, the Dockerfile that builds the image, the Go main
* its ENTRYPOINT starts -- and the environment the container is given.
* mode=topology stops before the program. */
TEST(probe_mcp_trace_run) {
static const GpfFile files[] = {
{"go.mod", "module example.com/app\n\ngo 1.22\n"},
{"cmd/server/main.go", "package main\n\nfunc main() {}\n"},
{"svc/Dockerfile", "FROM golang:1.22 AS build\nWORKDIR /src\nCOPY . .\n"
"RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server\n"
"FROM gcr.io/distroless/static\nCOPY --from=build /out/server /server\n"
"ENTRYPOINT [\"/server\"]\n"},
{"skaffold.yaml", "apiVersion: skaffold/v4beta6\nkind: Config\nbuild:\n artifacts:\n"
" - image: acme/server\n context: .\n docker:\n"
" dockerfile: svc/Dockerfile\n"},
{"k8s/app.yaml",
"apiVersion: v1\nkind: Service\nmetadata:\n name: server\nspec:\n selector:\n"
" app: server\n---\n"
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: server\nspec:\n"
" template:\n metadata:\n labels:\n app: server\n spec:\n"
" containers:\n - name: server\n image: acme/server:1.0\n"
" env:\n - name: LISTEN_PORT\n value: \"8080\"\n"}};
char *r = gpf_tool_on(files, 5, "trace_path",
"{\"function_name\":\"Service/server\",\"project\":\"%s\","
"\"direction\":\"outbound\",\"mode\":\"run\",\"depth\":5,"
"\"format\":\"json\"}");
ASSERT_NOT_NULL(r);
/* qualified names are grouped by prefix, split at a dot */
ASSERT_NOT_NULL(strstr(r, "Deployment/-/server"));
ASSERT_NOT_NULL(strstr(r, "\"__image__docker\",\"rows\":[[\"io/acme/server\""));
ASSERT_NOT_NULL(strstr(r, ".svc\",\"rows\":[[\"Dockerfile\"")); /* its Module */
ASSERT_NOT_NULL(strstr(r, ".cmd.server\",\"rows\":[[\"main\"")); /* what it starts */
ASSERT_NOT_NULL(strstr(r, "Deployment/-/server#env/server/LISTEN_PORT"));
free(r);
r = gpf_tool_on(files, 5, "trace_path",
"{\"function_name\":\"Service/server\",\"project\":\"%s\","
"\"direction\":\"outbound\",\"mode\":\"topology\",\"depth\":5,"
"\"format\":\"json\"}");
ASSERT_NOT_NULL(r);
ASSERT_NULL(strstr(r, ".cmd.server\",\"rows\":[[\"main\""));
free(r);
PASS();
}
/* trace_path mode=deploy walks deploy chains: in from a chart to the shell
* function that installs it (DEPLOYS), the script calling the function
* (CALLS) and the CI job running the script (SPAWNS). The call graph mode
@@ -4679,6 +4723,7 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_mcp_search_filters_kind_and_namespace);
RUN_TEST(probe_mcp_trace_topology);
RUN_TEST(probe_mcp_trace_topology_follows_deploys);
RUN_TEST(probe_mcp_trace_run);
RUN_TEST(probe_mcp_trace_deploy);
RUN_TEST(probe_dockerfile_bases_are_images);
RUN_TEST(probe_dockerfile_variants_are_dockerfiles);