Commit Graph
1521 Commits
Author SHA1 Message Date
wargloom cabece18f1 fix(mcp): return structured compact results
Tools advertise an object outputSchema, so strict MCP clients reject compact TOON responses that omit structuredContent.

Signed-off-by: wargloom <wargloom@gmail.com>
2026-07-12 13:55:08 +03:00
Martin Vogel 8c9ac98e30 Merge pull request #1036 from DeusData/fix/797-varlength-paths
fix(cypher): shortest-path var-length semantics + advertised depth clamp
2026-07-11 23:54:12 +02:00
Martin Vogel d0626b0508 fix(cypher): keep cypher.c windows.h-free — use _Thread_local directly
The compat.h include added for CBM_TLS pulled windows.h into cypher.c,
whose legacy 'far' macro (windef.h defines it empty) broke the
existing EXISTS-pattern local of that name on every Windows leg. Use
C11 _Thread_local for the depth-clamp flag instead; cypher.c stays
free of windows.h.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 22:30:53 +02:00
Martin Vogel 73a3c347d9 fix(cypher): shortest-path var-length semantics + advertised depth clamp
Three of #797's four findings fixed (the fourth — repeated-variable
unification — already holds on main and is now pinned by the test):

- Walk padding: the BFS recursive CTE deduped (node, hop) PAIRS, so a
  single self-loop minted every hop level for every node it could
  reach — *k..k matched fabricated walks of arbitrary length (a
  100-hop 'longest chain' artifact) and the row set exploded to
  nodes x depth (the reported crash). The traversal now returns each
  node once at its MINIMAL hop (MIN/GROUP BY): variable-length matches
  nodes whose shortest path lies in [min,max]. This is deliberately
  shortest-path semantics, not openCypher trail enumeration — right
  for reachability/depth audits on call graphs and linear instead of
  exponential; trace_path shares the traversal and now also reports
  each node once on cyclic graphs.
- Silent 100-hop cap: a clamped hop range now surfaces as
  result->warning ('clamped ... an empty result may mean clamped, not
  no such path'), emitted by query_graph in both TOON and JSON forms,
  on top of the existing log WARN.

Reproduce-first: cypher_exec_varlength_path_semantics_issue797 —
self-loop + real 2-chain fixture; RED on the walk semantics (*2..2
returned 3 endpoints instead of 1), GREEN now, incl. variable
unification, zero rows for nonexistent path lengths, and the clamp
warning assertion.

Closes #797

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 20:58:28 +02:00
Martin Vogel e19a699493 Merge pull request #1034 from DeusData/fix/874-cypher-coalesce
feat(cypher): coalesce(var.prop, literal) in WHERE clauses
2026-07-11 20:33:07 +02:00
Martin Vogel 4382b97908 feat(cypher): coalesce(var.prop, literal) in WHERE clauses
The WHERE parser rejected coalesce() outright ('unexpected operator'),
blocking null-safe numeric filters over optional graph properties —
the exact audit-query shape from the report:
  MATCH (f:Function) WHERE coalesce(f.transitive_loop_depth, 0) >= 2
RETURN-side coalesce already worked; only the WHERE leaf lacked it.

Implementation: the condition leaf gains a coalesce_default literal.
The operator/value tail of condition parsing is factored into a shared
parse_condition_op so both subjects (var[.prop] and
coalesce(var.prop, literal)) accept the full operator set (comparisons,
IS [NOT] NULL, IN). At eval time a missing/empty property value is
substituted with the default before the operator runs. Supported form
is coalesce(var.prop, literal) — matching openCypher's two-argument
null-substitution for this subset.

Reproduce-first: cypher_exec_where_coalesce_issue874 runs the
reporter's exact query — RED (rc=-1 parse error) on the old parser —
and pins both directions: a failing default excludes property-less
nodes, a passing default includes them.

Closes #874

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 19:10:07 +02:00
Martin Vogel ae956495cd Merge pull request #1030 from DeusData/fix/898-sequential-json
fix(pipeline): valid JSON for sequential-path service edges and Route nodes
2026-07-11 17:23:50 +02:00
Martin Vogel 93740cedd6 fix(branch): restore files clobbered by a stale-base soft reset
The previous commit accidentally staged a stale-main vs current-main
delta (a git reset --soft against an outdated origin/main ref),
reverting the #1028 wide-canonicalization work and #1027's regression
test. Restore every touched file to current main; the net branch diff
is now exactly the intended two-file #898 fix (pass_calls.c +
tests/test_mcp.c).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 16:01:04 +02:00
Martin Vogel 51921d9063 fix(pipeline): valid JSON for sequential-path service edges and Route nodes
Two emission bugs in the sequential pipeline (pass_calls.c) — the
parallel path was correct, so the two pipelines produced different
bytes for the same input:

