json_extract_prop() scanned a non-string property up to the first ',' and a
string property up to the first '"', ignoring nesting and backslash escapes.
Any array/object property was therefore truncated at its first INTERNAL comma,
and any string containing an escaped quote was cut short.
decorators: ["@Roles('OWNER', 'ADMIN')","@Get()"]
projected as: ["@Roles('OWNER'
This makes decorator/route/authz queries unusable on frameworks whose
decorators take multiple arguments (NestJS, Angular, Spring). Values are now
copied as balanced constructs, honoring string state and escape pairs; scalar
and plain-string paths are unchanged.
Signed-off-by: KolisCode <jhohantma@gmail.com>
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>
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>
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>
The result projection is materialized per row into fixed-width stack
arrays (CBM_SZ_32 columns) in execute_return_simple and its siblings,
indexed by the parsed RETURN item count — which the parser left
unbounded. A RETURN/WITH clause with more than 32 projection items
wrote past those arrays. Reject an over-wide projection at parse time,
with a fork-isolated regression test driving a 48-column RETURN.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Explicit variable-length Cypher bounds ([:CALLS*1..N]) and the client
`depth` on trace_call_path / detect_changes were passed straight into
cbm_store_bfs, so an arbitrarily large value drove an unbounded traversal
(a DoS on cyclic graphs). The MCP_MAX_DEPTH ceiling was defined but never
applied, and CYP_MAX_DEPTH capped only the unbounded var-length forms.
Add env-configurable ceilings cbm_cypher_max_depth() (CBM_CYPHER_MAX_DEPTH,
default 10) and cbm_mcp_max_depth() (CBM_MCP_MAX_DEPTH, default 15), clamp
both entry points to them, and WARN on clamp -- never a silent truncation.
Remove the now-dead CYP_MAX_DEPTH / MCP_MAX_DEPTH constants.
Reproduce-first: cypher_exec_var_length_explicit_bound_capped (12 -> 10 rows)
and tool_trace_call_path_depth_clamped (depth 1000 -> hop 15).
Closes#887.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The truncation check `header_end + (int)comp_size > data_len` was
bypassable for comp_size >= 2^31: the int cast turns the size negative,
the sum drops below data_len, and the entry is accepted -- authorizing
inflate to read up to ~4GB past the downloaded archive buffer during
self-update ZIP extraction. Replace it with
`header_end > data_len || comp_size > (uint32_t)(data_len - header_end)`,
which is overflow-safe and strictly tighter.
Regression guard cli_extract_binary_from_zip_rejects_truncated_deflate_size_over_int_max
builds a 52-byte archive whose entry claims comp_size=0xFFFF0000 with a
self-terminating DEFLATE stream: the old check admits it and extraction
returns non-NULL (verified red pre-fix); the new check rejects it.
Secondary cppcheck cleanups from the same review:
- cli.c: extract zip_read_u16le/zip_read_u32le little-endian helpers
(behavior-identical) and widen zip_extract_entry sizes to size_t with
an explicit UINT_MAX guard before the zlib uInt narrowing.
- cypher.c: drop unreachable IS NULL / IS NOT NULL null guards in
eval_condition (resolve_condition_value result is checked earlier).
Distilled from PR #784.
Co-authored-by: SS-42 <noreply@incogni.to>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Fixes the dry-run lint failure: the #627 cypher and #514/#520/#546 mcp
additions had lines exceeding the column limit. clang-format 22.1.1
(project version) wraps them; no logic change.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The return-clause `limit` was zero-initialized (calloc) and every guard tested
`limit > 0`, so an explicit `LIMIT 0` was indistinguishable from "no LIMIT
clause" and returned ALL rows. Standard Cypher treats `LIMIT N` as an upper
bound, so `LIMIT 0` must return zero rows.
Default `limit` to the sentinel -1 ("no limit"); the parser still sets 0 for an
explicit `LIMIT 0`. Change the truncation guards (rb_apply_skip_limit,
bindings_skip_limit) and the apply call to `limit >= 0`, so limit 0 truncates to
0 while -1 returns all. The proj_cap optimization stays `> 0` (LIMIT 0 falls
through to full projection, then the final truncation yields 0).
Reproduced by repro_new_cypher_limit_zero (was RED): `RETURN ... LIMIT 0` must
yield row_count == 0.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A node group variable carried through a WITH aggregation
(e.g. `WITH g, count(*) AS c RETURN g.file_path`) returned blank for every
property except its name: the carried virtual binding held only the group
key (the node's name) and lacked a store handle, so node_prop() could
neither read other fields nor compute degrees.
Fix: capture the node id of a bare node group-var in with_agg_find_or_create
and tag the carried virtual binding with it; in node_prop(), when such a stub
(id set, string fields unpopulated) is asked for a missing property, re-fetch
the full node via cbm_store_find_node_by_id and project it. Also propagate
the store onto virtual bindings so node_prop can re-fetch and compute
degrees. The stub gate is heuristic but never yields a wrong value — worst
case is one redundant indexed lookup. Adds regression test
cypher_exec_with_node_groupvar_prop.
Signed-off-by: Kris Kersey <kris@kerseyfabrications.com>
MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method) returned at most 10 results
for any class, regardless of how many methods it actually has.
Root cause: bind_cap was set to scan_count (the number of nodes matched in
the initial pattern — typically 1 when querying a single class by name).
max_new = bind_cap * 10 = 10, so the edge expansion loop exited after
collecting 10 results. No error, no warning, no truncation indicator.
This is language-agnostic: any class with more than 10 methods in any
language was silently truncated. The fix is two characters:
bind_cap = scan_count > max_rows ? scan_count : max_rows
Regression test: a Python class with 15 methods must return all 15 via
MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method) with label filtering.
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
Per-function complexity metadata is now stored on graph nodes and queryable,
alongside several indexing performance and correctness fixes developed and
validated together (3704 tests, ASan/UBSan clean).
Bottleneck metrics (query via query_graph):
- Tier A (in the extraction AST walk): cyclomatic (complexity), cognitive
(nesting-weighted), loop_count, loop_depth (max nested-loop depth),
param_count, max_access_depth.
- Tier B (new pre-dump pass, pass_complexity.c): transitive_loop_depth
propagated along CALLS edges + a recursive flag (direct self-recursion and
mutual-recursion cycles), plus the call-context signals linear_scan_in_loop,
alloc_in_loop, recursion_in_loop and unguarded_recursion.
- query_graph and get_architecture tool descriptions document the metrics and
the Leiden community clusters.
Cypher engine:
- node_prop exposes arbitrary persisted node properties to WHERE/RETURN.
- Fix projection aliasing: multi-property rows shared a single static buffer so
every column returned the last value read; now per-column/rotating buffers.
- Fix a stack-use-after-scope in aggregate RETURN (caller-owned value buffers).
Indexing performance:
- Gate C/C++ #define Macro-node extraction to full mode (it is ~49% of nodes on
the Linux kernel); moderate/fast skip it.
- Emit the complexity property block only for Function/Method nodes so the
millions of Macro/Field/Variable/Class/Enum nodes no longer carry zeroed
fields — large RAM reduction at scale.
- Classify node types via tree-sitter TSSymbol bitsets in cbm_kind_in_set
instead of per-node strcmp scans (thread-local cache, strcmp fallback;
behaviour-identical).
- Subsample frequent (Zipfian) tokens in the semantic co-occurrence finalize;
~14x faster finalize on the kernel, output unchanged.
- pass_lsp_cross: replace O(n^2) linear dedup with hash-set dedup.
Windows:
- Canonicalize drive-letter case during path normalization so "c:/repo" and
"C:/repo" derive the same project key and cache file (#394/#227/#367).
Tests: extraction, pipeline and cypher regressions covering all of the above.
Add a single-hop, anchored existence predicate:
WHERE NOT EXISTS { (f)<-[:CALLS]-() } -- functions with no caller
WHERE EXISTS { (f)-[:CALLS]->() } -- functions that call something
Parsed via parse_exists_predicate (reusing parse_node/parse_rel) into a leaf
condition (op=EXISTS, anchor variable, edge type, direction); evaluated against
the bound node with cbm_store_find_edges_by_source_type / _by_target_type. This
is edge-type-specific, so it finds true orphans that in_degree/out_degree miss
(e.g. a node with only a DEFINES edge but no CALLS caller). Multi-hop / nested-
WHERE EXISTS is intentionally unsupported and errors clearly.
Tests cover the dead-code (NOT EXISTS) and has-outgoing (EXISTS) cases; smoke
coverage added. ASan-clean.
Refs: Cypher read suite (EXISTS predicate)
Add an args-list (cbm_func_arg_t) to return items and parse comma-separated
arguments (var.prop or string/number literals), enabling multi-argument scalar
functions in projections:
- coalesce(a, b, ...) first non-empty value
- substring(s, start[, len]) 0-indexed substring
- replace(s, from, to) replace all occurrences
- left(s, n) / right(s, n) leading/trailing n chars
Arguments are freed in free_return_clause (ASan-clean). Unit test
cypher_func_multiarg plus smoke coverage for substring + coalesce.
Refs: Cypher read suite (tier 2b)
Add single-argument string functions to projections — size, length, trim,
ltrim, rtrim, reverse — via the generic function recognizer.
More importantly, change the engine to FAIL LOUDLY on unsupported syntax instead
of silently projecting an empty column. An unknown function call (e.g.
split(...), coalesce(...)) or list indexing/slicing in RETURN/WITH now returns a
clear "unsupported function '<name>' (supported: ...)" error rather than a
valid-looking but blank result — the same silent-empty failure mode that hid the
labels() bug. This supersedes the #373 graceful-resync behaviour (its test is
flipped to assert the error; a new test covers an unknown function in RETURN).
Decision: full Cypher Tier 3 (lists/maps/paths/comprehensions/params — a value
data-model rewrite) is intentionally NOT implemented; clear errors on
unsupported features are the higher-value, lower-risk completeness win for the
read subset agents actually use.
Refs: Cypher read suite (tiers 1-2a + unsupported-syntax hardening)
Add single-argument functions to RETURN/WITH projections, dispatched via a
generic IDENT-call recognizer so new functions no longer need a dedicated lexer
keyword:
- labels(n) -> ["<label>"]
- type(r) -> relationship type
- id(n) -> node/edge identity
- keys(n) -> JSON list of the node's non-null property keys
- properties(n) -> the node/edge properties JSON object
- toInteger/toFloat/toBoolean(x) -> numeric/boolean casts (null on non-numeric)
Previously labels()/id()/keys()/etc. fell into the unknown-function path and
silently projected empty (labels(n) returned ""); they now evaluate correctly.
Regex =~ in WHERE was already supported. Unit tests for each function plus
smoke-test coverage via the live query_graph CLI.
Refs: full Cypher read suite (tier 1 of 3)
The buffer-full guards in parse_node (label alternation) and with_proj_key
assigned to ll/kl right before break — never read again. cppcheck 2.20.0
(--ci) flags these as unreadVariable and fails the lint gate. Replace with a
bare break. No behavior change.
split(f.path,'/')[0] and similar unknown function-call / index
expressions left their trailing tokens unconsumed, desyncing the parser into
a misleading default star projection (columns f.name/f.qualified_name/
f.label, one blank row per node). parse_return_item now consumes the balanced
parentheses/brackets of an unknown call so the parser stays in sync: the
requested RETURN/WITH column shape is preserved (aliases intact), sibling
aggregates like count(*) still compute, and the unsupported expression simply
projects empty instead of corrupting the whole projection. Existing scalar
forms (labels(), toInteger()) are unaffected.
count(DISTINCT x) was a parse error ("expected token type 85, got 9").
parse_aggregate_item now consumes an optional DISTINCT inside the call and
records it on the return item. Both aggregation paths track the set of unique
values per group and emit its size for COUNT(DISTINCT): the RETURN path reuses
the existing collect_lists machinery; the WITH path gains distinct_lists/
distinct_n fields. Plain COUNT and the other aggregates are unchanged.
Unit test: COUNT(DISTINCT f.label)=1 (all same label), count(f.label)=4
(non-distinct), COUNT(DISTINCT f.name)=4 (unique names). Smoke suite adds a
count(DISTINCT) query_graph check.
openCypher node-label alternation was unsupported (parse error after the
first label). parse_node now consumes `:A|B|C` into a single "A|B|C" label
string (mirroring the existing relationship-type alternation). The seed path
unions per-label results (scan_alternation_labels), and target/hop label
filters use label_alt_matches, which ORs over the alternatives. Single-label
matching is unchanged.
Unit test (Function|Module → 5 rows; Function|Class → 4) plus a query_graph
alternation check in the smoke suite. Relationship-type alternation tests
remain green.
openCypher label predicates in WHERE clauses were a parse error
("expected token type 67, got 73"). Parse `var:Label` in the WHERE leaf as
a condition with op=HAS_LABEL/value=Label, and evaluate it against the bound
node label in eval_condition (honoring NOT). Composes with AND/OR and works
in count(...) queries.
Unit test covers true/false/negated label predicates; smoke suite adds a
query_graph WHERE-label check.
The DISTINCT keyword on a WITH clause was parsed (r->distinct) but never
applied, so WITH DISTINCT silently returned duplicate rows. Add a dedup pass
in execute_with_clause that drops projected rows whose full value tuple
duplicates an earlier one (first occurrence kept), gated on wc->distinct.
No-op for the aggregation path (which already collapses per group).
Unit test (4 same-label functions collapse to 1 row; control without DISTINCT
keeps 4) plus a query_graph WITH DISTINCT check added to the smoke suite.
- str_util: add cbm_validate_project_name() — strict allowlist
([A-Za-z0-9._-], no leading dot, no '..', no path separators) so a
project name can never escape the cache directory.
- store/mcp/http_server: validate project names (fail-closed) before
interpolating them into .db file paths (path-traversal hardening;
defense-in-depth for the wrong-path class in #331).
- store: fix use-after-free in the LIKE pool — like_pool_add() frees its
argument when the pool is full, so capture pool_was_full before the add
and skip the subsequent bind that would touch the freed pointer (two
call sites). Also fix the find_edges_by_url_path LIKE pattern (stray
double-percent) and add NULL guards to cbm_store_checkpoint /
cbm_store_node_degree / cbm_store_restore_from.
- mcp: emit "result": null when a JSON-RPC response would otherwise
carry neither result nor error (2.0 compliance); bounds-check the
project-name accumulator against truncation.
- http_server: JSON-escape directory and parent names in the browse
endpoint to prevent stored-XSS/JSON injection via crafted names.
- cypher: expr_free now recurses on EXPR_FREE_STACK overflow instead of
silently dropping subtrees (memory leak).
Distilled from #352 onto current main (store.c like-pool hunk re-resolved
against the v0.7.0 search refactor; restored cypher.c EOF newline).
Adds three changes that landed together in PR #257, with a JSON-escape
hardening on the new File node properties so unusual file extensions
can't produce malformed output.
Mode A — search_graph results expose node properties_json
---------------------------------------------------------
emit_search_results in src/mcp/mcp.c now calls enrich_node_properties
for each result and parks the parsed yyjson_doc* in an out-array. The
two exit paths in handle_search_graph (success and sq_type_error) walk
that array and yyjson_doc_free each entry after yy_doc_to_str runs, so
zero-copy strings stay alive until serialization completes. Callers no
longer have to round-trip through get_code_snippet just to inspect a
node's properties.
Mode B — Cypher bare edge return surfaces full properties JSON
--------------------------------------------------------------
binding_get_virtual in src/cypher/cypher.c returns e->properties_json
(or "{}" if absent) when prop is NULL, instead of "". `RETURN r` on
an edge variable is now actually useful for inspecting timestamps,
weights, etc. without naming each property up front.
Temporal data on FILE_CHANGES_WITH edges and File nodes
-------------------------------------------------------
cbm_change_coupling_t gains last_co_change. cbm_commit_files_t and
the internal commit_t gain a timestamp field. Both git-log paths
(libgit2 and popen --pretty=format:COMMIT:%H:%ct) populate it.
cbm_compute_change_coupling carries a parallel pair_timestamps hash
table and writes the max timestamp to last_co_change. The edge
properties JSON now includes "last_co_change":<unix-epoch>.
cbm_pipeline_githistory_compute additionally aggregates per-file
change_count and last_modified into a new cbm_file_temporal_t array
exposed via result->file_temporal. cbm_pipeline_githistory_apply
upserts those values onto matching File nodes so callers can run
hotspot queries (most-changed, recently-changed) directly. Both the
sequential cbm_pipeline_pass_githistory and the parallel run_githistory
in pipeline.c free file_temporal and trigger apply when either
couplings or temporal entries are present.
The extension stored on the File node properties is now JSON-escaped
via cbm_json_escape before snprintf'ing into the props blob. The
original PR concatenated the raw extension; a hypothetical path with
a quote or backslash in the extension would have produced malformed
JSON. Practical risk was tiny (real extensions never contain those
characters) but the escape is cheap and correct.
Tests
-----
- githistory_coupling_carries_last_co_change pins last_co_change as
the max of the per-pair commit timestamps even when an unrelated
pair sits between them in the input order.
- cypher_bare_edge_return_exposes_properties_json regresses Mode B
by asserting `RETURN r` on an HTTP_CALLS edge surfaces its url_path
in the response.
- tool_search_graph_includes_node_properties regresses Mode A by
asserting search_graph's response contains the signature/return_type/
is_exported keys from a HandleRequest node's properties_json.
Existing githistory tests had their cbm_commit_files_t initializers
extended with a zero timestamp (backward-compatible — last_co_change
falls back to 0 for callers that don't supply timestamps).
Cherry-picked from #257; renamed the timestamp field types to
long long for portability across 32-/64-bit time_t platforms, used
the existing cbm_json_escape utility instead of raw concatenation
for the extension, and added explicit tests for all three changes.
Closes#257.
Co-authored-by: Adam Schulte <adamschulte148@gmail.com>
Apply the platform.h memory wrappers across the largest free / realloc
hot spots so cleanup paths are uniform and pointers are NULLed on free
to prevent accidental use-after-free / double-free.
- src/cypher/cypher.c (~85 sites): replace free((void *)x) with
safe_str_free(&x) for owned const char * fields; the array-pointer
cleanup in parse_in_list error paths uses safe_free(vals) since vals
is const char **.
- src/store/store.c (~81 sites): same const-string substitution across
cbm_search_result_t cleanup, project list, schema info, etc.
- src/mcp/mcp.c (~21 sites): same, plus collect_grep_matches growth
loop converted to safe_grow(gm, gm_count, gm_cap, PAIR_LEN) — safe
here because grep_match_t holds fixed-size char arrays only.
- src/pipeline/pass_githistory.c: free_counter callback uses
safe_str_free for the hash key.
Refactor work cherry-picked from #210; #210 itself bundled .claude/,
.github/workflows/, and a stray .claude/worktrees/ submodule that did
not belong in the repo, and most of its other changes had landed via
#206 / #207 / #208 already. The cypher / store growth-path safe_realloc
loops are intentionally NOT converted to safe_grow because their
elements own heap pointers — a failed realloc would leak them; the
non-destructive realloc + cleanup pattern from #206 is preserved.
Co-authored-by: map588 <map588@pitt.edu>
Follow-up to #206. Adds the allocation-check coverage that the original
PR did not reach:
- store: NULL-check malloc in cbm_store_get_schema node-labels and
edge-types blocks; replace safe_realloc with non-destructive realloc +
cleanup so a partial result is freed (and node-labels block freed via
cbm_store_schema_free) instead of leaked.
- cypher: NULL-check heap_strdup return in parse_props (key/value),
parse_rel_types (first and subsequent types), and parse_in_list (op
and list values). Each failure path frees prior heap_strdup'd entries
before returning.
- cypher: parse_case_expr realloc-failure cleanup now also frees
then_val for the just-parsed branch and for each previously stored
branch, not just the when_expr trees.
- Enum member extraction: C#/Java/TS/C++ enum values as Variable nodes
- JS/TS destructured variable splitting: const { A, B, C } = require()
emits individual Variable nodes instead of one blob
- Cypher -- SQL-style single-line comments
- Cypher in_degree/out_degree virtual node properties (thread-safe
via binding store pointer, enables dead code detection via Cypher)
- Cypher regex matching in inline property filters
- Resolution tiebreaking: test/mock QN deprioritization and C# field
type hint resolution for obj.Method() disambiguation
Co-Authored-By: Tommy <Koolerx@users.noreply.github.com>
- platform.c: move nlen into if(env) block (variableScope)
- userconfig.c: move home into else block (variableScope)
- pipeline.c: remove dead rc!=0 check after httplinks removal
- cli.c: rmdir_recursive propagates cbm_rmdir errors, remove redundant local
- clang-tidy: whitelist cosmetic-only checks, enable all safety checks
- Thread-safe getenv via environ iteration (concurrency-mt-unsafe fix)
- Multiple files: add constants.h includes for named constants
Lower thresholds to best-practice values:
cognitive-complexity: 250 -> 25 (industry default)
statements: 400 -> 200
lines: 800 -> 400
Add NOLINTNEXTLINE to 168 functions that currently exceed these thresholds.
Each suppression will be removed as the function is split in subsequent
commits. Zero net behavioral changes.
Also: extract execute_with_clause from cypher execute_single.
Enforce WarningsAsErrors: '*' with zero suppressions of entire check categories.
All non-FP warnings fixed at source; true false positives get per-instance NOLINT
with reasoning. Fixes include: braces around statements (1679), isolate declarations
(75), explicit casts for multilevel pointer conversions, missing includes, function
parameter documentation via NOLINT, and proper NOLINTBEGIN/NOLINTEND nesting.
Complete C implementation of the indexing pipeline (src/), parallel worker
pool, graph buffer with merge support, SQLite writer, and 1893-test suite.
Linter setup: zero warnings from clang-tidy (all checks enabled, no NOLINT
suppressions), cppcheck, and clang-format. All issues fixed at source —
proper headers for external linkage, GROW_ARRAY macro restructured to
eliminate type parameter, null-deref paths guarded, named intermediates
for suspicious-argument checks.