Commit Graph
4204 Commits
Author SHA1 Message Date
Martin Vogel bb650ca45a Merge branch 'main' into test/ts-lsp-allocation-failures 2026-10-18 00:17:58 +02:00
Martin Vogel 9dba7fe1af Merge pull request #2676 from DeusData/feat/smart-ci-live
ci(test-impact): make PR test selection narrow for real
2026-10-17 23:56:49 +02:00
Martin Vogel 48d33fadf6 test(ci): hold README pushes to the test legs in the selector history replay
README.md, the site files and pkg/npm/README.md are read by test_cli, so
a change to one now runs the test legs (T1-tests). The September replay
held 15 README pushes to the docs tier the analysis assigned and failed
every test leg's contract step. Those rows are now a documented
deviation, read-by-tests, which like makefile-by-name may only raise a
tier: 767 pushes as expected, 53 documented fail-safe deviations, all 22
real failures still caught.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 21:53:13 +02:00
Martin Vogel 57b557ced8 fix(test-impact): close the role table's fail-opens, follow literals through macros
The first commit of this branch is the Smart CI change as of the rebase
onto 9f85f104. A review of it found places where the engine could run
fewer tests than a change needs; each now runs everything or selects the
right tests, with a test that fails when its fix is reverted.

Engine:
- A `#define NAME "literal"` line counted as naming a role but selected
  nobody, so the tests that start the blocking-git probe through their
  marker macros would not have run. The scan now follows NAME, as a whole
  word, to the code that uses it, through chains of up to 4 macros; a
  define it cannot follow runs everything.
- main dispatches through the role table, so no edge leads from main to a
  handler. A table entry the engine cannot read, or whose handler is not
  exactly one function, was skipped; it now runs everything.
- In a runner file, only a test case names a role's literal. The runner's
  own helper that reads the marker no longer counts as a spawner, so a
  role only the runner names runs everything again.
- A handler reached first by a parse gap or by the spawn lift was never
  lifted. Role lifts and parse gaps now run to a fixpoint, before the
  runner check and again after the spawn lift.
- A reached sink that main calls (the blocking-git probe) counted as a way
  into main and hid the real one: #2644 now names the start-up path main
  -> cbm_index_supervisor_capture_build_fingerprint.
- No pointer past the end of a role line; the RUNNER_REACHED note clamps
  its prefix.

CI:
- pkg/npm/README.md, which test_cli reads, runs the test legs instead of
  the docs tier.
- The build rule lists the harness scripts the test legs run:
  run-test-wave.py, path-safety.sh, clean.sh.
- The revert guard rejects the ancestor's coverage when a diff fails.
- The vetoed selection is uploaded (test-selection-vetoed).

Docs: SMART_CI.md states the new fail-safe cases, the version that lacks
the new config keys, the selection base, and that the narrowed leg builds
with gcc while the coverage comes from clang.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 20:44:27 +02:00
Martin Vogel 7f6f567840 wip: Smart CI narrows PR CI for real (pre-rebase checkpoint)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 14:48:52 +02:00
Martin Vogel 9f85f1043a Break-glass merge PR #2674 2026-10-17 14:40:44 +02:00
Martin Vogel b1225e57ad chore(pkg): sync packaging metadata to v0.12.1
Every release surface of the version-metadata contract states the
newest tag: the Chocolatey nuspec and installer (checksum re-pinned to
the released windows-amd64 archive), the Scoop manifest (both Windows
archive hashes), the Homebrew formula (all four archive hashes; Linux
installs the static -portable builds), the Go installer, the Nix flake,
server.json, and the npm and PyPI wrappers. Every hash is taken from the
release's checksums.txt.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-17 14:38:59 +02:00
Martin Vogel a7158ac2f4 Break-glass merge PR #2669 v0.12.1 pkg/go/v0.12.1 2026-10-16 23:38:08 +02:00
Martin Vogel 6382fad7b3 fix(ci): give the wrapper smoke a Go toolchain and a deletable work tree
The v0.12.1 release smoke failed on every leg in Phase 13w, the
package-wrapper install gate added in #2664:

