Commit Graph
230 Commits
Author SHA1 Message Date
Martin Vogel 8cef96dd88 Merge origin/main into fix/adr-reindex-race
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-31 16:38:41 +01:00
Martin Vogel b33a645e47 Merge origin/main into fix/permanent-daemon-watch
test_daemon_application.c: the new auto-watch test passes false for the approve_broad parameter that #2017 added to cbm_workspace_grant_add, the same value the existing callers use; the merged tree did not compile without it.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-30 22:37:40 +01:00
Martin Vogel 359bd2f3ff Merge origin/main into fix/adr-reindex-race
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-30 22:29:41 +01:00
Martin Vogel 7abfb0d847 Merge branch 'main' into feat/claude-code-plugin-marketplace 2026-10-29 07:07:36 +01:00
Martin Vogel ed17097e34 Merge origin/main into fix/permanent-daemon-watch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-29 04:07:49 +01:00
Martin Vogel 9f707646a8 Merge origin/main into fix/adr-reindex-race
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-29 03:29:37 +01:00
Martin Vogel 8fae39e294 Merge origin/main into fix/adr-reindex-race
tests/test_pipeline.c: main and this branch added tests at the same spot;
both are kept unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-28 18:29:56 +01:00
Martin Vogel 97f8aef644 Merge branch 'main' into feat/claude-code-plugin-marketplace 2026-10-28 18:16:38 +01:00
Martin Vogel 545c1bd519 Merge origin/main into fix/permanent-daemon-watch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-28 18:15:03 +01:00
Martin Vogel 5d97172b89 Merge origin/main into docs/windows-troubleshooting
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-28 18:12:53 +01:00
Martin Vogel 70d6d5017c docs(watcher): describe the watch lifetime under daemon start
A daemon started with `daemon start` now keeps a project watched after its
last owning MCP session closes, until the daemon stops, the project is
deleted, its directory disappears, or auto_watch is set to false. README
(the watcher paragraph and the index_repository row), the watcher_enabled
row of docs/CONFIGURATION.md and the comment above the index_status watch
provider still said every watch ends with its last session.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-28 00:51:48 +01:00
Martin Vogel 57d641bfeb Merge branch 'main' into feat/claude-code-plugin-marketplace 2026-10-27 19:18:22 +01:00
Martin Vogel 04cb937236 Merge origin/main into fix/adr-reindex-race
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-27 19:17:15 +01:00
Martin Vogel 402617555c feat(cli): note when a Claude Code session keeps searching without the graph
The search hook gives each Grep, Glob or Bash search the matching graph
symbols, but a session can keep searching text for questions the graph answers
in one call (#509). Once a session has run three searches with no
codebase-memory-mcp tool call, the next augmentation now adds one factual line
saying so. The next note comes after six searches, then twelve, and any graph
tool call resets the count.

- State: a per-session counter in the process that runs hook requests (the
  daemon), keyed by the payload's session_id, in a bounded 64-slot table that
  evicts the least recently used session, behind a spin lock. Only searches
  that reach the graph lookup count. A search without an augmentation still
  counts, and the note waits for the next augmentation.
- Reset: Claude's managed PostToolUse entry now matches
  "Read|mcp__.*codebase-memory-mcp__.*", covering plain and plugin server
  names. The previous Read-only entry, as installs up to v0.12 wrote it, is
  recognized as owned, so install replaces it in place and uninstall removes
  either.
- Scope: Claude Code only. Codex sends the same event payload with a turn_id
  but has no reset hook, so its sessions are not tracked, and payloads without
  a session_id are not tracked either.
- Cost: a graph tool call now runs one hook round-trip to the daemon for the
  reset, about 130 ms on a local build.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-27 19:04:47 +01:00
Martin Vogel 8cfefa6d38 Merge origin/main into fix/adr-reindex-race
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-25 20:14:53 +01:00
Martin Vogel d0937940a9 docs(readme): name the Codex PreToolUse and Copilot PostToolUse search hooks
The Codex CLI row still listed only SessionStart and SubagentStart after the
PreToolUse Bash hook landed (#2719), and the hook section described only
Claude Code's search context. The row now names the Codex search hook, and the
section says how Codex, Copilot CLI and VS Code's Copilot runtime get search
context.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-25 20:03:40 +01:00
Martin Vogel a18b4a4574 feat(cli): give Copilot searches graph context through a PostToolUse hook
Copilot CLI's preToolUse output can only decide permissions, and its command
hooks fail closed on errors, so the graph context that Claude Code and Codex
searches get could not ride a pre-tool hook there. postToolUse
additionalContext is appended to the tool result the model sees on the same
turn (github/docs content/copilot/reference/hooks-reference.md). VS Code's
agent mode shares the Copilot harness and reads the same ~/.copilot/hooks
manifest.

- Manifest: $COPILOT_HOME/hooks/codebase-memory-mcp.json adds a PascalCase
  PostToolUse entry, matcher "Bash|Grep|Glob|bash|powershell|grep|rg|glob",
  running hook-augment --event PostToolUse --dialect copilot. The previous
  lifecycle-only manifest, as installs up to v0.12 wrote it, stays recognized
  as owned: install upgrades it in place and uninstall removes either
  generation. A foreign manifest still fails closed.
- hook-augment: the Copilot dialect reads either payload form
  (tool_name/tool_input or toolName/toolArgs), maps runtime tool names to the
  Claude names (bash/powershell to Bash, grep/rg to Grep, glob to Glob),
  parses arguments passed as a JSON string, and answers with top-level
  additionalContext.
- Fast path: Copilot's forced PostToolUse goes through the no-op gate before
  executable-identity hashing. The gate also skips tools the augmenter does
  not read, because VS Code's local hook parser ignores matchers, so a
  non-search shell call or an edit exits in about 20 ms instead of paying the
  full round-trip of about 130 ms.
- The context header no longer says the search results are "below": after a
  post-tool hook they are above it.
- Tests: the three Copilot foreign-manifest tests now create
  .copilot/mcp-config.json. Without it Copilot CLI was never detected, so
  install and uninstall never reached the manifest, and the tests also passed
  with the ownership checks removed.

Refs #509

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-25 19:50:35 +01:00
0xb007ab1eandClaude Opus 4.8 001fbe269a fix(adr): store ADRs in a per-project sidecar DB, untouched by reindex
Reworks the ADR-loss-across-reindex fix to Alternative 1 (per the PR review):
remove the race rather than narrow it. ADRs live in a per-project sidecar SQLite
DB ("<graph_db>.adr.db"), separate from the graph DB a reindex rebuilds or
deletes, so there is no capture, re-read, re-apply, or lock — and no sleeps,
polling, or timeout deciding an outcome.

store.c:
- ADR writes (store/delete/update_sections) use a cached read-write handle opened
  SQLITE_OPEN_READWRITE|CREATE and configured like the graph store (PRAGMA
  busy_timeout=10000 + journal_mode=WAL) so cross-process access waits rather
  than failing SQLITE_BUSY. A :memory: store keeps ADRs in the graph DB.
- Reads never write: cbm_store_adr_get uses the cached RW handle if open, else
  opens the sidecar SQLITE_OPEN_READONLY (with the same busy timeout) when the
  file exists, else reads the legacy graph row without creating anything. When
  the sidecar exists but is NOT marked migrated (half-created), it falls back to
  the legacy graph row so a partial sidecar never hides an ADR.
- One-time migration is atomic: adr_copy_legacy_once wraps the marker check,
  legacy copy, and marker insert in a single BEGIN IMMEDIATE...COMMIT
  (re-checking the marker under the lock; ROLLBACK->ERR on failure). New public
  cbm_store_adr_migrate_once. Sidecar path via cbm_path_for_file_api (Windows
  long-path). Dropped the write-only adr_db_path field.

pipeline.c: migrate a legacy ADR into the sidecar BEFORE the format-change and
forced-full routes unlink the old generation; preserve-and-abort on failure, and
do NOT fail open — a query-open returning NULL preserves unless the final DB
genuinely does not exist. Migration targets the FINAL path (staging suffix
stripped). The early-capture/re-apply machinery and its test hook are gone.

artifact.c: the exported artifact carries the ADR — snapshot_inject_adr always
DELETEs project_summaries first, then injects only when an ADR is present, so a
deleted ADR never resurrects through a lingering legacy row. Import restores the
ADR into the destination sidecar via cbm_store_adr_restore_from_graph_if_absent:
it copies the artifact's ADR (INSERT OR IGNORE — never clobbers a local one)
regardless of the migrated marker, so a teammate who already indexed the project
(marked sidecar, no ADR) still receives the shared decision record. store.c
factors the row copy into adr_copy_rows, shared by the one-time legacy migration
and the ungated restore.

mcp.c: delete_project removes "<db>.adr.db" (+ -wal/-shm/-journal) only after the
graph unlink succeeds; is_project_db_file skips *.adr.db. cli.c: exclude
*.adr.db from index listing/counting. README + memory-core baselines updated.

Tests (file-based): ADR written mid-reindex survives on BOTH the full and delta
routes (route-pinned FORCED_FULL / CLOSURE_REPAIR); a legacy row migrates before
a forced-full/format reindex with no prior read; migration failure (a directory
at <db>.adr.db) preserves-and-aborts with the old generation intact;
delete_project via the MCP handler removes the sidecar + journals; the artifact
export->import round trip keeps the ADR; a deleted ADR does not resurrect on
import; an import does not clobber a local ADR; an import into an already-indexed
destination (marked sidecar, no ADR) restores the artifact's ADR; a half-created
sidecar falls back to the legacy row. The import tests remove the destination
graph DB (keeping the sidecar + marker) before importing, matching production
(try_artifact_bootstrap imports only when the graph DB is missing) so the rename
succeeds on Windows, not only on POSIX.

Closes #516

Signed-off-by: 0xb007ab1e <134006168+0xb007ab1e@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-22 04:45:43 -05:00
wargloom 0325d6a3fa fix: watch explicit MCP index targets
Signed-off-by: wargloom <wargloom@gmail.com>
2026-10-21 14:03:07 +03:00
Martin VogelandKody 4c09f7c70c feat(plugin): Claude Code plugin marketplace, discovery slice
Make this repository a Claude Code plugin marketplace so users can find
and add the server from inside Claude Code:

  claude plugin marketplace add DeusData/codebase-memory-mcp
  claude plugin install codebase-memory@codebase-memory

This is slice 1 of #1188: discovery only. The plugin registers the MCP
server and nothing else; the skill, the graph agents and the context
hooks still come from `codebase-memory-mcp install`.

- .claude-plugin/marketplace.json: one plugin, sourced from ./plugin.
- plugin/.claude-plugin/plugin.json: version pinned to the current
  release. Without a pinned version every commit to main would be a new
  plugin version for installed users.
- plugin/.mcp.json: stdio server `npx -y codebase-memory-mcp@0.12.1`.
  The pin keeps main from advertising a server version npm does not
  have; the npm package downloads the release binary on first start and
  verifies it against the release checksums.
- .gitignore: the root .mcp.json stays ignored; plugin/.mcp.json is
  published.
- README: install section, and "use one or the other": with both the
  plugin and `install`, Claude Code runs the server twice under two tool
  namespaces.
- tests/test_version_metadata_contract.sh: both plugin files are
  release-tracking surfaces; a new claim form reads the npm package spec
  (`codebase-memory-mcp@X.Y.Z`), and the unregistered-surface sweep now
  covers plugin/ and .claude-plugin/. The post-release sync that bumps
  server.json must bump these pins too, or this contract goes red.

No language count is stated anywhere in the plugin metadata, so the
language-count contract has nothing new to track.

Refs #1188

Co-authored-by: Kody <kaidi.shi.1121@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-21 00:34:29 +02:00
Martin Vogel 13c38feeff docs: refresh Lens recordings and correct README claims
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-20 22:39:10 +02:00
Martin Vogel e81b6599a8 docs: center the MCP server while keeping Lens visuals up front
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-20 20:02:47 +02:00
Martin Vogel 912dae61c9 docs: add Lens walkthroughs and restore GitHub star badge
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-20 19:57:57 +02:00
Martin Vogel e77b494b3e docs: improve site discovery and invite the Discord community
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-20 19:53:16 +02:00
Martin Vogel dd357a7c97 docs: refresh project messaging and Lens website
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-20 19:08:27 +02:00
Oscar Madera d1ab2426ee Merge branch 'main' into docs/windows-troubleshooting 2026-10-20 08:40:03 -05:00
Oscarbol09 a933d5318c docs(troubleshooting): add Windows troubleshooting guide
Centralize recurring Windows-specific troubleshooting entries in a dedicated document linked from README.md. Each entry includes confirmed symptoms, affected version ranges, current workarounds, and canonical tracking issue references, updated to reflect v0.10.x and v0.11.0 status.

Refs #1170

Signed-off-by: Oscarbol09 <omaderabolano@correo.unicordoba.edu.co>
2026-10-19 18:45:28 -05:00
Martin Vogel 5cdbe9b0b6 fix(deploy): taint Helm changes of and/or, later conditions, sortAlpha, parse errors
Review round 4 of #2677 (user decisions: R4-1..R4-4, R4-6, R4-7, R4-9;
R4-5 measured and left out). Every case ends in an UNKNOWN image, never a
wrong one.

Changes not run exactly (helm_template.c):
- A deep merge into a values map is not exact when a change was made at or
  below its path (a set storing a map there): every values map a set or a
  merge changes keeps its path, and such a merge leaves the images after it
  UNKNOWN.
- and / or at an operand whose truth is not known -- UNKNOWN, or a map a
  change made unknown -- leave the images after them UNKNOWN when an
  operand after it may change maps.
- In an if / with an UNKNOWN condition skips, the else-if / else-with
  conditions after it are looked at too: a change there taints, an
  assignment there makes its variable UNKNOWN.
- sortAlpha sorts a []string in place: it counts as a change (a list is
  never poisoned, so it taints). No other Sprig list function changes its
  argument in place.
- A template, a define body or a scanned template file the grammar does not
  parse may hide a change: its images are UNKNOWN.

Work (helm_template.c):
- A capture's keys leave the key set one by one; the set is no longer
  cleared by walking its whole capacity after each capture.
- Template files are found by a hash of their path, and an include of a
  name that is no define is answered once per render.

Known limit, documented (README): each template file renders on its own,
while Helm renders a chart's files with one shared .Values. Running them in
Helm's order is the planned follow-up; the coarse rule across files cost 83
more correct images on the 20 infra corpora and was left out.

On the 20 infra corpora: Image edges identical to round 4 (0 lost, 0
added), no budget trip.

Tests: R4-1..R4-4 and R4-9 fail on 3401def5 by their assertions (each sub
case too); R4-6 and R4-7 are correctness controls, their cost measured
outside the suite.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-19 23:42:36 +02:00
Martin Vogel 5966b7f081 Break-glass merge PR #2664 2026-10-16 18:16:17 +02:00
Martin Vogel 10c7fc3206 fix(pkg): remove the archive size caps from the npm, PyPI and Go wrappers
v0.12.0 could not be installed through npm, pnpm, pip or the Go wrapper on
any platform (#2657): each wrapper refused an archive member above 256 MiB,
and every v0.12.0 binary is 331-335 MB. The v0.11.0 binaries were already
about 300 MB, so the Go wrapper, which has had the cap since v0.10.0, was
broken before; npm and PyPI gained it in v0.12.0. The archive is verified
against the release's checksums.txt before it is unpacked, so a byte cap
only ever refused what we ship.

- npm, PyPI, Go: delete every byte-size cap (compressed archive, per member,
  aggregate, checksum manifest, download size). The checksum, the exact
  member set, path and link refusal, the member count, HTTPS and the
  --version check stay.
- All three honour CBM_DOWNLOAD_URL like install.sh, for testing: https, or
  plain http to an exact loopback authority with no proxy and no redirect.
- Smoke: scripts/smoke-wrappers.sh installs the three wrappers from the
  checkout against the release fixture and runs the binary each one
  installed. smoke-test.sh Phase 13w runs it; SMOKE_REQUIRE_WRAPPERS=1 makes
  it required on the PR smoke lanes and in the release smoke, which gates
  publishing.

Other install paths found broken while checking every channel:
- Homebrew: the formula was still at 0.10.3 and installed the dynamic Linux
  build, which needs glibc 2.38 (Ubuntu 22.04, Debian 12 and RHEL 9 lack it).
  It now installs 0.12.0 with the static -portable Linux assets and tracks
  the newest release in the version contract like the other surfaces.
- Go: `go install .../pkg/go/cmd/codebase-memory-mcp@vX.Y.Z` never worked
  (no pkg/go/v* tags, so Go fetched the whole repository, over its 500 MB
  module limit), and @latest built main's wrapper, whose version constant
  lags each release. The release now also tags pkg/go/vX.Y.Z, and the
  wrapper installs its own module version (the constant only for local and
  untagged builds).
- npm: a stable release also moves the `next` dist-tag to itself; `next`
  still pointed to 0.9.1-rc.1.
- Docs: winget and Chocolatey were listed as install channels but are not
  published; they are removed until they are. Windows arm64 ships and is
  now listed.

Fixes #2657.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 18:09:06 +02:00
Martin Vogel c040476634 docs(readme): point the main CI badge at the release workflow
The badge tracked dry-run.yml, which runs only when dispatched; its last
runs on main were cancelled or failed during the v0.12.0 preparation, so it
read "failing" although main passed the full matrix in the v0.12.0 release
run. release.yml runs the same full matrix on main for every release.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 01:47:23 +02:00
Martin Vogel eca1a004b4 feat(configlink): TOML and INI keys link by exact name in their component
A config key linked to every code symbol whose name contained it. In
YAML, JSON and HCL files that guess was retired in WP0.9; TOML and INI
files kept it, and in 17 infrastructure repositories every one of their
72 key_symbol edges was a coincidence: `.gitleaks.toml useDefault` linked
to TestConfig_UseDefault and three other symbols, a toml-merge tool's test
data `config_path` to 46 `*ConfigPath` constants all over deckhouse,
`grafana.ini admin_user` to user-cluster constants, a Rust Cargo.toml
dependency to other services' go.mod requirements.

A .toml, .ini, .cfg or .conf key now links like an infrastructure key:
by exact name (camelCase and snake_case still normalize to one key) and
only to code inside the config file's component, its directory or the
one around a config/, conf/, settings/ or etc/ directory. .env and
.properties keys keep substring matching; the tests that covered it on a
TOML key now use a .properties key.

Measured on the 17 repositories: key_symbol 95 -> 23, the 23 being the
infrastructure edges WP0.9 reviewed, the 72 removed all TOML/INI and all
coincidences, no other edge or node changed; deterministic over three
runs; the incremental re-index equals a full index for INI, TOML, code
and tfvars edits.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 22:09:37 +02:00
Martin Vogel dec56c9c69 feat(infra): an external chart is keyed by the URL that publishes it
An external Chart was <project>.__helm_dep__.<name>: keyed by its name,
inside one project. Two repositories naming the same published chart held
two unrelated nodes, and a project naming two charts of one name from two
publishers (kubermatic's cert-manager from charts.jetstack.io and from its
own mirror) would have merged them into one.

An external Chart whose repository URL is stated is now
__chart__<repository>/<name>, the same QN in every project, the URL in a
`repository` property: scheme and host lower-cased, a trailing slash and
`.git` dropped. A Chart.yaml dependency, a kustomize helmCharts entry, a
Flux HelmRelease chartRef and a Terraform helm_release of an oci:// chart
that name one published chart meet in one node. A chart reference (a Flux
OCIRepository url, `chart = "oci://..."`) is cut to its repository; a
repository URL never is, so repository oci://public.ecr.aws/karpenter
with chart karpenter stays oci://public.ecr.aws/karpenter/karpenter. A
repository that is no URL (an @alias, no scheme, file://, credentials in
the authority, an unexpanded ${...}) or that the pass does not read (a
Flux HelmRepository source, a Terraform `repository =`, a shell
`repo/name`) keeps the per-project node: no identity is claimed for what
was not stated. A query or fragment (`?token=...`) takes the per-project
node too, so a token never becomes part of a QN or a property.

One predicate now tells an external chart from a chart of the repository
where Terraform, shell and the Go installer index the chart directories:
an external chart a Chart.yaml depends on carries that file's path, and
must never stand in for the chart the directory declares.

Measured on 17 infrastructure repositories against an independent
reading: 141 of 141 edges into external charts reach the identity their
stated repository gives (114 by URL, 27 per project). The only
differences to the previous index are the 79 external Chart nodes that
moved to their URL identity, each with its repository property, and the
114 edges into them, re-pointed with type, source and properties
unchanged. Every chart gate (Helm includes, helmCharts, Terraform, the Go
installer, shell, CI, GitOps deploys) reads the same as before; three
runs are identical; the incremental re-index equals a full index for a
shared chart's owning file edited and deleted, a URL respelled, a
dependency moved to another publisher and to an alias, a chartRef URL
edit and a new Terraform oci:// release. The index rebuilds once on
upgrade with the other version 9 changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 21:06:48 +02:00
Martin Vogel 150851cc05 feat(infra): an Ansible kube / k8s task DEPLOYS the template it applies
kubespray applies its cluster add-ons with its own `kube` module: a
task runs kubectl apply on a manifest the play rendered from a template
a few tasks earlier. Both tasks were Functions and the template a File,
with nothing between them: no query reached the manifest a task applies.

A kube (kubespray's library module) or k8s (kubernetes.core /
community.kubernetes) task now DEPLOYS {via: ansible, module, line} the
repository file its play rendered the applied manifest from: a template
task's src (the role's templates/, else the task file's directory) or a
copy task's (its files/; never a remote_src copy, which reads the host).
The applied filename is traced through the play: a renderer whose dest
is the very string; {{ item.item.K }} over "{{ R.results }}" -> each
item of the renderer registering R; {{ item.K }} -> each of the task's
own items through the renderer of that dest; a literal filename a looped
renderer renders to; a k8s template: argument. A loop list is a literal,
a set_fact of the file, or a list of the role's defaults / vars main
file. Templates are found before copies to the same dest. remote_src is
read the way Ansible's boolean() reads it (y / yes / on / 1 / true / t,
any case): `remote_src: true` is a YAML boolean, not a string, and a
string-only read had kept every remote copy as a renderer.

The lists in a role's defaults / vars reach past the task file, so a
change to one takes the full rebuild (closure_ansible_decline); without
it, a list that names another template kept the edge to the old one. A
file holding Ansible tasks that the repair would re-walk as a dependent
-- a template it applies was deleted -- also takes the full rebuild
(closure_ansible_dependent_decline): the repair walks it without its
role's defaults, handlers and other task files, and drew none of its
list-fed edges.

Measured on 17 infrastructure repositories: 164 of 164 applied files
(52 of kubespray's 62 kube / k8s tasks, 158 templates) against an
independent reading, all in kubespray; nothing in the other 16; the rest
of the graph unchanged (+164 DEPLOYS only); deterministic over three
runs; the incremental re-index equals a full index for a template body
edit, a deleted template (with and without a same-named fallback beside
the task file), a task list edit, a copied manifest edit, a defaults
list edit and a deleted vars file. Of the 10 tasks left out, 4 act on
an object by name, one builds its definition with a template lookup,
one applies a remote_src copy, and 4 name their file through a filter, a
variable or two joined results lists.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 20:46:38 +02:00
Martin Vogel bff5e01e29 Merge branch 'main' into feat/infra-followup
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:59:10 +02:00
Martin Vogel 701d95e061 feat(infra): Ansible tasks are Functions, a notify CALLS its handlers
kubespray's 1,425 tasks and 65 handlers were text inside YAML files: a
search for "Containerd | restart containerd" found no node, and nothing
said which task triggers which handler.

Each task and handler of an Ansible file is now a Function, named by its
`name:` (else its module), {source: ansible, kind, module}, which its file
DEFINES -- a playbook's pre_tasks / tasks / post_tasks / handlers, a role's
tasks/ and handlers/ files, and the files those include; a block's block /
rescue / always tasks, never the block itself. A notify CALLS {via:
notify, line} every handler of that name or listen topic in the task's own
role (the directory before its tasks/, so nested roles stay apart), else
in its file.

A top-level YAML list is not Ansible by itself: JSON patches, image maps
and service lists are lists too, and would have made some 3,000 false
tasks in five other repositories. A file counts when it is a playbook, a
role's tasks or handlers file, or included by one (to a fixpoint).
Quoted names have their YAML escapes decoded. A notify joins two files,
so a change to Ansible content takes the full rebuild
(closure_ansible_decline); without it, renaming a listen topic or adding
a notifying task left the incremental graph different from a full index.

Measured on 17 infrastructure repositories: 1,490 of 1,490 tasks and
handlers (file, line, kind, name, module) and 276 of 276 notify edges
against an independent reading, all in kubespray; nothing in the other
16; the P7.1 include edges unchanged (211 of 211); deterministic over
three runs; the incremental re-index equals a full index for handler and
task file edits, a deleted handler file and a playbook edit. The only
other change: as every function joins the similarity corpus, four
SEMANTICALLY_RELATED scores in kubespray's Python module moved and one
pair was added.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:43:44 +02:00
Martin Vogel 166962619d feat(infra): an object a kustomization renames has a variant Resource
Cluster API deploys its manager as `capi-controller-manager` in
`capi-system`: config/default/kustomization.yaml puts `namePrefix: capi-`
and `namespace: capi-system` on the `controller-manager` of
config/manager/manager.yaml. The graph only had the declared name, so the
name the cluster runs -- the one docs, scripts and tests quote -- matched
no node.

A kustomization with a namePrefix or nameSuffix now gives every object of
its build a variant: a Resource named with the deployed name, in the
namespace the build gives it, `ecosystem: kustomize`, which the
kustomization's File DEFINES and which OVERRIDEs {via: namePrefix |
nameSuffix, kustomization, line} the object. Prefixes of nested
kustomizations compose outermost first, suffixes innermost first, and the
outermost namespace wins. CustomResourceDefinitions, Namespaces and
apiregistration APIServices keep their names, as kustomize's own
transformers skip them. Variants are minted after the Kubernetes joins:
inside a build, references use the declared names.

Measured on 17 infrastructure repositories: 56 of 56 renamed objects in
the 6 renaming kustomizations against an independent reading, with
deployed name, namespace and base object; nothing else changed (56 nodes,
112 edges added, no property of an existing node); deterministic over
three runs; the incremental re-index equals a full index when a prefix
changes, goes, nests, or a renamed object is renamed or deleted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:26:11 +02:00
Martin Vogel b1d93cc03e feat(infra): Kubernetes objects declared in Terraform are Resources
A `kubernetes_*` resource in a .tf file -- eks-blueprints' network
policies and storage classes, k8s.io's metrics-server stack -- declares
a cluster object, but the graph only had the Terraform block, named by
its Terraform address: no node carried the object's kind and name.

A kubernetes_<type> resource (also _vN) whose metadata name is a literal
is now a Resource of the same identity scheme as a manifest's object:
kind from the resource type, or a kubernetes_manifest's own apiVersion
and kind; name and namespace from the object's own metadata, never from
spec; `ecosystem: terraform` on its properties. The .tf file DEFINES it
and the block DEPLOYS it {via: terraform, line}. The objects are minted
before the Kubernetes joins, so a manifest of the same bundle references
them as it does a manifest's object (an Ingress maps to the Service). A
computed name, and a type that acts on an object declared elsewhere
(kubernetes_annotations, kubernetes_labels, ...), make none. Labels are
not read, so a Service selector does not reach a Terraform workload.

Measured on 17 infrastructure repositories: 21 of 21 literal-named
objects against an independent reading, with group, kind, name and
namespace; nothing else changed (21 nodes, 42 edges added, no property
of an existing node); deterministic over three runs; the incremental
re-index equals a full index when an object is renamed, gains a
namespace, or its file is added or deleted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:26:01 +02:00
Martin Vogel cdfdb8f877 feat(infra): a .tfvars file sets an ancestor module's variables, inferred
A .tfvars file in a directory without Terraform -- kubespray's
sample-inventory/cluster.tfvars, eks-blueprints' spokes/workspaces/*.tfvars --
is applied to a module further up with a -var-file the repository does
not state, and linked nothing.

When the file's own directory declares none of its names, it now sets
the variables of the nearest ancestor module that does: each assignment
to a declared variable OVERRIDEs {via: tfvars, inferred: true, line} the
`variable` block, from the file's Variable. A file whose own directory
declares one of its names keeps the exact edge, without `inferred`.

Measured on 17 infrastructure repositories: 238 of 238 assignments --
124 to their own directory's module, 114 inferred to an ancestor's --
against an independent reading, with the inferred flag; nothing else
changed; deterministic over three runs; the incremental re-index equals
a full index when an ancestor-linked file changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:25:50 +02:00
Martin Vogel ce0ed0e3b9 feat(infra): an Ansible playbook or task file IMPORTS what it includes
Ansible's composition produced no structure: kubespray's 40 playbooks,
roles and task files, tied together by 217 import_playbook /
include_tasks / import_tasks / include_role / import_role / roles:
directives, were unconnected YAML files.

A playbook or task file -- a YAML file whose top level is a sequence;
manifests, values, CI and compose files are mappings -- now IMPORTS
{local_name: <directive>, via: ansible, line} what each directive names:
import_playbook a playbook from its directory; include_tasks /
import_tasks / include a task file from its directory, else from its
role's tasks/; roles: items and include_role / import_role a role's
Folder, roles/<name> at or above its directory (a role by "./" / "../"
path from the directory). The ansible.builtin. prefix is understood; a
templated target links nothing.

Measured on 17 infrastructure repositories: 211 of 211 composition pairs
of kubespray against an independent reading, nothing else in any
corpus; deterministic over three runs; the incremental re-index equals
a full index in 4 scenarios (an included file edited or deleted, an
include pointed elsewhere, a playbook edited).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:25:40 +02:00
Martin Vogel fb73491ee8 feat(infra): a Terragrunt unit IMPORTS what it includes and reads
A Terragrunt unit (terragrunt.hcl) composes itself from other files --
`include` blocks, read_terragrunt_config calls, dependencies on other
units -- and the graph held none of it: its blocks only met same-named
blocks elsewhere through the generic name matcher.

Every unit is now evaluated in its own context, the way Terragrunt
evaluates it (also the files it includes): an include IMPORTS
{local_name, via: terragrunt, line} the file its path names; a
read_terragrunt_config IMPORTS {via: terragrunt, read, line} the file it
names -- from the block holding the call or, for a call in an included
file, from the unit's include block that brings that file in, `read`
naming the file holding the call; a dependency DEPENDS_ON the other
unit's terragrunt.hcl; a local terraform source IMPORTS its Folder.
Paths may be string templates of find_in_parent_folders (searched from
the unit's parent directory upward), dirname and get_terragrunt_dir;
anything else is unknown and links nothing. A run holding any .hcl file
re-links every unit (a .hcl change takes the full rebuild since the
previous commit).

Measured on 17 infrastructure repositories: 30 of 30 composition pairs
of the 6 units of the Terragrunt repository against an independent
evaluator, nothing else; deterministic over three runs; the incremental
re-index equals a full index in 5 scenarios (a read file edited or
deleted, the included root file edited or reading another file, a unit
edited).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 06:58:15 +02:00
Martin Vogel 949e875337 feat(infra): a .tfvars assignment OVERRIDEs the variable of its module
A .tfvars file's keys were Variable nodes connected to nothing: nothing
said which variable `region = "eu"` sets.

An assignment to a variable the module of the file's own directory
declares now OVERRIDEs {via: tfvars, line} that `variable` block, from
the file's Variable (child to base, as a values or patch overlay). A
.tfvars file elsewhere is applied by a -var-file the repository does not
state, and links nothing. A run touching a .tfvars file re-resolves like
one touching a .tf file.

Measured on 17 infrastructure repositories: 124 of 124 assignments of
the 19 files that set variables of their own directory, nothing else;
deterministic over three runs; the incremental re-index equals a full
index in 4 scenarios (a value edited, the variables file edited, a
.tfvars file deleted).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 05:26:59 +02:00
Martin Vogel d1bf070b78 feat(infra): a Terraform module call IMPORTS its source; module.X.Y reads its output
A module call with a local source -- `module "net" { source =
"./modules/net" }` -- linked nothing: 379 such calls on the 7 Terraform
repositories of 17 infrastructure corpora, and every `module.net.id`
reached at most the calling block, never the output it reads.

Such a call now IMPORTS {local_name, via: terraform, line} the Folder of
its source directory (the repository root: the Project), when that
directory lies inside the repository and holds .tf / .tofu files; a
registry, git or http source, or a path not starting with ./ or ../,
makes none. `module.net.id` -- also `module.net[0].id`, `module.net.0.id`
and `module.net[*].id` -- USAGEs {ref: output} the `output "id"` of that
directory, beside the USAGE of `module.net`.

Output reads cross directories, so a run touching any .tf / .tofu file
now resolves every module directory of the repository: an unchanged
caller keeps its reads when the callee file is re-parsed. A full index
takes as long as before; an incremental re-index touching a .tf file
pays for parsing them all (+0.24 s on a 2,000-file repository).

Measured: 349 of the 379 calls name a directory that exists in the
repository and IMPORT it, nothing else (the 30 others name paths a test
harness creates or that are absent); 644 of 644 output reads (holding
block to output) against an independent reading; the reference pairs
of the module resolver stay exact; only edges are added; deterministic
over three runs; the incremental re-index equals a full index in 6
scenarios (an output's value edited in the callee, its outputs file
deleted, a source moved, a widely called module edited, the root
module's caller edited).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 04:54:15 +02:00
Martin Vogel a9d7a96227 feat(infra): a Terraform reference USAGEs the block of its module declaring it
Terraform references were resolved by the generic name matcher: by their
last name segment, across the whole repository. `format()` CALLS a
variable named format of another module, `local.x` meets a resource named
local, `.id` an output id anywhere. Measured on the 7 Terraform
repositories of 17 infrastructure corpora: 12,437 edges, recall 0.63 and
precision 0.39 against an independent reading of the references, and not
one of them left the block holding its reference.

A reference -- var.X, local.X, module.X, data.T.N, T.N -- now USAGEs
{ref, via: terraform, line}, from the top-level block holding it, the
block declaring it in the same module: the .tf / .tofu files of its
directory (a locals entry: its locals block); an entry of the block's
depends_on list DEPENDS_ON the object instead. The generic matcher makes
no edge for a .tf / .tofu file any more; a name only mentioned in string
text is no reference, nor is a block's reference to itself. A changed
file re-resolves its whole directory, so an unchanged file's reference
meets the declaration a change adds and loses the one it removes.

A bare block label (`variable region {}`, valid HCL) now names its block
like a quoted one; such blocks used to share the keyword's name and QN.

Measured: 7,771 of 7,771 file-level and 13,397 of 13,397 block-level
reference pairs (edge type included) and nothing else; the 10 other
corpora are unchanged; deterministic over three runs; the incremental
re-index equals a full index in 7 scenarios (rename a variable, a module,
a locals entry; delete the variables file; add a file; add a declaration
an unchanged file references; quote a bare label); index time unchanged
(within 1 % on the four largest Terraform corpora).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 04:08:22 +02:00
Martin Vogel e58c5ba730 feat(infra): a Resource keeps its labels; a patch target selects by them
A Kubernetes object's metadata.labels were not in the graph: neither a
search for an object by label nor a kustomize patch whose target selects
by label could find anything.

A Resource now keeps its labels as `labels: "k=v,k2=v2"` (sorted; a
templated label, or one a selector cannot name, is left out), and a
patch target's labelSelector meets the objects of its build carrying its
terms -- `k=v` / `k==v`, `k!=v`, `k`, `!k`; a set-based term names no key
and meets nothing. A target with only a selector selects among every
kind; an annotationSelector is not evaluated and names nothing.

Measured on 17 infrastructure repositories: 1,956 Resources gain labels
and nothing else; an independent reading of metadata.labels agrees on
every one of the 1,804 objects both read (152 more are Go-templated,
which only the graph reads, through its skeleton). The 4 label-selector
patch pairs now resolve: kustomize patches 130 of 130.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 02:27:54 +02:00
Martin Vogel 35833e1f91 feat(infra): a kustomization DEPLOYS the charts it inflates
A kustomization's `helmCharts` inflate Helm charts into its build -- the
fourth way a repository puts a chart on a cluster, beside a Flux
HelmRelease, a Terraform helm_release and a helm command; the graph read
none of them.

Each entry now DEPLOYS from the kustomization -- its Kustomization
Resource, else its File: the unit a Flux or Argo deploy of it reaches -- to
the shared external Chart of that name, with {via: helmcharts, external,
chart, url, version, release, namespace, values, line}. A chart name with
a path names nothing; the values file inside the repository is an IMPORTS
(local_name: valuesFile) of the kustomization.

Measured on 17 infrastructure repositories: 20 edges, every one in an
independent reading of the kustomizations (20 entries, cluster-api 10,
k8s.io 10); no other node or edge changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 01:57:54 +02:00
Martin Vogel 947d3a1704 feat(infra): a kustomize patch OVERRIDEs the objects it changes
A kustomize overlay changes its base through patches; the graph read none of
the v5 `patches:` entries (path / patch / target mappings), so a patch file
showed up as a second copy of the object it patches, with no edge to it.

Each patch of a kustomization now OVERRIDEs, child -> base, the objects of
its build it changes, with {via: patch, kustomization, patch, line}:
- a patch file's documents: the object of the same group, kind and name
  (and namespace, when the document declares one);
- an inline patch (from the kustomization's File) or a JSON6902 operation
  file (from its own File): what its `target` selects -- kind, group,
  version, name, namespace; a label or annotation selector is not
  evaluated and names nothing;
- in the build of the kustomization stating it; a component's in the build
  of each kustomization including it.
A patch file is an IMPORTS (local_name: patches) of its kustomization and
never a member of its build: its documents select nothing, are selected by
nothing.

Resources now keep the version of their apiVersion (`version`), which the
identity leaves out and a target may name.

Each build indexes its own objects by name once: a lookup in the
repository-wide name index would visit every copy of a name for every
patch. The complexity fixture gains patch files (one of four documents), an
inline and a kind-only patch.

Measured on 17 infrastructure repositories: 126 edges, every one in an
independent reading of the 173 patches (122 resolvable in the
repositories); the 4 pairs without an edge use a label selector.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 01:24:09 +02:00
Martin Vogel 3b93db06b2 feat(infra): a script's curl / wget request HTTP_CALLS its Route
Shell scripts, Make recipes and CI steps download tools and call services
with curl and wget; the graph had no edge for any of them, so neither what
a repository fetches nor the scripts that call its own services were
visible.

Such a command now HTTP_CALLS the Route of each URL it states, with
{via: curl | wget, method, url_path, host, line}:
- the method from the flags: -X / --request / --method, a body (-d, -F,
  --data*, --json, --form*, wget --post-data / --post-file) POST, -T PUT,
  -I HEAD, -G back to GET;
- the URL from constant words (VER=1.2.3 ... v${VER}), unknown parts `{}`
  as in a Route path -- also in an unquoted word tree-sitter-bash splits
  around its expansions; a URL whose host is not literal, or the value of a
  proxy / referer option, is no request;
- never the user info, the query or the fragment: a token in a URL does not
  reach the graph;
- the Route: path-only (__route__POST__/v1/items) when the host is a
  Kubernetes Service of the repository in a service DNS form, where that
  service's handler meets it as a code client's request does; any other
  host -- a download, localhost, a compose service -- keeps it
  (__route__GET__https://dl.k8s.io/release/{}/bin/linux/amd64/kubectl,
  props {method, host, external}), and route_nodes makes no path-only twin
  of it.

Incremental indexing now drops a Route without a file that no edge touches
any more -- its last caller's file changed or went. A full index never
holds one; before, the Route of a deleted code client stayed behind too.

Measured on 17 infrastructure repositories: 92 edges, every one correct by
hand; an independent reading of the scripts and Makefiles finds 82
requests, 81 of them in the graph (the last is lost in a file
tree-sitter-bash cannot parse past `[ "$v" \< "1.33" ]`). No property of
any other node or edge changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 00:12:55 +02:00
Martin Vogel bd928b3eba feat(infra): CONNECTS_TO -- a service address in a container's environment
A workload's environment names the services it talks to
(PRODUCT_CATALOG_SERVICE_ADDR: productcatalogservice:3550, a URL, a
<svc>.<ns>.svc name); the graph had no edge for it, so the service topology
of an application was invisible.

A Kubernetes workload or a compose service now CONNECTS_TO the Service its
containers' environment addresses, with {via: env, key, container} -- never
the value:
- the address: the host of a URL or of host:port, a <svc>.<ns>.svc name in
  the value, or the whole value when the key names an address (*_ADDR,
  *_ADDRESS, *_HOST, *_URL, *_ENDPOINT, *_URI) and it is a bare DNS name; a
  templated value, localhost or an IP addresses nothing, and only service DNS
  forms count (svc, svc.ns, svc.ns.svc...), never another domain's host
  (cbm_env_address, shared by both sides);
- Kubernetes: a reference resolved like every other one -- a Service of the
  same bundle, in the named namespace or the object's own;
- compose: a service of the same file; a variable without a value takes the
  project's .env, whose values compose expands against the lines before them
  (CART_ADDR=cart:${CART_PORT}) -- the dotenv reader does that now too.

Consumers: trace_path's topology mode, the CLI's edge list and the README
(which also gains DEPLOYS and the env join of the previous commit).

Measured on 17 infrastructure repositories: 129 edges, every one correct by
hand; an independent reading of the values finds 133 pairs repo-wide, the 5
without an edge belong to another bundle or namespace.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-13 22:31:07 +02:00
Martin Vogel 01379f04ab Merge main into docs/readme-atlas-review
Resolves the four README conflicts against main 49424722: the README keeps
this branch's wording and style, and takes the content main added since
4d9592b5 in that style (list items "X: ..." instead of "X — ...", mid-
sentence dashes as semicolons):
- feature list and indexing pipeline: main's infrastructure-as-code
  bullets (container images, compose, Helm, Terraform, Service -> code);
- edge types: main's SPAWNS detail plus INFRA_MAPS, USAGE ref:image,
  DEPLOYS, DEPENDS_ON and CONFIGURES;
- trace_path: this branch's text (depth 1 to 15, tests excluded) plus
  main's topology / deploy / run modes.
Checked: the supported-agent README contract (45 surfaces, every agent),
the language-count contract (162 grammars), all 22 local links and images.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-13 21:59:25 +02:00