With #2134, an unnamed index_repository of a root that another indexed
project already owns adopts that owner's name, and the daemon keys the
job by it (application_index_project_key reads the forwarded "name").
The unnamed status poll that the #2144 async reply asks for ("the same
repo_path and status: true") still derived its key from the path alone
in index_status_project_key. For an owned root it never found the job
it had just started and answered "no index job or index is known for
project '<path-derived>'"; an ambiguous root got that same misleading
answer instead of the loud error the start returns.
index_status_project_key now runs the start path's
index_root_owner_resolve on the same canonical repo_path: an explicit
name still wins, a single other owner is the key, several owners fail
with the same error naming them, otherwise the path-derived key stays.
The status error buffer grows to hold that owner list.
The daemon's other readers of the key (job dedupe in
application_job_subscribe_locked, the synchronous cut-short marker set
and taken around application_index_run_sync) derive it from the
forwarded args, which already carry the owner name, so they agree with
the start; status was the only reader that diverged.
Refs #2134, #2144
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`from unittest.mock import patch` names a module outside the project.
Strategy 1 of cbm_pipeline_resolve_import_node cannot resolve it, so the
Strategy-3 symbol-name fallback bound the import to the only project
definition whose leaf is `patch` -- a REST view's PkgConfigView.patch --
and every `patch(...)` call became an import_map CALLS edge at confidence
0.95. Once that IMPORTS edge is gone the same call falls through to the
weak short-name strategies (unique_name 0.75, suffix_match), which bind it
again; `mock.patch(...)`, `json.loads(...)`, `os.path.join(...)` reached
project methods the same way, because the member guard exempts
import-rooted receivers.
Two per-language (Python-only) rules, both keyed on the import's module
chain rather than on any spelling list:
- Import side (pass_pkgmap.c, shared by both drivers): for an import
whose module is not in the project, a Strategy-3 hit must spell the
import's module chain as an ordered subsequence of its QN (a sys.path
root or src/ layout the resolver missed still qualifies), and whatever
names a module (an enclosing path segment, any segment of a plain
`import x`) may only bind a Module/File node. Imports of project
modules are unchanged (they may re-export from anywhere).
- Call side (pass_calls.c + pass_parallel.c, identical gates): a call
whose root identifier is bound by an external import (no IMPORTS edge
materialized it) and whose weak-strategy target contradicts that
import's chain is not emitted as a plain CALLS edge. import_map /
same_module / lsp_* edges are never touched.
django/django (parallel pipeline): CALLS 62,430 -> 60,123, TESTS
29,872 -> 28,571, IMPORTS 16,068 -> 15,491. Removed CALLS are
member calls through stdlib imports (1,900: os.path.join ->
defaultfilters.join, json.loads -> signing.loads, mock.patch ->
RedirectView.patch) and bare calls of stdlib names (398: date ->
defaultfilters.date, urlsplit -> MultiPartParser.parse); a random
25-edge sample was 25/25 false. Six IMPORTS are re-targeted to the real
definition (template_tests.utils.setup, multiple_database TestRouter,
pr_quality errors.Message).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A method call on an instance of a class defined in another file produced
no CALLS edge, while the identical code in the class's own file resolved
via lsp_ts_method. The type-aware tier typed the receiver wrongly in two
places, so the method lookup in the shared TS registry missed and the
call fell to method_not_in_registry:
1. `new C()` always qualified a bare constructor name against the
CURRENT module. `const t = new ImportedClass(); t.m()` and
`new ImportedClass().m()` were typed as a phantom
`<this module>.ImportedClass`. The constructor type now follows TS
scoping: a class declared in this module, else the class an
unambiguous import binding names, else the old spelling.
2. ts_import_symbol_qn (and the copy of its heuristic in
resolve_type_with_imports) treated an import value ending in
".<name>" as an already-qualified symbol QN. For the dominant TS
convention of a file named after its class (`Foo.ts` exports
`class Foo`, module `x.Foo`, class `x.Foo.Foo`), the receiver was
typed as the MODULE, for `new`, annotations and parameters alike.
The registry now decides the ambiguous case: a registered
`module.name` wins, else the value is the symbol QN as before.
Both are resolved through the existing shared cross-file registry; no
name-based fallback is reintroduced. Cost is one or two hashed lookups
on the finalized overlay / sealed shared registry, per `new` expression
or per ambiguous import spelling during the walk; nothing is looked up
during registration and nothing is written to the shared registry.
typeorm (3,593 .ts files, shallow f279fd13): lsp_ts_method 2,551 ->
3,130, all 579 new edges cross-file (cross-file lsp_ts_method 696 ->
1,275), 0 edges lost, other strategies unchanged, index time
unchanged. 20 sampled new edges checked against source: 20 true
positives.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Codex rewrites its [mcp_servers] tables whenever it edits MCP servers
itself (`codex mcp add`, its MCP settings, wrappers such as Orca that
drive it) and drops comment decor on the way. From our managed block it
keeps the table but loses the begin marker, omits `args = []`, and leaves
the end marker as an orphan below whichever table follows. Reproduced
with codex-cli 0.153.4: after `codex mcp add orca -- /bin/echo hi` the
file holds an unmarked
[mcp_servers.codebase-memory-mcp]
command = ".../codebase-memory-mcp"
env_vars = ["CBM_CACHE_DIR", "CBM_RUNTIME_DIR"]
and a stray `# <<< codebase-memory-mcp MCP <<<` under [mcp_servers.orca].
Every later install then failed with op=mcp_install on a table cbm itself
had written, twice over: cbm_toml_remove_legacy_table refused the orphan
marker as malformed, and its owned-shape check predated env_vars (#1562,
#1664), so even without the orphan the table counted as foreign.
cbm_toml_remove_legacy_table now drops a lone orphaned marker line in the
same atomic write (we are the only author of those markers, #1558; only
that line is removed, nothing is guessed) and treats env_vars naming only
CBM_CACHE_DIR / CBM_RUNTIME_DIR as part of the owned shape. A table with
any other key or forwarded name is still foreign and left byte-identical,
and a foreign table keeps the file untouched, orphan included. The scan is
split into toml_strip_legacy_table + toml_legacy_classify_assignment so
the entry point reads the file, heals, strips and writes once.
The same helper backs the Grok Build install, which gains the same
self-heal.
Tests: cli_upsert_codex_mcp_adopts_codex_rewritten_table uses the exact
bytes Codex produced; config_toml_legacy_remove_adopts_owned_env_vars_only
pins the env_vars boundary (owned names adopted, any other name foreign).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
tool_index_repository_reuses_existing_project_for_root_issue2134 counts
how often the canonical repo root occurs in list_projects. On Windows,
cbm_canonical_path returns "C:\Users\...", while the stored root_path is
"C:/Users/...", so the count was 0 and the test failed on the CLANG64
leg (test_mcp.c:8448: owners == 0, expected 1).
Normalize the expected root with cbm_normalize_path_sep, a no-op on
POSIX, before counting.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Linux, MSan, TSan and diag CI failed in mcp_mutation_guard:
tool_cross_repo_missing_inputs_fail_without_creating_ghost_databases
ASSERT(source_failed). handle_index_repository resolved the root owner
before dispatching cross-repo-intelligence mode. That test registers its
named target with the source's own root, so the unindexed source adopted
existing-cross-target as its project (log: index.root_owner_reused
project=existing-cross-target), and the missing source was no longer
reported. Cross-repo mode reads an existing source index and never writes
one, so the #2134 reuse now runs only on the re-index path, after that
dispatch. macOS CI stayed green because cbm_tmpdir() is /tmp, a symlink
to /private/tmp there, so the canonical repo_path never equalled the
fixture's stored root; with the fixture path canonicalized, the failure
reproduced 3/3 on macOS and passes 3/3 with this change.
The #2134 test itself called realpath() into a 1 KiB buffer. glibc's
FORTIFY check aborts on that ("buffer overflow detected", a SIGABRT of the
mcp suite on the ubuntu 2/3 shards), and MinGW has no realpath at all
(Windows compile error). It now uses cbm_canonical_path with a 4 KiB
buffer, the same canonicalizer production uses.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
index_repository without `name` derived the project key from the path
alone. When the root had first been indexed under an explicit name, a
later unnamed re-index forked a second index (same root_path,
path-derived name) and left the original untouched, so the stale index
kept being served while the call reported success.
Resolve the key from the indexes on disk before any work starts:
- the path-derived project exists -> keep it (unchanged behavior);
- exactly one other project owns the canonical root -> adopt its name
and update it in place, so existing indexes are never orphaned;
- several projects own the root -> fail loudly, name them and ask for
`name` instead of guessing or forking again;
- none -> first index under the derived name, as before.
The resolved name is written into the args handed to the daemon job
coordinator and the supervised worker, so the project lease key and the
index actually written stay the same project.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
NestJS controllers produced zero Route nodes. Two gaps in the decorator
route walk (extract_route_from_decorators):
- It only inspected decorator children of kind `call` (Python). A
TypeScript decorator's call is a `call_expression`, so @Get(':id') was
never examined.
- The path helpers only accept literals that start with "/". Nest writes
segments without one (@Controller('users'), @Get(':id')), and the
class-level prefix was composed only for Java/Kotlin (Spring/JAX-RS).
TS/TSX class methods now read the Nest verb decorators (@Get, @Post, @Put,
@Delete, @Patch, @Head, @Options, @All -> ANY) and compose them with the
class's @Controller / @JsonController prefix. The prefix can be a string,
an array (first entry), or the options object's `path`. Paths are rooted
at "/". A verb decorator outside a controller class forms no route. A
non-literal path argument (a constant) also forms none, so no route is
invented. Route and HANDLES are reused as they are: no new label.
Not covered: the app-level setGlobalPrefix() and enableVersioning() in
main.ts are cross-file and are not applied.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A plain '&' inside JSX -- a URL query in an attribute
(href="https://fonts.googleapis.com/css2?family=Inter&display=swap"),
or text such as <p>Tom & Jerry</p> -- produced a tree-sitter ERROR node
in .js, .jsx and .tsx files. The file was flagged parse_partial in the
coverage view, and the error recovery turned words from the URL
(create-next-app -> create/next/app) into identifiers that resolved to
unrelated symbols as false USAGE edges.
Root cause is upstream tree-sitter-javascript, which the tsx dialect
builds on: unescaped_{double,single}_jsx_string_fragment is
/([^"&]|&[^#A-Za-z])+/, so '&' is only accepted before a space/digit
or as a complete html_character_reference (& / &). jsx_text is
scanned by the external scanner, which stops at every '&', and nothing
in _jsx_child accepted a '&' that is not a reference. So '&' + letter in
a string, and any lone '&' in JSX text, had no valid token.
Fix: javascript and tsx become self-maintained forks of the pins we
already vendored (javascript @ 58404d8cf191; typescript tsx @
75b3874edb2d on its javascript base @ 3a837b6f3658), following the
arkts model. One patch (tools/tree-sitter-javascript/patches/
jsx-lone-ampersand.patch) is applied to both javascript bases:
- a one-character '&' token at lexical precedence -1, aliased to the
existing string_fragment (in strings) and jsx_text (in text); the
lower precedence keeps & / & lexing as html_character_reference
- the string-fragment regexes exclude a '&' directly before the closing
quote, so "a&" no longer runs past the end of the string
Node types are unchanged (same type/named set). The external scanners
stay upstream bytes. Parsers regenerated with tree-sitter-cli 0.25.10
(ABI 15; tsx moves from ABI 14, and its tree_sitter/ headers now match
arkts). Without the patch both fork dirs regenerate upstream exactly.
The upstream corpus passes unchanged: javascript 116/116; tsx gives the
same results as unpatched upstream.
Proof on vercel/next.js examples/ (canary 725705bd, 1,722 js/jsx/tsx
files): parse_partial 92 -> 47 (tsx 33 -> 0, jsx 2 -> 0, js 12 -> 2; the
two left are Babel do-expressions and Flow types), no newly flagged
file, code-graph nodes 14,739 -> 14,739, edges 24,773 -> 24,657: the
115 USAGE edges removed were all false cross-example edges from URL
fragments, plus a 4 SIMILAR_TO / +3 SEMANTICALLY_RELATED reshuffle
from the corrected ASTs.
Registered like arkts: MANIFEST.md fork rows + notes (ABI tally
recounted), audit FORKS map, THIRD_PARTY.md, vendored checksums.
LICENSEs stay upstream's MIT byte-identical.
Tests: parse_coverage gains a .js/.jsx/.tsx matrix (double/single-quoted
attribute, '&' before the quote, '&' + letter/space/digit in text, plus
&/& controls) and a guard that broken JSX is still flagged.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
An OPTIONAL MATCH with a relationship, whose start and terminal
variables are not bound by an earlier MATCH and whose start node
pattern matches no node, is joined in cross_join_with_rels() with an
empty candidate list. That path sized its output for a single binding,
but the OPTIONAL fallback then kept one row per existing binding with a
plain indexed store, so every row after the first was written past the
end of the allocation.
Route the fallback through binding_out_append(), the growing writer
that the expansion branch of the same function and the other OPTIONAL
fallbacks already use. The node-only join (cross_join_nodes) was fixed
earlier by sizing its buffer for the fallback rows; the relationship
variant kept the unchecked store.
Add a cypher regression test that runs such a query over four bound
functions and asserts one row per function with the optional variables
unbound. Before this change the test runner reported a
heap-buffer-overflow in cross_join_with_rels under ASan.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>