- macOS: the runner images have no Go on PATH, and
  SMOKE_REQUIRE_WRAPPERS=1 rightly fails without one. pr-smoke and the
  three release smoke jobs now set up Go from pkg/go/go.mod, with the
  same pinned step the wrapper test job uses.
- Windows: `mkdir -m 700` changes the mode in a second step, which MSYS2
  cannot do under the smoke profile's owner-only DACL. mktemp -d sets
  the mode at creation.
- Linux: the runner's older Go downloaded the 1.26.1 toolchain into the
  smoke profile's HOME. Go writes its module cache read-only, so the
  harness could not delete its profile afterwards. All Go state now stays
  in the wrapper's own work tree, and its cleanup restores write
  permission before removing it.

npm and PyPI passed on every leg that reached them; the gate itself is
unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 22:12:26 +02:00
Martin Vogel e7ea0aff8e Break-glass merge PR #2663 2026-10-16 18:16:50 +02:00
Martin Vogel 5966b7f081 Break-glass merge PR #2664 2026-10-16 18:16:17 +02:00
Martin Vogel ff829cbfee Merge pull request #2654 from DeusData/fix/image-unknown-registry
fix(docker): never invent a registry host for an image
2026-10-16 18:15:01 +02:00
Martin Vogel cfcf7528c4 Merge pull request #2653 from DeusData/docs/readme-main-ci-badge-release
docs(readme): point the main CI badge at the release workflow
2026-10-16 18:14:23 +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 851096ad37 feat(k8s): a Helm chart deploys its objects, images and values
A Chart node had one kind of outgoing edge, DEPENDS_ON for a dependency
its Chart.yaml declares. A chart without dependencies, most of them, was
an isolated leaf: nothing linked it to the objects its templates render,
to its values files, or to the images it runs. On 20 pinned infra
corpora, 238 of 260 Charts had no outgoing edge, while 2,626 objects sat
in chart templates/ directories.

- Chart -DEPLOYS {via: chart}-> each Resource of <chart>/templates/, for
  its nearest enclosing chart: a subchart's objects are the subchart's.
  The same edge a Flux Kustomization draws to what it applies.
- values.yaml / values-<x>.yaml beside Chart.yaml -CONFIGURES {strategy:
  helm_values}-> the Chart.
- Chart -DEPLOYS {via: chart, ref: image}-> each Image its objects name
  literally, read after pass_docker.c moved references onto component
  Images. A templated image names no Image, and no values default is read.

An incremental closure repair re-runs the K8s pass over its plan only, so
a values file or a template edited alone would lose the chart's edge; such
an edit now declines to the full rebuild, like a Chart.yaml edit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 18:07:31 +02:00
Martin Vogel 55b7916fba Merge pull request #2652 from DeusData/chore/sync-version-0.12.0
chore(pkg): sync packaging metadata to v0.12.0
2026-10-16 18:06:30 +02:00
Martin Vogel e99c15997b fix(docker): never invent a registry host for an image
An image reference that names no registry host was stored under an
invented one: `FROM nginx` became docker.io/library/nginx, `image:
acme/web` docker.io/acme/web, and a local `docker build -t myapp` or a
skaffold artifact `image: api` docker.io/library/myapp. docker.io is only
Docker's default. Podman and CRI-O resolve a short name through a search
list, mirrors rewrite it, and a local build tag may exist on no registry
at all. The graph stated a host the code never names.

An Image is now the repository as written, tag and digest dropped, with
a `registry` property: the host the reference names, else "unknown". A
stated host is kept as written; Docker Hub's spellings (docker.io,
index.docker.io, registry-1.docker.io) are docker.io, with library/ for
an official one-segment name. This holds for every source: FROM and
COPY --from, compose services, K8s containers, shell and skaffold build
tags, and the per-component Images of a repository built from several
Dockerfiles.

`FROM nginx` and `image: docker.io/library/nginx` are now two Image
nodes. Relating them would state the host again, so the graph does not.

