From 22f83b21791acc381b5e2c8a4ea5db7105d7c694 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 3 Jul 2026 19:08:47 +0200 Subject: [PATCH 1/4] fix(mcp): get_architecture overview subset, aspects enum + validation Distills the design from PR #560 onto current main: - "overview" now expands to every aspect except file_tree (the per-file listing that alone can push the payload past the MCP output cap). Previously the token matched nothing and silently degraded to just {total_nodes,total_edges}. Both aspect gates (want_aspect in store.c, aspect_wanted in mcp.c) share one predicate, cbm_store_arch_aspect_in_overview, declared in store.h so the two sites cannot drift. - The aspects items schema gains an enum of the 13 valid tokens (advisory, client-side), mirroring VALID_ASPECTS. - Server-side validation (authoritative): unknown aspect tokens now return an isError result listing the valid values instead of a silent near-empty response. Bounded snprintf; respects the existing 16-token aspects cap. Reproduce-first tests: overview-subset and unknown-aspect tests fail on the unfixed code; a parsed tools/list guard pins the schema enum. Co-authored-by: Kirchlive Signed-off-by: Martin Vogel --- src/mcp/mcp.c | 72 +++++++++++++++++-- src/store/store.c | 12 ++++ src/store/store.h | 6 ++ tests/test_mcp.c | 172 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 4 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index d7ee3b63b..994c4b857 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -437,11 +437,15 @@ static const tool_def_t TOOLS[] = { "representative top_nodes, and the packages/edge_types that bind it) — use these to grasp " "the real architectural seams, which often cut across the folder layout. Optional path scopes " "analysis to nodes under that directory prefix (file_path).", + /* The aspects enum mirrors VALID_ASPECTS (see aspect_is_valid) — update both together. */ "{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"path\":{\"type\":" "\"string\",\"description\":\"Optional directory prefix to scope architecture (e.g. " "apps/hoa)\"}," - "\"aspects\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"project\"]" - "}"}, + "\"aspects\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"enum\":[\"all\"," + "\"overview\",\"structure\",\"dependencies\",\"routes\",\"languages\",\"packages\"," + "\"entry_points\",\"hotspots\",\"boundaries\",\"layers\",\"file_tree\",\"clusters\"]}," + "\"description\":\"Aspects to include. 'all' = everything; 'overview' = compact summary " + "(all except file_tree); omit = all.\"}},\"required\":[\"project\"]}"}, {"search_code", "Search code", "Graph-augmented code search. Finds text patterns via grep, then enriches results with " @@ -2244,7 +2248,29 @@ static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) { return result; } -/* Check if an aspect is requested (NULL aspects = all, or array contains "all" or the name). */ +/* Canonical list of valid aspect tokens for get_architecture. Single source + * of truth for the server-side validation (authoritative); the JSON-Schema + * enum in the TOOLS entry above is the advisory client-side mirror — update + * both together when the aspect set changes. */ +static const char *VALID_ASPECTS[] = { + "all", "overview", "structure", "dependencies", "routes", "languages", "packages", + "entry_points", "hotspots", "boundaries", "layers", "file_tree", "clusters", NULL}; + +static bool aspect_is_valid(const char *name) { + if (!name) { + return false; + } + for (int i = 0; VALID_ASPECTS[i]; i++) { + if (strcmp(name, VALID_ASPECTS[i]) == 0) { + return true; + } + } + return false; +} + +/* Check if an aspect is requested. NULL aspects = all. The array can contain + * "all" (everything), "overview" (everything except file_tree — see + * cbm_store_arch_aspect_in_overview in store.c), or the aspect name itself. */ static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, const char *name) { if (!aspects_arr) { return true; /* no filter = all */ @@ -2254,7 +2280,16 @@ static bool aspect_wanted(yyjson_doc *aspects_doc, yyjson_val *aspects_arr, cons yyjson_val *val; while ((val = yyjson_arr_iter_next(&iter)) != NULL) { const char *s = yyjson_get_str(val); - if (s && (strcmp(s, "all") == 0 || strcmp(s, name) == 0)) { + if (!s) { + continue; + } + if (strcmp(s, "all") == 0) { + return true; + } + if (strcmp(s, "overview") == 0 && cbm_store_arch_aspect_in_overview(name)) { + return true; + } + if (strcmp(s, name) == 0) { return true; } } @@ -2331,6 +2366,35 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) { } } + /* Server-side validation: reject unknown aspect tokens with an isError + * result listing the valid values. The JSON-Schema enum is advisory — + * many MCP clients do not validate arguments against tool schemas — so + * without this check a typo degraded to a silent near-empty payload. */ + for (int i = 0; i < aspects_strs_count; i++) { + if (!aspect_is_valid(aspects_strs[i])) { + char valid_list[CBM_SZ_256]; + size_t off = 0; + for (int j = 0; VALID_ASPECTS[j] && off < sizeof(valid_list); j++) { + int n = snprintf(valid_list + off, sizeof(valid_list) - off, "%s%s", + j > 0 ? ", " : "", VALID_ASPECTS[j]); + if (n < 0) { + break; + } + off += (size_t)n; + } + char msg[CBM_SZ_512]; + snprintf(msg, sizeof(msg), "Unknown aspect '%s'. Valid: %s.", aspects_strs[i], + valid_list); + char *err = cbm_mcp_text_result(msg, true); + free(project); + free(scope_path); + if (aspects_doc) { + yyjson_doc_free(aspects_doc); + } + return err; + } + } + cbm_schema_info_t schema = {0}; /* Counts-only: this handler renders label/type counts but never property * keys, and full key discovery json_each-scans every row (seconds-to- diff --git a/src/store/store.c b/src/store/store.c index 62744e4eb..c64c416df 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -5450,6 +5450,15 @@ static int arch_clusters(cbm_store_t *s, const char *project, const char *path, /* ── GetArchitecture dispatch ──────────────────────────────────── */ +/* "overview" = compact architecture summary: every aspect EXCEPT the large + * per-file listing (file_tree), which alone dominates the payload on real + * repos and can push the MCP response past the output cap. Declared in + * store.h and shared with aspect_wanted in src/mcp/mcp.c so the store-side + * DB gate and the MCP-side serialization gate cannot drift. */ +bool cbm_store_arch_aspect_in_overview(const char *name) { + return strcmp(name, "file_tree") != 0; +} + static bool want_aspect(const char **aspects, int aspect_count, const char *name) { if (!aspects || aspect_count == 0) { return true; @@ -5458,6 +5467,9 @@ static bool want_aspect(const char **aspects, int aspect_count, const char *name if (strcmp(aspects[i], "all") == 0) { return true; } + if (strcmp(aspects[i], "overview") == 0 && cbm_store_arch_aspect_in_overview(name)) { + return true; + } if (strcmp(aspects[i], name) == 0) { return true; } diff --git a/src/store/store.h b/src/store/store.h index 9816784f1..a4541e2df 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -333,6 +333,12 @@ bool cbm_store_arch_path_scoped(const char *path); /* When scoped, writes normalized directory prefix into norm_out. Returns false if unscoped. */ bool cbm_store_normalize_arch_path(const char *path, char *norm_out, size_t norm_sz); +/* True when architecture aspect `name` belongs to the "overview" subset: + * every aspect EXCEPT the large per-file listing (file_tree). Shared by both + * aspect gates — want_aspect (store.c) and aspect_wanted (mcp.c) — so the + * two sites cannot drift. */ +bool cbm_store_arch_aspect_in_overview(const char *name); + /* Delete all nodes for a project (cascade deletes edges). */ int cbm_store_delete_nodes_by_project(cbm_store_t *s, const char *project); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 8d263c570..e8b8a17e0 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -361,6 +361,75 @@ TEST(mcp_ingest_traces_items_disallow_additional_properties_issue731) { PASS(); } +/* Guard for PR #560 (schema enum): the get_architecture aspects items schema + * must carry an enum of the valid tokens — including the new "overview" — + * mirroring VALID_ASPECTS in mcp.c. Parsed structurally like + * mcp_ingest_traces_items_disallow_additional_properties_issue731. */ +TEST(mcp_get_architecture_aspects_schema_enum_pr560) { + char *json = cbm_mcp_tools_list(); + ASSERT_NOT_NULL(json); + + yyjson_doc *doc = yyjson_read(json, strlen(json), 0); + ASSERT_NOT_NULL(doc); + yyjson_val *root = yyjson_doc_get_root(doc); + ASSERT_NOT_NULL(root); + yyjson_val *tools = yyjson_obj_get(root, "tools"); + ASSERT_NOT_NULL(tools); + ASSERT_TRUE(yyjson_is_arr(tools)); + + yyjson_val *tool; + yyjson_arr_iter iter; + yyjson_arr_iter_init(tools, &iter); + yyjson_val *get_arch = NULL; + while ((tool = yyjson_arr_iter_next(&iter)) != NULL) { + yyjson_val *name = yyjson_obj_get(tool, "name"); + if (name && yyjson_is_str(name) && strcmp(yyjson_get_str(name), "get_architecture") == 0) { + get_arch = tool; + break; + } + } + ASSERT_NOT_NULL(get_arch); + + yyjson_val *input_schema = yyjson_obj_get(get_arch, "inputSchema"); + ASSERT_NOT_NULL(input_schema); + yyjson_val *properties = yyjson_obj_get(input_schema, "properties"); + ASSERT_NOT_NULL(properties); + yyjson_val *aspects = yyjson_obj_get(properties, "aspects"); + ASSERT_NOT_NULL(aspects); + ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(aspects, "type")), "array"); + yyjson_val *items = yyjson_obj_get(aspects, "items"); + ASSERT_NOT_NULL(items); + ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(items, "type")), "string"); + yyjson_val *enum_arr = yyjson_obj_get(items, "enum"); + ASSERT_NOT_NULL(enum_arr); + ASSERT_TRUE(yyjson_is_arr(enum_arr)); + + /* The enum must be exactly the valid-token set — no more, no less. */ + static const char *expected[] = {"all", "overview", "structure", "dependencies", + "routes", "languages", "packages", "entry_points", + "hotspots", "boundaries", "layers", "file_tree", + "clusters"}; + size_t expected_count = sizeof(expected) / sizeof(expected[0]); + ASSERT_EQ(yyjson_arr_size(enum_arr), expected_count); + for (size_t i = 0; i < expected_count; i++) { + bool found = false; + yyjson_val *ev; + yyjson_arr_iter eiter; + yyjson_arr_iter_init(enum_arr, &eiter); + while ((ev = yyjson_arr_iter_next(&eiter)) != NULL) { + if (yyjson_is_str(ev) && strcmp(yyjson_get_str(ev), expected[i]) == 0) { + found = true; + break; + } + } + ASSERT_TRUE(found); + } + + yyjson_doc_free(doc); + free(json); + PASS(); +} + TEST(mcp_text_result) { char *json = cbm_mcp_text_result("{\"total\":5}", false); ASSERT_NOT_NULL(json); @@ -1118,6 +1187,106 @@ TEST(tool_get_architecture_emits_populated_sections) { PASS(); } +/* Distills PR #560 (overview subset): "overview" must expand to a compact + * subset — every aspect EXCEPT file_tree. Before the fix, "overview" was not + * registered in either aspect gate (want_aspect in store.c, aspect_wanted in + * mcp.c), so aspects=["overview"] silently degraded to just + * {total_nodes,total_edges}. RED on unfixed code: no "entry_points" key. */ +TEST(tool_get_architecture_overview_compact_subset_pr560) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + + cbm_store_t *st = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(st); + + const char *proj = "arch560"; + cbm_mcp_server_set_project(srv, proj); + cbm_store_upsert_project(st, proj, "/tmp/arch560"); + + cbm_node_t main_fn = {0}; + main_fn.project = proj; + main_fn.label = "Function"; + main_fn.name = "main"; + main_fn.qualified_name = "arch560.cmd.main"; + main_fn.file_path = "cmd/main.go"; + main_fn.start_line = 1; + main_fn.end_line = 3; + main_fn.properties_json = "{\"is_entry_point\":true}"; + ASSERT_GT(cbm_store_upsert_node(st, &main_fn), 0); + + /* A File node so the file_tree aspect has real content — makes the + * "overview drops file_tree" assertion below non-vacuous. */ + cbm_node_t file_node = {.project = proj, + .label = "File", + .name = "main.go", + .qualified_name = "arch560.cmd.main.go", + .file_path = "cmd/main.go"}; + ASSERT_GT(cbm_store_upsert_node(st, &file_node), 0); + + /* Sanity: with "all", both entry_points and file_tree surface. */ + char *resp_all = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":560,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"get_architecture\"," + "\"arguments\":{\"project\":\"arch560\",\"aspects\":[\"all\"]}}}"); + ASSERT_NOT_NULL(resp_all); + char *inner_all = extract_text_content(resp_all); + ASSERT_NOT_NULL(inner_all); + ASSERT_NOT_NULL(strstr(inner_all, "\"entry_points\"")); + ASSERT_NOT_NULL(strstr(inner_all, "\"file_tree\"")); + free(inner_all); + free(resp_all); + + /* "overview": substantive content (entry_points, node_labels) but NO + * file_tree section. */ + char *resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":561,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"get_architecture\"," + "\"arguments\":{\"project\":\"arch560\",\"aspects\":[\"overview\"]}}}"); + ASSERT_NOT_NULL(resp); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "\"entry_points\"")); + ASSERT_NOT_NULL(strstr(inner, "\"node_labels\"")); + ASSERT_NULL(strstr(inner, "\"file_tree\"")); + + free(inner); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +/* Distills PR #560 (server-side validation): unknown aspect tokens must be + * rejected with an isError result listing the valid values. Before the fix + * the JSON-Schema accepted any string and both aspect gates simply never + * matched, so a typo like "bogus_aspect" produced a silent near-empty payload + * with isError:false. RED on unfixed code: no isError, no "Unknown aspect". */ +TEST(tool_get_architecture_rejects_unknown_aspect_pr560) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + + cbm_store_t *st = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(st); + + const char *proj = "arch560v"; + cbm_mcp_server_set_project(srv, proj); + cbm_store_upsert_project(st, proj, "/tmp/arch560v"); + + char *resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":562,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"get_architecture\"," + "\"arguments\":{\"project\":\"arch560v\",\"aspects\":[\"bogus_aspect\"]}}}"); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "\"isError\":true")); + ASSERT_NOT_NULL(strstr(resp, "Unknown aspect 'bogus_aspect'")); + /* The error must teach the valid vocabulary, including the new token. */ + ASSERT_NOT_NULL(strstr(resp, "overview")); + ASSERT_NOT_NULL(strstr(resp, "file_tree")); + + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + /* Reproduce-first for #640: query handlers must accept the `project_name` * alias, not only the canonical `project` key. list_projects surfaces the field * as "name" and the error hint says "pass the project name", so a caller @@ -3736,6 +3905,7 @@ SUITE(mcp) { RUN_TEST(mcp_index_repository_declares_name_override_issue571); RUN_TEST(mcp_tools_array_schemas_have_items); RUN_TEST(mcp_ingest_traces_items_disallow_additional_properties_issue731); + RUN_TEST(mcp_get_architecture_aspects_schema_enum_pr560); RUN_TEST(mcp_text_result); RUN_TEST(mcp_text_result_skips_structured_content_for_plain_text); RUN_TEST(mcp_cancel_matches_request_id); @@ -3797,6 +3967,8 @@ SUITE(mcp) { RUN_TEST(tool_delete_project_not_found); RUN_TEST(tool_get_architecture_empty); RUN_TEST(tool_get_architecture_emits_populated_sections); + RUN_TEST(tool_get_architecture_overview_compact_subset_pr560); + RUN_TEST(tool_get_architecture_rejects_unknown_aspect_pr560); RUN_TEST(tool_get_architecture_accepts_project_name_alias_issue640); RUN_TEST(tool_search_graph_accepts_project_name_alias_issue640); RUN_TEST(tool_get_architecture_path_scoping); From 23de5b23af1064ffd8c82d37935b4289e36ce50e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 3 Jul 2026 19:22:19 +0200 Subject: [PATCH 2/4] docs: add .cbmignore how-to (syntax, precedence, negation semantics) Documents the current behavior as implemented in src/discover/discover.c and src/discover/gitignore.c: where .cbmignore is read from, the glob features the parser supports, the layered precedence against built-in skip lists / .gitignore hierarchy / git global excludes, and what negation can and cannot override today. Planned negation unification (un-skipping built-in dirs, non-negatable safety core, shared predicate for auxiliary walkers) is listed in an explicitly not-yet-implemented subsection. Linked from the README "Ignoring Files" section. Signed-off-by: Martin Vogel --- README.md | 2 + docs/cbmignore.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/cbmignore.md diff --git a/README.md b/README.md index c1c17f9f8..a0732fac3 100644 --- a/README.md +++ b/README.md @@ -452,6 +452,8 @@ Anything outside this subset (write/`MERGE`/`CALL` clauses, unsupported function Layered: hardcoded patterns (`.git`, `node_modules`, etc.) → `.gitignore` hierarchy → `.cbmignore` (project-specific, gitignore syntax). Symlinks are always skipped. +See [docs/cbmignore.md](docs/cbmignore.md) for the full `.cbmignore` how-to: syntax, precedence across the ignore layers, and negation semantics. + ## Configuration ```bash diff --git a/docs/cbmignore.md b/docs/cbmignore.md new file mode 100644 index 000000000..3d19e61e8 --- /dev/null +++ b/docs/cbmignore.md @@ -0,0 +1,121 @@ +# `.cbmignore` — Excluding Files from Indexing + +`.cbmignore` is a project-specific ignore file that controls which files the +indexer sees. It uses gitignore-style syntax and is read from the **root of +the indexed directory** (`/.cbmignore`). Nested `.cbmignore` files in +subdirectories are not read. + +It applies at **file discovery time** — the directory walk that selects files +for parsing. Every indexing path uses the same discovery: the initial +`index_repository`, manual re-indexing, and background auto-sync. A path +matched by `.cbmignore` never enters the graph. Changes to `.cbmignore` take +effect on the next (re-)index. + +Unlike `.gitignore`, it has no effect on git itself — it only shapes what the +indexer sees. Commit it to share indexing excludes with your team, or list it +in `.gitignore` to keep personal excludes untracked. + +To verify it works: directory subtrees skipped during discovery are reported +in the `index_repository` response under `excluded` +(`{"dirs": [up to 25 paths], "count": , "truncated": }`). + +## Syntax + +One pattern per line. Blank lines are ignored, lines starting with `#` are +comments, and trailing whitespace is trimmed. + +| Feature | Meaning | +|---|---| +| `*` | matches any run of characters, except `/` | +| `?` | matches exactly one character, except `/` | +| `**` | matches across directory boundaries (`**/name`, `dir/**`, `a/**/b`) | +| `[abc]`, `[a-z]` | character classes; `[!a-z]` / `[^a-z]` negate the class | +| trailing `/` | pattern matches **directories only** | +| `/` anywhere else | anchors the pattern to the repo root | +| no `/` in pattern | matches the file/directory name at **any depth** | +| leading `!` | negation — re-includes a previously matched path; the **last matching pattern wins** | + +Examples: + +```gitignore +# Generated protobuf output, anywhere in the tree +*.pb.go + +# A specific top-level directory (leading / anchors to the repo root) +/third_party/ + +# Any directory named "snapshots", at any depth (trailing / = directories only) +snapshots/ + +# Everything under any fixtures directory +**/fixtures/** + +# Anchored glob: generated clients for any single-character API version +/api/v?/generated/ + +# Character class: yearly log folders 2020-2029 +/logs/202[0-9]/ + +# Ignore all YAML, but keep CI configs (negation — last match wins) +*.yaml +!ci.yaml +``` + +## Precedence + +Discovery applies its filters in a fixed order — the first layer that rejects +a path wins. For directories: + +1. **Built-in skip list** — `.git`, `node_modules`, `dist`, `target`, + `vendor`, tool caches, etc. (60+ names; the fast/moderate index modes add + more, e.g. `docs`, `examples`, `testdata`). Not overridable from any + ignore file today. +2. **Repo `.gitignore`** — `/.gitignore` merged with + `/info/exclude` (worktree-aware); later patterns win on + conflict. Honored even when the indexed directory is not a git repo root. +3. **Nested `.gitignore` files** — picked up during the walk and matched + relative to their own directory. +4. **`.cbmignore`** — a positive match skips the path; a negated match can + only rescue paths from layer 5. +5. **Git global excludes** — `core.excludesFile` from `~/.gitconfig` or the + XDG git config (default `$XDG_CONFIG_HOME/git/ignore`); consulted only + when the project is a git repo with a config. + +For files, built-in suffix filters (`.png`, `.o`, `.db`, …; fast modes add +archives, media, lockfiles, `.min.js`, …) and fast-mode filename/substring +filters run **before** the ignore files, and a maximum-file-size cap runs +after them; none of these are overridable from `.cbmignore`. Symlinks are +always skipped. + +## Negation (`!`) — current behavior + +- **Within `.cbmignore`**: standard gitignore semantics. Patterns are + evaluated top to bottom and the last matching pattern wins, so + `!pattern` re-includes something an earlier line excluded. +- **Parent pruning** (same caveat as git): when a directory is excluded, the + walk never descends into it — you cannot re-include a file whose parent + directory is excluded. Negate the directory itself if you need its + contents. +- **Across layers**: a `.cbmignore` negation overrides the **git global + excludes** layer only. Example: your `~/.config/git/ignore` ignores + `*.sql`, but this project's SQL should be indexed — add `!*.sql` to + `.cbmignore`. Negation cannot override the built-in skip lists, the repo + `.gitignore`/`info/exclude`, nested `.gitignore` files, the built-in + suffix/filename filters, or the size cap. + +### Planned (not yet implemented) + +The negation story is being unified; none of the following works yet: + +- `!` in `.cbmignore` will be able to un-skip ordinary built-in skip + directories (`obj/`, `dist/`, `target/`, …) so build-output-like + directories that actually contain source can be indexed. +- A small safety core stays non-negatable by design — `.git`, + `node_modules`, and worktree-internal directories — because indexing them + risks OOM and correctness issues (see issue #489). +- Auxiliary filesystem walkers will honor the same ignore predicate as + discovery, so every code path sees an identical ignore decision + (unification tracked in a follow-up issue). + +Until these land, the "Precedence" and "Negation — current behavior" sections +above describe the actual behavior. From 30f0577c155d557fecdddf8aa5170215fc540294 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 3 Jul 2026 20:19:30 +0200 Subject: [PATCH 3/4] ci: re-trigger CodeQL gate (poll-timeout flake, no code change) Signed-off-by: Martin Vogel From 76e87f4086d728643235f98fb7583d0afe6ac453 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 3 Jul 2026 23:31:12 +0200 Subject: [PATCH 4/4] ci: re-trigger CodeQL gate (gate lookup fixed on main via #820) Signed-off-by: Martin Vogel