diff --git a/README.md b/README.md index 3a65ad4d7..3f3b6c6b7 100644 --- a/README.md +++ b/README.md @@ -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 `` 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 `` 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 diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 9453ba0fa..7791bf27f 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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]; diff --git a/tests/fixtures/complexity/Dockerfile/Dockerfile b/tests/fixtures/complexity/Dockerfile/Dockerfile new file mode 100644 index 000000000..985fd1488 --- /dev/null +++ b/tests/fixtures/complexity/Dockerfile/Dockerfile @@ -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"] diff --git a/tests/fixtures/complexity/Dockerfile/Dockerfile.worker b/tests/fixtures/complexity/Dockerfile/Dockerfile.worker new file mode 100644 index 000000000..35c273d65 --- /dev/null +++ b/tests/fixtures/complexity/Dockerfile/Dockerfile.worker @@ -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"] diff --git a/tests/fixtures/complexity/Dockerfile/compose.yaml b/tests/fixtures/complexity/Dockerfile/compose.yaml new file mode 100644 index 000000000..eb41e0006 --- /dev/null +++ b/tests/fixtures/complexity/Dockerfile/compose.yaml @@ -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 diff --git a/tests/fixtures/complexity/Dockerfile/run.sh b/tests/fixtures/complexity/Dockerfile/run.sh new file mode 100644 index 000000000..5382ea94c --- /dev/null +++ b/tests/fixtures/complexity/Dockerfile/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "running $APP_MODE" diff --git a/tests/test_complexity.c b/tests/test_complexity.c index da019f527..775141cfc 100644 --- a/tests/test_complexity.c +++ b/tests/test_complexity.c @@ -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); diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index d4d2099f1..f2a69bf7d 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -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);