The Image QN changes (__image__nginx instead of
__image__docker.io/library/nginx), so CBM_INDEX_FORMAT_VERSION moves to
3. Refreshed in place, an older index would give only touched files the
new identity and hold both forms of one image; every existing index is
rebuilt once on its first index after upgrading.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 02:10:53 +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 3f9948689d chore(pkg): sync the Scoop manifest to v0.12.0
The version-metadata contract tracks pkg/scoop as a release surface, and
the first sync commit missed it: version, both download URLs and both
Windows archive hashes, taken from the release's checksums.txt.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 00:57:11 +02:00
Martin Vogel db14b11093 chore(pkg): sync packaging metadata to v0.12.0
Every packaging surface states the newest v-prefixed tag, as the
version-metadata contract requires: the Chocolatey nuspec and installer
(checksum re-pinned to the released windows-amd64 archive), the Go
installer, the Nix flake, server.json, the npm and PyPI wrappers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 00:14:58 +02:00
Martin Vogel ec6427c381 Break-glass merge PR #2650 v0.12.0 2026-10-15 18:54:48 +02:00
Martin Vogel 58ca1012e7 Break-glass merge PR #2651 2026-10-15 18:53:58 +02:00
Martin Vogel dc516fe298 Merge pull request #2648 from DeusData/fix/audit-wget-token
fix(security): allow the bare wget token the shell pass compares
2026-10-15 18:48:49 +02:00
Martin Vogel 8a6a1047d6 ci(security): pin the ScanCode install; pluggy 1.7.0 breaks it
The license gate installed `scancode-toolkit` unpinned. pluggy 1.7.0
(uploaded to PyPI 2026-10-15 09:50 UTC) no longer finds the hookspecs that
plugincode 32.0.0 declares on a class (`@pre_scan_spec class
PreScanPlugin`), so scancode crashes at import with "ValueError: did not
find any 'pre_scan' hooks" and `security / license-gate` fails on every
run, whatever the tree. The last green gate started at 09:50:14 UTC;
every run since is red. `ci-ok` and the release `verify` job both need the
security workflow, so this blocks every PR and the release.