- Brokered ASYNC_CALLS edge props closed the method value's quote but
  not the broker's, emitting "broker":"bullmq} on EVERY such edge;
  the fixup below only repaired truncation, which never fires in the
  normal case. Rebuilt as the parallel path's incremental snprintf.
- create_svc_route_node stored the RAW method/broker string as the
  synthesized Route node's properties (literally 'bullmq' instead of
  {"broker":"bullmq"}). Now emits the parallel path's exact JSON.

Consequences of the malformed rows: json_extract errors across the
query surfaces, the edges generated columns (local_name_gen, url_path
index) fail, and PRAGMA quick_check aborts with 'malformed JSON' —
which since the artifact deep-integrity check (#895) also means such
caches are refused at import.

Reproduce-first: sequential_service_edge_props_are_valid_json_issue898
indexes a small celery fixture (sequential path), asserts >=1
ASYNC_CALLS edge exists (non-vacuous), every edge/node properties blob
is json_valid, and the broker is json_extract-able exactly like the
parallel path emits it. RED on the old emitters (1 malformed edge +
1 malformed Route node), GREEN now.

Note: the reporter's original bullmq JS repro no longer classifies on
current main because CommonJS require()/ESM import resolution has a
separate gap — that is exactly open issue #871; probed and confirmed
while building this fix.

Closes #898

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 15:54:58 +02:00
Martin Vogel 8f0f14b028 Merge pull request #1028 from DeusData/fix/973-wide-canonicalization
fix(win): wide-path canonicalization — drop ANSI _access/_fullpath
2026-07-11 15:30:51 +02:00
Martin Vogel aca02e06ef Merge pull request #1027 from DeusData/fix/773-parser-allocator-epoch
fix(pipeline): destroy TLS parsers at allocator-epoch boundaries (second-index SIGABRT)
2026-07-11 14:13:57 +02:00
Martin Vogel 14198fc1e1 fix(win): wide-path canonicalization — drop ANSI _access/_fullpath (#973)
Three path-canonicalization sites routed UTF-8 paths through the ANSI
CRT (_access + _fullpath): the repo_path canonicalization in
handle_index_repository, project-name derivation in fqn.c, and
resolve_canonical_path (the containment check). ANSI decoding is
locale-dependent by construction — on a CJK system codepage (the
reporter runs Big5) the UTF-8 bytes of a CJK repo path re-decode into
different characters, corrupting canonicalization while our CI (en-US
codepage) sails through: that is why the existing non-ASCII guards
stayed green while v0.9.0 crashed in the field.

New cbm_canonical_path: realpath on POSIX (unchanged behavior); on
Windows a wide GetFileAttributesW existence check + GetFullPathNameW,
never touching the ANSI codepage. resolve_canonical_path now also
requires existence on Windows, aligning it with the POSIX realpath
semantics it mirrors.

Guard: the Windows non-ASCII CLI guard gains the reporter's exact
shape — Traditional-Chinese directory (雷達測試), flag-form
--repo-path + --mode fast, real supervised spawn. Honest caveat: CI
cannot switch the system codepage, so the guard validates the
mechanics under en-US; the locale-specific corruption is fixed by
construction (no ANSI path decoding remains on these sites).

Closes #973

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 14:08:24 +02:00
Martin Vogel 2f7befb9ad fix(pipeline): destroy TLS parsers at allocator-epoch boundaries
SIGABRT (free of a non-libmalloc pointer in ts_stack_delete) on the
second index_repository in one process, whenever a sequential run is
followed by a parallel one. The sequential path parses on the CALLING
thread and left its TLS parser alive; the next parallel run switches
the global tree-sitter allocator to the slab (cbm_slab_install), the
calling thread participates in extraction, reuses the stale parser,
and its per-file teardown frees mimalloc-epoch memory through
slab_free's foreign-pointer fallback -> plain free() -> libmalloc
abort. The v0.9.0 supervisor masks this on the default MCP path (fresh
worker process per index); the in-process path and every embedded
consumer died. Root-caused with allocator-epoch instrumentation: the
victim parser is created at epoch 0 in cbm_pipeline_pass_definitions
and deleted at epoch 1 in extract_worker.

Two boundary fixes:
- run_sequential_pipeline destroys the calling thread's parser at run
  end (also bounds its memory like the parallel workers already do);
- cbm_parallel_extract destroys the calling thread's parser BEFORE
  cbm_slab_install, so no parser ever crosses the allocator switch.

Guards: prod-tier smoke invariant inv_second_index_inprocess
(sequential-then-parallel double index over stdio, RED with signal 6
on the unfixed binary — ASan builds mask the allocator seam, so the
unit tier cannot catch this class) + suite-tier fork-isolated
index_second_inprocess_run_survives_issue773 pinning the in-process
double-index contract. Repro 5/5 clean after the fix (was 5/5 abort).

Closes #773

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 12:51:29 +02:00
Martin Vogel 1a38d0a2e8 Merge pull request #1016 from DeusData/fix/stale-wal-sidecars
fix(store): remove destination WAL/SHM sidecars before installing a fresh DB
2026-07-11 05:36:24 +02:00
Martin Vogel d7e6082be7 Merge pull request #1019 from DeusData/fix/artifact-torn-export
fix(artifact): snapshot exports consistently and refuse corrupt imports
2026-07-11 04:51:02 +02:00
Martin Vogel 4ac5bb6d80 Merge pull request #1018 from DeusData/fix/surface-sqlite-corrupt
fix(store): surface SQLITE_CORRUPT from row scans instead of truncating
2026-07-11 04:36:52 +02:00
Martin Vogel 43d84ee6f2 fix(store): replace-on-rename for dump installs on Windows
The new stale-sidecar guard exposed a pre-existing Windows gap in the
same code path: rename() fails with EEXIST when the destination
exists, so cbm_store_dump_to_file could never install over a previous
DB generation on Windows. New cbm_rename_replace uses
MoveFileExW(MOVEFILE_REPLACE_EXISTING) with wide paths (raw
MoveFileExA would re-mangle non-ASCII cache paths); POSIX keeps plain
atomic rename.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 04:14:04 +02:00
Martin Vogel 3ec08b4623 Merge pull request #1015 from DeusData/fix/mcp-discovery-empty-lists
fix(mcp): answer resources/prompts discovery with empty lists, not -32601
2026-07-11 03:48:41 +02:00
Martin Vogel b79c6da931 fix(artifact): snapshot exports consistently and refuse corrupt imports
The FAST export path (the watcher/incremental auto-update) read the
raw main-file bytes of a live WAL-mode store: committed rows still in
the -wal were missing entirely and mid-checkpoint reads produced torn
snapshots (field artifact: four short indexes + btreeInitPage error 11
across ~12 nodes-table pages). Import then installed the torn file
byte-for-byte — the integrity check only sanity-checked the projects
table.

- Export: both quality levels now snapshot via VACUUM INTO (the
  consistent copy); only the index-stripping remains BEST-only, FAST
  keeps zstd-3. The fast path pays a full-copy VACUUM INTO now —
  correctness over speed; a torn team-shared artifact costs far more.
- Import: new cbm_store_check_integrity_deep (shallow check + PRAGMA
  quick_check) — used only on this rare path, hot-path callers keep
  the cheap shallow check.

Reproduce-first: artifact_fast_export_snapshots_live_wal_store
exports FAST while the writer connection is open — RED on the old
code with 0 of 60 nodes surviving the round-trip (all rows lived in
the WAL); GREEN now. store_deep_integrity_detects_page_corruption
pins the import guard both directions (healthy passes, zero-banded
pages refused).

Closes #895

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 03:28:32 +02:00
Martin Vogel b422a16889 fix(store): surface SQLITE_CORRUPT from row scans instead of truncating
Every row-scan loop discarded the terminal sqlite3_step code, so
SQLITE_CORRUPT mid-scan was indistinguishable from a clean end of
results: against a page-corrupted DB, counts answered from covering
indexes stayed correct while row fetches died at the first corrupt
table page — every query surface returned plausible truncated/empty
answers with no error (search total=379 with results=[], Cypher counts
diverging from schema counts, BM25 total=0, empty schema sections).

All 31 scan loops now capture the terminal code and fail loudly with
CBM_STORE_ERR + a store error message when a scan aborts, with
per-site cleanup (partial results freed or handed to the caller's
normal free path). Two deliberate soft spots, both enrichment-only and
documented inline: include_connected neighbor names (no store handle)
and the cluster adjacency scan (partial adjacency; primary surfaces
already fail loudly). Best-effort schema property-name discovery and
two capped helper scans keep their by-design early-exit semantics.

Reproduce-first: corrupt_page_scan_returns_error_not_truncation
zeroes a 40-page mid-file band per the issue's dd recipe and asserts
scans return CBM_STORE_ERR. RED on the old code with the exact
reported symptom (OK with 264 of 2000 rows silently truncated); GREEN
now. Fixture-health guards make a healthy-scan pass impossible to
mistake for a fix.

Closes #896

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 03:13:45 +02:00
Martin Vogel 394198e8e9 fix(store): remove destination WAL/SHM sidecars before installing a fresh DB
Three code paths install a brand-new DB file where a previous
generation lived without removing the destination's -wal/-shm:
the pipeline page writer (cbm_writer_open), artifact import (which
cleaned the TMP file's sidecars but not the destination's), and
cbm_store_dump_to_file (whose comment wrongly asserted stale sidecars
'become stale and get recreated'). SQLite validates a WAL against the
sidecar's own header/checksums — not the main file — so a crashed
session's leftover WAL is replayed ON TOP of the freshly installed
file at the next open, splicing old-generation pages into it.

New shared helper cbm_remove_db_sidecars (compat_fs, wide-path safe on
Windows) called at all three install points.

Reproduce-first: dump_install_ignores_stale_wal_sidecar follows the
issue's recipe (hot-copy a live WAL, clean close, restore the copy,
install generation 2, reopen). RED on the unguarded code — the replay
clobbered the fresh generation entirely (fresh row unqueryable);
GREEN with the cleanup. Non-vacuous: asserts the hot WAL is non-empty
before the copy.

Closes #897

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 02:40:54 +02:00
Martin Vogel a432cf729e fix(mcp): answer resources/prompts discovery with empty lists, not -32601
Clients like Cline probe resources/list, prompts/list and
resources/templates/list on connect regardless of declared server
capabilities; the -32601 Method-not-found errors surfaced as a failed
connection (status=error) even though tools worked fine. Empty lists
are the interoperable answer for a tools-only server.

Reproduce-first: mcp_discovery_methods_return_empty_lists is RED on
the unhandled dispatch (Method not found) and GREEN with the empty
lists.

Closes #958

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 02:25:27 +02:00
Martin Vogel 751d2293f9 Merge pull request #1013 from DeusData/feat/compact-tool-output
feat(mcp): TOON compact tool output — ~10x fewer response tokens across the query surface
2026-07-11 02:13:00 +02:00
Martin Vogel be9976dd31 fix(hook-augment): request format=json for the internal search_graph call
search_graph now defaults to TOON text, but ha_format_context parses
the inner payload as JSON ("results"), so the Grep/Glob/Read hook
augmenter silently produced no hookSpecificOutput — caught by the
test_hook_augment.py Windows guard going red. Both internal call sites
route through ha_build_args; request the JSON encoding there.

Verified end-to-end: indexed fixture + PreToolUse payload via stdin
emits the symbol context again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 00:23:36 +02:00
Martin Vogel bbeedf3775 test(windows): request format=json in the non-ascii guard's query_graph calls
query_graph now defaults to TOON text; the guard's definition-count
probe parsed it as JSON, swallowed the parse failure, and reported the
ASCII baseline as having zero definitions (SETUP FAIL on the
test-windows-guards leg). Scripted consumers use the format="json"
escape hatch.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 22:53:44 +02:00
Martin Vogel bd996c8b91 Merge pull request #1012 from DeusData/fix/cli-unknown-flag-reject
fix(cli): reject unknown tool flags with a nearest-flag suggestion
2026-07-10 22:35:23 +02:00
Martin Vogel 0756b2b366 Merge pull request #1011 from DeusData/fix/import-map-suffix-swallow
fix(registry): don't swallow call suffixes on aliased-import direct hits
2026-07-10 22:29:54 +02:00
Martin Vogel 0fd175cf54 feat(mcp): TOON output for trace/query/search_code/architecture + context-bomb defaults
Completes the compact-output pass across the remaining query tools
(consumers are LLM agents; every response byte is context tokens):

- trace_path: callees[N]{qn,hop}/callers[N]{qn,hop} tables (risk/test/
  args columns per flags); the per-hop JSON key envelope was 84% of the
  payload. 3,589B -> 1,634B on a representative trace.
- query_graph: rows[N]{cols}: table (columns double as the header);
  format:"json" restores columns/rows arrays.
- search_code compact mode: results/raw/dirs tables; drops the
  field (duplicate of the qn tail). 2,488B -> 1,135B for 17 hits.
- search_code full mode: per-hit source capped at a 60-line window
  anchored on the first match, with source_start/source_truncated
  markers — uncapped whole-symbol dumps ran to 142KB per response; the
  complete symbol stays one get_code_snippet call away.
- get_architecture: default (no aspects) is now a summary (languages,
  packages, entry_points + always-on totals/label/type counts) with an
  aspects_hint — the old default rendered EVERYTHING including the full
  file_tree (94KB -> 4.5KB -> 2.8KB with TOON tables); all sections
  emit as TOON tables, aspects:[...] and ["all"] keep full access
  (52KB vs 94KB even for the explicit full dump).
- get_code_snippet: drops the property-blob enrichment (41% of the
  response; signature/docstring are literally in the source; fp/sp/bt
  never belonged there). Metrics remain reachable via search_graph
  fields=[...].
- list_projects: per-project 12-field git block (mostly null) replaced
  by the branch name; name/root/nodes/edges/size kept.
- search_graph description: in/out documented as TOTAL all-edge degree,
  NOT caller/callee counts (agent evals mis-read it as callers).
- format:"json" escape hatch on every converted tool.

Guards: tool_output_byte_budgets pins absolute byte ceilings on default
search/trace outputs (a property blob sneaking back into row emission
blows them immediately); snippet_enriched_properties inverted to pin
the no-spill contract. Suite 5,984 green; smoke updated to the TOON
contract end-to-end.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 21:18:56 +02:00
Martin Vogel 2950e4ec03 fix(cli): reject unknown tool flags with a nearest-flag suggestion
cbm_cli_build_args_json typed unknown flags as strings and shipped them
as JSON args the server ignores: 'trace_path --max-depth 1' succeeded
but traced at the default depth 3 — silently-wrong output that inflated
an agent evaluation by thousands of tokens of unwanted hop-2/3 rows.

Unknown flags for a known tool now fail loudly with a Levenshtein
nearest-flag suggestion:
  unknown flag --max-depth for this tool (did you mean --depth?) — run
  'cli trace_path --help' for the supported flags

Unknown-tool (no schema), raw-JSON, --args-file, stdin, and supervisor
worker argv paths are unaffected — none of them route through the flag
converter.

Reproduce-first: cli_build_args_json_unknown_flag_rejected is RED on
the permissive code (JSON produced) and GREEN with the rejection.

Closes #997

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 21:12:33 +02:00
Martin Vogel afcce2d42b fix(registry): don't swallow call suffixes on aliased-import direct hits
The #979 direct-hit early return in resolve_import_map fires before
suffix handling, so any imported_symbol.method() call whose base symbol
is itself an indexed node resolved to the base node instead of
base.method — the exact mis-resolution the neighboring comment warns
about. On django this degraded the graph by ~11K edges (CALLS
74,685->72,175, TESTS 38,032->29,236); auth.login => Signal.send became
an edge onto the user_logged_in Variable node.

Take the direct hit only for suffix-less callees — precisely the
aliased direct-symbol import case (#875) the early return was added
for. Verified on django: CALLS 74,708 (baseline + the 23 genuine #979
wins), TESTS 38,035; the #979 bare-alias test stays green.

Reproduce-first: resolve_import_map_alias_with_suffix_hits_method is
RED on the unguarded code (returns the bare symbol QN) and GREEN with
the guard.

Closes #1000

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 21:07:11 +02:00
Martin Vogel 4843a34065 feat(mcp): TOON compact output for search_graph — ~90% fewer response tokens
Tool responses land in an LLM agent's context window, where the legacy
per-node JSON objects were dominated by data no agent can use: the
similarity/semantic pipeline intermediates fp (~450B minhash hex), bt
(body-token bag) and sp (structural profile) plus 14 complexity metrics
were grafted onto every result by enrich_node_properties — ~64% of every
node emission, ~370 tokens per search hit.

search_graph now defaults to TOON (Token-Oriented Object Notation,
toonformat.dev): scalars as 'key: value' lines and results as a
'results[N]{qn,label,file,lines,in,out}:' header + one row per hit.
Published benchmarks show equal-or-better LLM retrieval accuracy at
40-60% fewer tokens than JSON; rows now also carry line ranges, which
the legacy output lacked.

- fields:[...] opts into per-node property columns (complexity,
  signature, docstring, ...); fp/sp/bt are blocklisted even there
- format:"json" restores the legacy verbose objects byte-identically;
  include_connected forces it (nested neighbor lists)
- BM25 and semantic modes emit the same table shape (rank/score column)
- semantic-only calls no longer run the unfiltered regex search that
  silently prepended up to 200 enriched nodes (102.8KB -> 4.2KB)
- default limit 200 -> 50 (BM25 100 -> 50): cheap first page, page via
  has_more/offset as before
- new emitter module src/mcp/compact_out.{c,h} (string builder + TOON
  scalar/table emission with spec quoting rules)

Measured on a real index (14-hit regex search): 20,793B -> 1,327B
(-94%); BM25 19,667B -> 5,081B; semantic 102,837B -> 4,181B. A typical
discovery session drops ~50.9K -> ~4.7K tokens combined with the
follow-up slices.

Guards: tool_search_graph_toon_never_leaks_internal_fields (sentinel
fp/sp/bt never emitted, even when requested), rewritten
tool_search_graph_includes_node_properties pins default-lean/fields-
opt-in/json-escape-hatch; suite 5983 passed, smoke green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 08:23:19 +02:00
Martin Vogel 6b57db271a Merge pull request #990 from DeusData/fix/issue-labeler-regex
fix(ci): issue labeler no-oped on every issue — (?i) is invalid in JS RegExp
2026-07-10 01:26:28 +02:00
Martin Vogel 9a497c0fe6 Merge pull request #989 from DeusData/chore/lsp-originality-perl
chore(lsp): add Perl reference to originality-check manifest
2026-07-10 01:06:40 +02:00
Martin Vogel 22a8ff1f9a Merge pull request #987 from DeusData/fix/relative-repopath-guard
test(mcp): regression guard for relative repo_path canonicalization (#794)
2026-07-10 00:48:53 +02:00
Martin Vogel fff4761d37 fix(ci): issue labeler no-oped on every issue — (?i) is invalid in JS RegExp
github/issue-labeler compiles patterns with JavaScript RegExp, which
rejects PCRE-only inline (?i) groups with SyntaxError: Invalid group, so
the workflow failed before evaluating any rule and no issue ever got an
area label (#764).

Replace the third-party action with first-party actions/github-script:
the same config rules are compiled with the i flag applied centrally
(case-insensitivity preserved), labels stay additive-only via addLabels,
and a pattern that fails to compile now fails the run loudly instead of
silently no-oping. Strip (?i) from all 7 config patterns and document
the line format the workflow parses.

Verified locally: all 7 rules compile and match expected labels across
sample issue texts; the old pattern reproduces the SyntaxError.

Closes #764

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-10 00:03:43 +02:00
Martin Vogel f7127ee841 Merge pull request #986 from DeusData/fix/php-trait-alias-oom-guards
test(php-lsp): regression guards for aliased-trait name-collision OOMs (#765, #951)
2026-07-09 23:57:35 +02:00
Martin Vogel 10390f184c chore(lsp): add Perl reference to originality-check manifest
Perl LSP landed in #461; scan its implementation against PerlNavigator
(MIT, the leading OSS Perl language server) for defensive
copy-detection, same rationale as the phpactor entry. Verified clean:
no verbatim string/comment overlap.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 23:43:41 +02:00
Martin Vogel 2dca0a39be Merge pull request #461 from halindrome/perl-lsp-semantic-resolution
feat(perl-lsp): Perl LSP-tier semantic resolution (Closes #459)
2026-07-09 23:37:34 +02:00
Martin Vogel ca7c7f4515 fix(test): realpath fortify abort + Windows compile in repo-path guard
glibc _FORTIFY_SOURCE aborts realpath() into a sub-PATH_MAX buffer
regardless of the actual path length, and Windows has no realpath at
all. Size the buffers at PATH_MAX and skip the guard on Windows (the
#794 canonicalization logic is platform-independent; the four Unix
legs carry it).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 23:22:34 +02:00
Martin Vogel 97fcae0fad test(mcp): regression guard for relative repo_path canonicalization
On v0.8.1, index_repository with repo_path "." flowed the literal dot
through everything derived from the path: the project was named
"root", the projects table stored root_path="." verbatim, the
post-dump integrity check flagged bad_root_path and auto-deleted the
entire DB — while the response still reported status "indexed" with
full node counts. Verified fixed on current main (the path is
canonicalized before derivation: correct project name, absolute
root_path, persistent and queryable DB).

The guard indexes with repo_path "." from inside the repo and asserts
the three things the old failure mode broke: path-derived project name
(never "root"), absolute canonical root_path in the projects table,
and a DB that survives the run and answers queries.

Closes #794

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 22:37:24 +02:00
Martin Vogel df7cae4d83 style: restore original test_php_lsp.c formatting
The previous commit's clang-format pass reformatted the whole file
(tests are not in the lint-enforced LINT_SRCS set, so the committed
file was never format-clean under the local formatter). Restore the
original formatting; the branch's net diff against main is now exactly
the two new regression guards.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 22:32:26 +02:00
Martin Vogel 7f5d2d8bcc test(php-lsp): regression guards for aliased-trait name-collision OOMs
Two reported indexer OOMs reduce to the same self-flattening loop the
trait-uses-itself fix closed (self-flatten short-circuit + snapshotted
loop bound in flatten_trait_into_class):

- a trait whose name collides with an aliased `use ... as` import it
  composes (#765's exact single-file reproducer) — the alias
  canonicalizes onto the enclosing trait via the short-name fallback
- a class sharing its short name with an aliased trait import (#951's
  reproducer, reduced to one file: the trait definition being
  unavailable is what forces the short-name fallback onto the class)

Both fixtures were verified RED against the pre-guard code (each hangs
the suite within the 90s alarm when the two guards are reverted, each
independently as the first test run) and GREEN with the guards in
place. Landing them as permanent regression guards; the issues close as
fixed-by-the-#920-pair.

Closes #765
Closes #951

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 22:24:11 +02:00
Shane McCarron 0815d78377 fix(perl-lsp): iterate wide AST children via cursor, batch method-table build
Two O(n^2) patterns in the Perl LSP, the class the ARM
extract_wide_flat_file_is_linear guard flags:

- Every child-iteration loop used indexed ts_node_child(node, i), which is
  O(i) per call and O(n^2) over a wide flat node. Route them all through a new
  perl_collect_children() helper mirroring wd_collect_children() in
  extract_defs.c: a forward TSTreeCursor collect for wide nodes (>= 64
  children), NULL (cheap indexed access) for the small common case.

- perl_attach_methods rebuilt each package's whole NULL-terminated method
  array on every sub via perl_type_add_method, O(subs^2) on a single-package
  file. Replace with a collect -> qsort-by-package -> build-once batch
  (O(S log S)); source order within a package is preserved so first-defined
  still wins on redefinition.

No behavior change: all perllsp_* resolution tests and
lsp_perl_deep_expression_no_crash pass; check-lsp-originality.sh reports clean.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:47:33 -05:00
Shane McCarron 89aecfd2fc fix(perl-lsp): guard pathologically nested Perl before tree-sitter parse
A deeply nested Perl call chain f(f(f(...))) overflowed the stack while
parsing on small-stack platforms: tree-sitter's GLR parser recurses once
per nesting level in stack_node_add_link (vendored ts_runtime/src/stack.c)
merging the ambiguous parse-stack heads that Perl's f(...) grammar produces.
The overflow happens during ts_parser_parse, before any LSP walk runs, so
the CBM_LSP_PERL_MAX_WALK_DEPTH guards can never fire. This crashed
test-windows (CLANG64) with an AddressSanitizer stack-overflow and hung
test-unix (ubuntu-24.04-arm) to a 4h timeout on lsp_perl_deep_expression_no_crash.

Add cbm_source_nesting_exceeds(): an O(n) early-exit bracket-depth scan, and
skip extraction (clean has_error, zero edges) for Perl input whose nesting
exceeds CBM_PERL_MAX_PARSE_NESTING (128) before it reaches tree-sitter.
Scoped to Perl because only its ambiguous call grammar drives the GLR
recursion to the nesting depth; C/Java/Python parse the same shape with a
single stack head. 128 is ~10x above any real Perl nesting and well below
the depth where the parser overflows a 1MB stack.

Correct the lsp_perl_deep_expression_no_crash comment to describe the actual
mechanism (parser recursion, not the LSP walk).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 868c4097f5 docs(perl-lsp): document Perl in the Hybrid LSP section
Perl gained a full Hybrid LSP tier in this PR but was absent from the Hybrid LSP docs. Add a capabilities row to the "Languages with full Hybrid LSP" table, list Perl in the intro + features language lists, and bump the badge from 9 to 10 languages. Perl was already in the general language-support list and the resolution-quality tier (grammar-level); this covers the new type-resolution tier.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron a0571049b7 fix(perl-lsp): address QA round 2 follow-ups
Optional, non-blocking follow-ups from the #461 review:

- perl_eval_function_call_type: split qualified static calls on the LAST
  "::" (mirroring perl_resolve_function_call) so return-type chaining on
  Foo::Bar::sub() resolves instead of yielding unknown.
- cbm_run_perl_lsp: finalize the type registry into a per-call scratch
  arena (as php_lsp/java_lsp do) before resolution, so method/func/symbol
  lookups use O(1) hash buckets instead of a per-file linear scan.
- perl_infer_self_type: match the `shift` invocant idiom at word
  boundaries and accept `$_[0]`, so `my $self = shifty();` no longer
  mis-binds the receiver (tightens the zero-edge guarantee).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron f3e6bcf713 chore(planning): drop bundled VBW planning summary from PR
Remove .vbw-planning/phases/22-perl-lsp-semantic-resolution/22-03-SUMMARY.md
per maintainer request — internal planning/scratch doc that should not land
in the repo.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron a100820243 fix(perl-lsp): split qualified calls on last "::" so multi-level packages resolve
perl_resolve_function_call split a package-qualified call name on the FIRST
"::" via strstr. For multi-level packages (Foo::Bar::sub) this yielded
pkg "Foo" and a sub name still containing "::" ("Bar::sub"), which never
resolved. The call then fell through to the generic bare-name fallback, which
matches on the bare sub name only — collapsing distinct packages' same-named
subs onto a single winner and orphaning the rest. Single-level calls
(Foo::sub) were unaffected, which is why the existing test missed it.

Split on the LAST "::" instead so the full package name reaches
perl_lookup_method, which resolves to the correct fully-qualified sub.

Adds perllsp_static_multilevel_package_call covering the multi-level form.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron aa97ec55f2 fix(perl-lsp): address QA round 1
F1: dot the Exporter import target so seeded CPAN exported subs resolve.
perl_collect_qw_imports built colon-form targets (Scalar::Util::blessed)
but the stdlib registry keys curated CPAN subs in dotted form
(Scalar.Util.blessed) and lookup is exact-match. Wire in perl_pkg_to_dot
to dot the module portion and drop the now-unnecessary (void) cast.

F2: add a recursion-depth guard (CBM_LSP_PERL_MAX_WALK_DEPTH=512) to both
AST walkers (perl_resolve_calls_in_node, perl_pass1_scan) via a depth-
guarded wrapper + inner split, mirroring java_lsp's JAVA_LSP_MAX_WALK_DEPTH.
Past the cap a subtree is skipped (graceful degradation, no wrong edge),
preventing stack overflow on pathologically nested input.

F3: lock the shared last-"::"-segment normalization in lsp_resolve.h with a
direct regression test over cbm_pipeline_find_lsp_resolution: a qualified
static call still resolves AND the cross-namespace mis-attribution edge case
is bounded by caller-QN equality + the confidence floor.

F4: implement SUPER:: dispatch. Populate enclosing_parent_qn from the
enclosing package's first @ISA parent and resolve $self->SUPER::method() to
that parent's method (strategy perl_method_super). No known parent or
unresolved method emits no edge (zero-edge guarantee preserved).

Tests: perllsp_cpan_exported_function, perllsp_super_dispatch,
perllsp_super_no_parent_no_edge, lsp_perl_deep_expression_no_crash,
lsp_resolve_qualified_static_call_normalizes_colons,
lsp_resolve_misattribution_is_bounded.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 83f907a84c test(perl-lsp): add Perl LSP resolution suite mirroring test_php_lsp.c
Replace the plan-01 placeholder with the full Perl LSP test suite covering the
ten foundational resolution scenarios from 22-RESEARCH.md, plus the extract_perl
/ find_resolved / require_resolved / find_resolved_with_strategy helpers cloned
from test_php_lsp.c:

  1. method via bless-assignment      6. use parent MRO
  2. constructor class-method type    7. use base MRO
  3. static package call              8. Exporter import (use Mod qw(f))
  4. $self method dispatch            9. require fallback
  5. @ISA inheritance               10. unresolvable receiver -> zero edges

Assertions match the resolver's actual QN scheme (module_qn.subname, dotted —
the Perl package governs dispatch, not the emitted QN), and the negative test
confirms the zero-edge guarantee for untyped scalar and unindexed package
receivers. Register suite_perl_lsp in test_main.c so scripts/test.sh runs it.

All 10 perl_lsp tests pass (3563 passed, 1 pre-existing unrelated failure:
search_code_multi_word in tests/test_mcp.c).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00