Install exactly scancode-toolkit 32.5.0 (the version every green run
used) with pluggy constrained to 1.6.0 through PIP_CONSTRAINT, which the
pip that pipx runs reads. The local hint in license-gate.sh names the
same command.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-15 18:16:16 +02:00
Martin Vogel eb294298e0 fix(daemon): settings startup survives busy and interrupted writes
Nondeterministic Windows reds came from races on _config.db:
test-windows-guards section_start_status_port / daemon_lifecycle ("the
daemon did not accept the UI configuration"), section_churn_stability
("could not load saved runtime settings"), the windows-2025 release
smoke whose `--ui=true --port` server exited about 5 s after start
(Phase 15, read as "no embedded UI assets"), and section_crash_recovery
("the stale daemon state never cleared").

- Every _config.db connection now waits out a committing writer
  (cbm_runtime_settings_wait_for_writers: one busy handler, logs
  config_db.busy_wait once per busy wait, a 3 s bound for a wedged
  holder). Startup's 50 ms window let a daemon's routine commit end
  unrelated CLI starts; cbm_config_open and the UI load had no wait at
  all, so a concurrent commit failed the daemon's UI save and made the
  load keep defaults.
- A process killed mid-write (crash, taskkill /F, power loss) left a hot
  rollback journal that the read-only settings reads cannot roll back
  (SQLITE_READONLY_ROLLBACK). Every later command then failed with
  "could not load saved runtime settings", permanently. Settings startup
  and the UI load now recover once through a read-write open that rolls
  the interrupted write back (config_db.hot_journal_recovered); a failed
  recovery is reported with the journal path and stays fatal.
- write_value no longer rewrites an unchanged value. The upsert fired the
  revision trigger, so every `daemon start` on a UI build committed and
  locked readers out although nothing changed.
- `daemon start` and the stdio server wait for the set_context and
  set_ui_config replies within their startup budget instead of the 1 s
  probe budget (MAIN_CONNECT_TIMEOUT_MS) that decided the command
  whenever the daemon's save ran long.

Tests, red on 8df0639c and green here:
- runtime_settings suite: three single-threaded lock tests (a log sink
  commits the held lock when the waiter logs config_db.busy_wait, so no
  test depends on a duration) and two hot-journal tests (a portable
  fixture snapshots the database and its journal mid-transaction and
  checks the journal is hot before the code under test runs).
- tests/windows/test_daemon_stability.py: new section
  start_port_waits_for_settings_writer holds the lock against a real
  `daemon start --port` and releases it on events; the crash section
  now prints the status output it saw.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-15 18:07:25 +02:00
Martin Vogel d21693df58 fix(security): allow the bare wget token the shell pass compares
The v0.12.0 release smoke blocked every binary in its string audit:
"BLOCKED: Dangerous commands found in binary: wget". The line is the bare
string "wget" that src/pipeline/pass_shell.c compares a script's program
name with, and records as the `via` of a curl / wget HTTP_CALLS edge
(3b93db06). The binary never runs it.

Allow that whole line only, like the rst grammar's bare "telnet". A line
that carries a wget command (flags, a target, a URL) stays blocked; the
contract test now pins both cases.

Proof on the release run's own artifacts (run 38644765106, main 8df0639c):
linux-amd64-portable and darwin-arm64 binaries BLOCKED before, all three
audits OK after. test_security_strings_allowlist.sh: 49 passed; with the
allowlist line reverted, the new bare-token case fails.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-15 03:18:42 +02:00
Martin Vogel 8df0639c62 Break-glass merge PR #2642 2026-10-14 23:56:37 +02:00
Martin Vogel 5a6f85faca Break-glass merge PR #2644 2026-10-14 23:56:23 +02:00
Martin Vogel 6bf6e34e43 perf(store): evict idle adjacency layers, budget them, own memory class
The daemon keeps adjacency layers (store/graph_mem.h) resident across
requests -- 237 MB for the kernel -- with no end of life but LRU pressure.

- Idle eviction: a layer nobody has held for CBM_GRAPH_MEM_IDLE_EVICT_MS
  (10 minutes, measured from its last release) is evicted on the next
  request: cbm_mcp_handle_tool sweeps after releasing the request store, and
  every acquire sweeps too. No timer thread. Ten minutes outlasts the gaps
  between an agent's graph calls within one task, while a session that moved
  on stops pinning the memory; a wrong guess costs one rebuild (1.2-1.5 s on
  the 17 GB kernel index, tens of ms on ordinary repos). The clock is a test
  seam (cbm_graph_mem_set_clock_for_testing), so tests move time instead of
  waiting.
- Worker budget: application_worker_memory_slice_locked hands index workers
  the aggregate minus the resident layer bytes (cbm_graph_mem_cached_bytes),
  at least one byte per active job so a slice never reads as "no cap".
- Memory class: layers are charged to the new CBM_MEM_CLASS_GRAPH_CACHE
  ("graph_cache" in census and class logs) instead of STORE.

Responses stay byte-identical to the baseline on django and redis (tools
and Cypher) and on the kernel trace/hub and extra BFS/degree cases.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 22:54:02 +02:00
Martin Vogel 932dd1d594 Merge pull request #2643 from DeusData/feat/infra-followup
feat(infra): env and service joins, kustomize variants, Terraform / Terragrunt, Ansible
2026-10-14 22:39:29 +02:00
Martin Vogel 8b4d5e8b5b perf(store): in-memory adjacency layer for trace_path and degree filters
On a kernel-scale index (8.5M nodes, 16.9M edges) trace_path's recursive
CTE and search_graph's per-row degree subqueries spend seconds in B-tree
seeks. graph_mem reads the edges once, in one sequential scan of the
covering UNIQUE index, into a typed CSR (node id -> out/in neighbours with
interned edge types) and answers both from RAM:

- store_bfs (shortest-path mode): BFS over the layer with the CTE's exact
  semantics (minimal hop per node, the start only via a cycle back to it,
  (hop, id) order, probe row, JOIN-skips for ids without a node row); node
  rows and edge collection still come from SQL. Trail mode is unchanged.
- cbm_store_search with min/max_degree: the two correlated COUNT(*)
  subqueries become cbm_adj_degree(); filter, order, page and total are the
  same SQL.

Layers live in a process-wide registry (the daemon closes its stores after
every request), refcounted across request threads, allocated through the
memory core (CBM_MEM_CLASS_STORE) and bounded: all layers stay under a
quarter of the budget, idle ones are evicted oldest-first, and a layer that
does not fit is not built (SQL path, never a partial layer). A layer is
keyed by db path + store generation + the rollback-journal file state (the
header change counter every rollback commit bumps), read in one read
transaction with the scan; only read-only query stores of sealed DBs
qualify. The file state matters: the cross-repo pass writes CROSS_* edges
into other projects' published DBs in place without a new generation; such
a DB is in WAL mode and keeps the SQL path until the next publish.

Kernel, macOS, same index, fresh daemon: trace_path schedule 7.5 s -> 21 ms
warm (1.23 s first call incl. a 1.2 s build), search_graph Function
min_degree 50 4.2 s -> 170 ms warm (1.35 s first). Layer: 16,890,273 edges,
237 MB. Responses byte-identical to the baseline on django, redis and the
kernel (tools, Cypher and extra BFS/degree cases).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 22:19:02 +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 312fb36844 perf(store): index-friendly plans for architecture, schema counts and anchored name search
Three query rewrites over the existing indexes; every MCP response stays
byte-identical to c9cacdf0 (tools + Cypher suites on django, redis and the
linux kernel, same index each).

- arch_packages: GROUP BY +n.name. With a bare column the planner walked
  idx_nodes_name over every node to get groups in name order, with a table
  lookup per node to test the label (6.3 s of the 7.3 s get_architecture on
  the 8.5M-node kernel graph, for 5 Package nodes). The unary plus seeks
  idx_nodes_label instead; the name tie-break is explicit because a sorted
  GROUP BY follows the ORDER BY direction and reversed tied counts.
- get_schema_impl: label / edge-type counts step through the distinct values
  (MIN(col) > previous) and count each value's index range instead of a
  GROUP BY scan: kernel 0.26 -> 0.16 s and 0.54 -> 0.32 s. Values reach
  ORDER BY in the same ascending order, so ties keep their order.
- cbm_store_search: an anchored name_pattern ("^schedule$", "^kmalloc")
  adds idx_nodes_name ranges for its literal prefix (every ASCII letter case
  of the first three letters under iregexp, which runs in the C locale),
  one UNION ALL arm per range merged by ORDER BY name, id without a sort;
  the count sums the ranges. LIKE hints and the regex stay in the WHERE, so
  the rows are those of the plain scan. Patterns that do not compile keep
  the plain query (and its error).

Kernel, interleaved medians n=5: get_architecture 9.2-10.3 s -> 1.03 s,
get_graph_schema 0.76 s -> 0.48 s, cycles 2.24 s -> 2.04 s, search_graph
^schedule$ 1.15 s -> 14 ms, ^kmalloc + label 0.64 s -> 14 ms. Peak daemon
RSS for get_architecture +67 MB -> +22 MB.

The ranged SQL is built with a bounds-checked append (no snprintf-derived sizes) into CBM_MEM_CLASS_STORE buffers; a build that does not fit rolls its binds back and the plain query runs.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 21:20:07 +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 181837343e perf(cypher): stream query execution chunk by chunk from a paged node scan
On the kernel index (8.5 M nodes, 1.9 M CALLS edges) `MATCH (a)-[:CALLS]->(b)
RETURN count(*)` peaked at 18-25 GB RSS (killed in a 12 GB container): the
store materialised every start node with all its strings (4.9 GB), every one
was deep-copied into a 1.6 KB binding, and every expanded row was materialised
again before projection.

cypher: execute_single now runs the start nodes through the pipeline in chunks
of 1024 (pattern-0 hops, late WHERE, WITH, RETURN). Each hop emits a binding's
expansions in input order and every consumer (RETURN rows / star / default
projection / aggregation, WITH aggregation / projection) keeps its state
between chunks, so the concatenated output is the single-pass output: same
rows, row order, first-seen group order, ORDER BY ties, DISTINCT, LIMIT and
max_rows truncation, OPTIONAL fallback rows, warnings. Queries with additional
MATCH patterns keep one pass (their join strategy depends on the first binding
of the whole set). Start nodes and fetched neighbours move their strings into
the binding instead of being deep-copied and freed.

store: cbm_store_node_scan_open/_next/_close hand out the rows of
cbm_store_find_nodes / _by_label a page at a time. Pages are keyset queries
along the index the eager query walks (idx_nodes_file: NULL file_path rows by
id, then the rest of the last file by id, then the files after it;
idx_nodes_label: by id), each reset before it returns, so no read lock is held
between pages. open verifies the eager plan with EXPLAIN QUERY PLAN and falls
back to the eager rows for any other plan.

Kernel, same index, medians of 3 (macOS arm64):
  count(*)    peak RSS 24.9 GB -> 256 MB,  23.0 s -> 13.0 s
  top-callee  peak RSS 12.4 GB -> 1.26 GB, 11.7 s -> 10.9 s
Responses byte-identical to the baseline on django, redis and the kernel
(19 Cypher + 24 tool cases each); the streamed scan equals the eager scan
field by field, in order, for every project and label of all three indexes.

A scan that fails part-way fails the whole query ("query aborted: reading the graph failed") instead of answering from the pages read so far:
The streamed start-node scan answered from the rows of the pages it had read
when a later page failed (a SQLite step error or a page re-prepare failure):
a partial result that looked complete. A failed page, a scan that cannot
open, or a failed eager fallback now marks the start source failed;
execute_chunk returns an error and cbm_cypher_execute reports
"query aborted: reading the graph failed (<store error>)" with no rows. The
store error is logged as cypher.scan_failed.

The test seam cbm_store_node_scan_test_fail_page_after (CBM_ENABLE_TEST_SEAMS)
fails one page after a given number of successful pages; the new test fails
the second page of a label scan (rows and count(*)) and a later page of the
all-nodes scan, and asserts an error result with no rows.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 21:05:58 +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 a372a1fa5e perf(pipeline): prove an unchanged re-index on the live generation
Every index_repository call copied the whole existing database into a
stage (sqlite3 backup) before routing, and the exact no-op then sealed
that copy and renamed it back over the live file. On the Linux kernel
index the copy alone was a 19 GB write per unchanged re-index.

The copy is now deferred until routing needs it. The exact no-op is
decided read-only on the live generation with the same checks the
staged route applies to its copy (shallow integrity, format version,
current coverage metadata and doc-link layer, byte-identical semantic
manifest); the run then publishes nothing and only refreshes the
artifact from the live database. Any other outcome copies and routes
exactly as before, so every change -- including a same-size edit that
keeps the original mtime -- still rebuilds from content hashes.
cbm_pipeline_new no longer resolves the Git context the run start
always re-resolved (seven git subprocesses per call).

No-op index_repository, macOS, median of three runs of n=3:
django 840.8 -> 539.0 ms, redis 450.7 -> 310.5 ms, Linux kernel
18449.5 -> 2405.0 ms. Responses and graphs are byte-identical to the
baseline for the no-op and for modify/add/delete/rename edits.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 20:29:55 +02:00
Martin Vogel 7729a8ee9f fix(mcp,cypher): allocate the new query-path memory through the memory core
lint-memory-core failed on #2642: the new search_code scanner, result index, root containment cache and Cypher sort/group/distinct scratch used malloc/calloc/realloc/free directly (mcp.c +22, cypher.c +26, git_context.c +1). They now go through cbm_alloc/cbm_calloc/cbm_realloc/cbm_free/cbm_mem_strdup: file buffers as CBM_MEM_CLASS_EXTRACT, the rest as CBM_MEM_CLASS_OTHER, each block freed with the class it was allocated with. The result index reserves its slot before a result's strings exist, so its insert cannot fail and needs no cleanup; cbm_git_branch_resolve writes into caller buffers instead of returning heap strings. cypher.c ends below its baseline (128 < 132), which is tightened. Output unchanged: 42/42 tool and Cypher responses identical on django; mcp, cypher and git_context suites green under ASan.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 20:18:55 +02:00
Martin Vogel 5ca4326621 fix(infra): strip the ansible.builtin. prefix without strlen on the offset
GCC 13 on x86-64 (-O1, ASan/UBSan, -Werror) rejected an_key's memmove(out, out + bl, strlen(out + bl) + 1) as -Wstringop-overread, a region of size 0; arm64 GCC and clang accept it. The key length is now measured once and the prefix compared with memcmp. Every C file this branch changes compiles warning-free with that GCC.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 20:12:36 +02:00
Martin Vogel 04a67f8a84 fix(cypher): build the DISTINCT row key without snprintf-derived sizes
CodeQL cpp/overflowing-snprintf (alert 85) flagged rb_row_key: it advanced `used` by snprintf's return value and passed `cap - used` as the next size, which would underflow if a call ever truncated. The buffer is sized for every column first, so it never did; the key is now copied with explicit bounds instead. The key bytes are unchanged, so DISTINCT results are identical.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 19:11:39 +02:00
Martin Vogel 908186334e fix(infra): an example URL in a comment uses the audit's placeholder host
security-static allows URL literals in C sources only on its allow-list; a comment continuation line is scanned like code, so the curl example in pass_shell.c named a real host. It now uses the placeholder https://host/... that scripts/security-audit.sh accepts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 19:03:46 +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 c9cacdf018 perf(mcp): fast query tools on every platform
search_code
- Scan the indexed files in process instead of through grep (POSIX) or a
  PowerShell Select-String pipeline (Windows), with the same grep -f
  semantics: one pattern per line, ERE regex, binary-file and NUL handling,
  -Hn records. The subprocess runner stays for the unindexed recursive
  fallback and a Windows file_pattern it cannot prefilter.
- Windows now matches like macOS and Linux: Select-String matched
  case-insensitively, used .NET regex syntax and read NUL-containing
  files as text. A trailing CR is still ignored there, so `foo$` keeps
  matching CRLF checkouts.
- Result merge, root containment and source reads no longer scale with
  hits x results: a node-id index, one canonical root per request with a
  per-file verdict cache, and fread spans instead of per-byte fgetc.

query_graph
- ORDER BY (RETURN and WITH) was a bubble sort. A stable merge sort keeps
  the same comparisons and so the same order; WITH keys are resolved once
  per row instead of twice per comparison.
- GROUP BY, DISTINCT (RETURN and WITH) and COUNT(DISTINCT) looked every
  row up in a linear list. They hash the same keys now.

Store reads
- Read tools run their statements in one deferred read transaction,
  renewed every 100 ms, instead of taking the database file lock once per
  statement.

list_projects
- Resolve only the branch the listing prints: 2 git processes per project
  instead of 7.

On macOS and Linux every response is byte-identical to before:
search_code 30/30 (django), 28/28 (this repo) and 28/28 (Linux, GNU
grep); all tools and the Cypher cases 86/86 on shared django and redis
indexes. On django: search_code 908 -> 355 ms, WITH ... ORDER BY
41 s -> 62 ms, count(*) 508 -> 184 ms, list_projects 94 -> 40 ms. On the
Windows ARM VM: search_code 1.6-3.4 s -> 0.2-0.5 s.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 18:26:57 +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 246d1a0a4b fix(incremental): a Terraform input change declines to the full rebuild
A Terraform module's output read can live in one file (outputs.tf:
module.net.vpc_id) and the module's source in another (main.tf). When
only main.tf changed -- its source pointed elsewhere -- the incremental
repair recomputed that file's edges alone, so the read, held by two
unchanged files, stayed in the graph although a full reindex no longer
has it. Measured on kube-hetzner: 18 stale output reads after moving
one module's source.

Terraform, OpenTofu and Terragrunt inputs reach past their own file the
way Kubernetes bundles do: the files of a module directory resolve each
other's references, a module call decides what another file reads, a
.tfvars file sets the variables beside it. A change to a .tf, .tofu,
.tfvars or .hcl file now declines the repair to the full rebuild, as a
kustomization or manifest change already does.

The incremental re-index now equals a full index in all 18 Terraform
scenarios, the stale-read one included; a Terraform edit costs a full
rebuild (fabric: 1.64 -> 1.90 s). A pipeline test moves a module's
source and requires the read to go.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-14 06:16:28 +02:00