Skip to content

fix: resolve dogfood issues #408–#411#418

Open
carlos-alm wants to merge 19 commits intomainfrom
fix/issues-408-411
Open

fix: resolve dogfood issues #408–#411#418
carlos-alm wants to merge 19 commits intomainfrom
fix/issues-408-411

Conversation

@carlos-alm
Copy link
Contributor

Summary

Fixes all four open dogfood issues from v3.1.2:

Test plan

  • npm run lint passes
  • All 448 integration tests pass (pre-existing WASM grammar failures unchanged)
  • No Rust compilation errors (linker-only failures on Windows dev env)
  • Verify codegraph info shows correct version on CI
  • Verify codegraph build --engine native produces dynamic-imports edges
  • Verify node scripts/benchmark.js survives engine failure gracefully

Closes #408, closes #409, closes #410, closes #411

Wrap each benchmarkEngine() call in benchmark.js so a WASM crash
doesn't prevent native from running. Move disposeModel() into a
finally block in embedding-benchmark.js so model cleanup always
runs even after failures.
…409)

Add disposeParsers() export that calls .delete() on cached WASM
parsers and queries, freeing linear memory. Call tree.delete() on
parsed syntax trees in builder.js after AST analysis completes.
Prevents memory accumulation across repeated builds in the same
process.

Impact: 4 functions changed, 21 affected
Add dynamicImport field to the Import struct in types.rs. Detect
import() call expressions in the Rust JS extractor and emit them
as imports with dynamicImport=true, matching the WASM engine's
behavior. Extract destructured binding names from
const { a } = await import(...) patterns. Add backward-compat
patch in patchNativeResult for older binaries.

Impact: 5 functions changed, 2 affected
Add getNativePackageVersion() to native.js that reads the version
from the platform-specific npm package.json. Use this in the info
command and getActiveEngine() to prefer the package version over
the binary-embedded version, which may be stale if Cargo.toml was
not bumped before the binary was compiled.

Impact: 1 functions changed, 9 affected
@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:464-470. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR resolves four dogfood issues from v3.1.2: benchmark crash resilience (#408), WASM memory management to prevent segfaults on repeated builds (#409), native engine dynamic-import tracking (#410), and stale native version reporting (#411). The changes are well-scoped and the many issues raised in earlier review rounds have all been addressed.

  • bug: benchmark scripts crash entirely when one engine/model fails #408 (benchmark crash) — Both benchmark scripts now catch per-engine failures independently, use err?.message ?? String(err) for safe error formatting, and embedding-benchmark.js moves disposeModel() into a finally block.
  • bug: WASM engine segfaults after repeated builds in same process #409 (WASM segfault)disposeParsers() correctly frees Parser, Query, and Language WASM objects and resets _initialized; builder.js calls .delete() on syntax trees before nulling them.
  • bug: native engine does not track dynamic import() expressions #410 (native dynamic imports) — A new dynamic_import field on the Rust Import struct is set when a call_expression with function = "import" is detected; extract_dynamic_import_names handles object_pattern (including pair_pattern, object_assignment_pattern, and rest_pattern), identifier, and array_pattern (with identifier and assignment_pattern) — however rest_pattern/rest_element inside array_pattern is not handled, so ...rest bindings in const [a, ...rest] = await import(...) are silently dropped.
  • bug: info command reports stale native engine version (3.1.0 instead of 3.1.2) #411 (stale version)getNativePackageVersion() reads from the platform npm package.json, with a shared resolvePlatformPackage() helper; the info command and getActiveEngine both prefer this over the binary-embedded version, and the "stale" annotation only appears when both sides report known, differing version strings.

Confidence Score: 4/5

  • Safe to merge with one minor correctness gap in the Rust extractor
  • All four issues are addressed and every concern raised across the previous review rounds has been fixed. The only new gap found is that rest_pattern/rest_element inside array_pattern is not handled in extract_dynamic_import_names, causing rest bindings in array-destructured dynamic imports to be silently dropped from the graph — a concrete correctness issue but not a crash or security problem.
  • crates/codegraph-core/src/extractors/javascript.rs — the array_pattern arm of extract_dynamic_import_names needs a rest_element/rest_pattern branch to match the object_pattern arm.

Important Files Changed

Filename Overview
crates/codegraph-core/src/extractors/javascript.rs Adds dynamic import() detection and a comprehensive extract_dynamic_import_names helper; one gap: rest_pattern/rest_element inside array_pattern is not handled, so rest bindings in array-destructured dynamic imports are silently dropped.
src/parser.js Adds disposeParsers() that correctly frees WASM parsers, queries, and Language objects and resets _initialized; getActiveEngine now prefers the package.json version over the binary-embedded version. No issues found.
src/native.js Extracts resolvePlatformPackage() to eliminate duplicated platform-detection logic, hoists _require, and adds getNativePackageVersion(). Clean refactor with no issues found.
src/cli.js The info command now prefers the package.json version, emits the "stale" annotation only when both sides are known version strings that disagree, and uses a single deduplicated import('./native.js'). No issues found.
src/builder.js Calls .delete() on cached WASM trees before nulling the reference, wrapped in a guard and try/catch. Correctly addresses the WASM memory accumulation issue (#409).
scripts/benchmark.js Wraps each benchmarkEngine() call in try/catch so one engine failure doesn't abort the other; adds an early-exit guard when both engines fail; uses err?.message ?? String(err) safely. No issues found.
scripts/embedding-benchmark.js Moves disposeModel() into a finally block so it always runs even when the benchmark throws; uses safe error formatting throughout. No issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["JS source file\n(call_expression)"] -->|"fn_node.kind() == 'import'"| B["Dynamic import() detected\n(javascript.rs walk_node)"]
    B --> C{"find_child: string\nor template_string?"}
    C -->|"yes"| D["strip quotes/backticks\n→ mod_path"]
    C -->|"no"| Z["skip — no import emitted"]
    D --> E["extract_dynamic_import_names()"]
    E --> F{"declarator\nname kind?"}
    F -->|"identifier"| G["names = [mod]"]
    F -->|"object_pattern"| H["collect: shorthand,\npair, obj_assign, rest"]
    F -->|"array_pattern"| I["collect: identifier,\nassignment_pattern\n⚠️ rest_element missing"]
    F -->|"other"| J["names = []"]
    G & H & I & J --> K["Import { source: mod_path,\nnames, dynamic_import: true }"]
    K --> L["symbols.imports.push(imp)"]
    L --> M["patchNativeResult()\nadds dynamicImport camelCase alias"]
    M --> N["Graph edge emitted"]

    subgraph "Version reporting (#411)"
        V1["getNativePackageVersion()\nreads platform pkg/package.json"]
        V2["native.engineVersion()\nbinary-embedded version"]
        V3{"both known\nand disagree?"}
        V1 & V2 --> V3
        V3 -->|"yes"| V4["show pkg version\n+ 'stale' annotation"]
        V3 -->|"no"| V5["show pkg version\nor binary version"]
    end

    subgraph "WASM memory management (#409)"
        W1["disposeParsers()\nparser.delete() + query.delete()\n+ lang.delete() + _initialized=false"]
        W2["symbols._tree.delete()\nin builder.js finalize loop"]
    end
Loading

Last reviewed commit: de32c38

src/parser.js Outdated
Comment on lines +466 to +470
if (native && version) {
try {
version = getNativePackageVersion() ?? version;
} catch {}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package version skipped when binary lacks engineVersion

The condition if (native && version) only consults the package.json when the binary already reports a version. If native.engineVersion is not a function (returning null on line 461), version is null, and getNativePackageVersion() is never called — even though the package.json has the correct version.

This defeats the stated goal of preferring the package version over the binary-embedded version. It's specifically a problem for older binaries that don't expose engineVersion(), which is exactly the scenario issue #411 describes ("stale binaries"). The condition should be if (native) so the package version is always preferred when the native engine is loaded:

Suggested change
if (native && version) {
try {
version = getNativePackageVersion() ?? version;
} catch {}
}
if (native) {
try {
version = getNativePackageVersion() ?? version;
} catch {}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed if (native && version) to if (native) so the package version is always consulted when the native engine is loaded, even if the binary lacks engineVersion().

Comment on lines +111 to +130
export function disposeParsers() {
if (_cachedParsers) {
for (const [, parser] of _cachedParsers) {
if (parser && typeof parser.delete === 'function') {
try {
parser.delete();
} catch {}
}
}
_cachedParsers = null;
}
for (const [, query] of _queryCache) {
if (query && typeof query.delete === 'function') {
try {
query.delete();
} catch {}
}
}
_queryCache.clear();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disposeParsers does not reset _initialized

After disposeParsers() is called, _cachedParsers is set to null so createParsers() will recreate them. However, _initialized is left true, meaning the subsequent call to createParsers() skips Parser.init(). This is benign as long as web-tree-sitter's Parser.init() only needs to run once per WASM module lifetime (which appears to be the case). If this assumption ever changes — for example if a future version of web-tree-sitter requires re-initialization after parser disposal — this would silently break.

Consider adding a comment here, or explicitly resetting _initialized = false so the next createParsers() call re-runs the WASM module setup safely:

Suggested change
export function disposeParsers() {
if (_cachedParsers) {
for (const [, parser] of _cachedParsers) {
if (parser && typeof parser.delete === 'function') {
try {
parser.delete();
} catch {}
}
}
_cachedParsers = null;
}
for (const [, query] of _queryCache) {
if (query && typeof query.delete === 'function') {
try {
query.delete();
} catch {}
}
}
_queryCache.clear();
}
export function disposeParsers() {
if (_cachedParsers) {
for (const [, parser] of _cachedParsers) {
if (parser && typeof parser.delete === 'function') {
try {
parser.delete();
} catch {}
}
}
_cachedParsers = null;
}
for (const [, query] of _queryCache) {
if (query && typeof query.delete === 'function') {
try {
query.delete();
} catch {}
}
}
_queryCache.clear();
// Reset so createParsers() re-initializes the WASM runtime on next call
_initialized = false;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — disposeParsers() now resets _initialized = false so the next createParsers() call re-runs Parser.init().

Comment on lines +80 to +93
export function getNativePackageVersion() {
const require = createRequire(import.meta.url);
const platform = os.platform();
const arch = os.arch();
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
const pkg = PLATFORM_PACKAGES[key];
if (!pkg) return null;
try {
const pkgJson = require(`${pkg}/package.json`);
return pkgJson.version || null;
} catch {
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createRequire called on every invocation

getNativePackageVersion() calls createRequire(import.meta.url) on every invocation. While Node.js's module cache makes the underlying require() result cheap after the first call, creating a new createRequire closure each time is unnecessary. Consider hoisting the require to a module-level constant so it is created once (the same way loadNative already creates one, just without sharing it). This would also bring getNativePackageVersion in line with the existing loadNative pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — hoisted createRequire to a module-level _require constant. Both loadNative and getNativePackageVersion (and detectLibc) now use the shared instance.

- Check package version even when binary lacks engineVersion() (parser.js)
- Reset _initialized on disposeParsers so WASM re-inits properly (parser.js)
- Hoist createRequire to module-level _require constant (native.js)

Impact: 5 functions changed, 10 affected
@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address Greptile review comments
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:464-470. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Critical Issues from Greptile Review

🚨 Logic Bug in getActiveEngine() (src/parser.js:466): The condition if (native && version) prevents package.json version lookup when native.engineVersion() returns null. This defeats the purpose of issue #411 fix for stale binaries. Should be if (native) to always prefer package version.

⚠️ Minor Issues:

  • disposeParsers() doesn't reset _initialized = false (though current implementation appears safe)
  • getNativePackageVersion() creates new createRequire on each call (minor inefficiency)

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
Logic bug in getActiveEngine() that prevents package.json version lookup for stale binaries (the exact scenario issue #411 describes).

## Final Recommendation
- Rating: ⭐⭐⭐☆☆ (3/5)
- Action: APPROVE WITH CONDITIONS
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. One critical logic bug needs fixing before merge - the version preference condition defeats the #411 fix for legacy binaries without engineVersion().

@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +1409 to +1416
const binaryVersion =
typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
console.log(` Native version: ${nativeVersion}`);
const pkgVersion = getNativePackageVersion();
if (pkgVersion && pkgVersion !== binaryVersion) {
console.log(` Native version: ${pkgVersion} (binary reports ${binaryVersion} — stale)`);
} else {
console.log(` Native version: ${binaryVersion}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading "stale" warning when binary lacks engineVersion

When native.engineVersion is not a function, binaryVersion is set to 'unknown'. If getNativePackageVersion() returns a valid version (e.g. '3.1.2'), the condition pkgVersion !== binaryVersion is true ('3.1.2' !== 'unknown'), causing the output:

Native version: 3.1.2 (binary reports unknown — stale)

This is misleading: the binary is not stale — it just predates the engineVersion() API. The "stale" message should only appear when both sides are known version strings and they disagree. Otherwise, showing the package version alone (without the "stale" annotation) is the correct behaviour.

Suggested change
const binaryVersion =
typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
console.log(` Native version: ${nativeVersion}`);
const pkgVersion = getNativePackageVersion();
if (pkgVersion && pkgVersion !== binaryVersion) {
console.log(` Native version: ${pkgVersion} (binary reports ${binaryVersion} — stale)`);
} else {
console.log(` Native version: ${binaryVersion}`);
}
const pkgVersion = getNativePackageVersion();
const knownBinaryVersion = binaryVersion !== 'unknown' ? binaryVersion : null;
if (pkgVersion && knownBinaryVersion && pkgVersion !== knownBinaryVersion) {
console.log(` Native version: ${pkgVersion} (binary reports ${knownBinaryVersion} — stale)`);
} else {
console.log(` Native version: ${pkgVersion ?? binaryVersion}`);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the stale warning now only appears when both the package version and binary version are known and disagree. When the binary lacks engineVersion() (reports unknown), we just show the package version without the misleading stale annotation.

Comment on lines +209 to +219
if let Some(args) = node.child_by_field_name("arguments")
.or_else(|| find_child(node, "arguments"))
{
if let Some(str_node) = find_child(&args, "string") {
let mod_path = node_text(&str_node, source)
.replace(&['\'', '"'][..], "");
let names = extract_dynamic_import_names(node, source);
let mut imp = Import::new(mod_path, names, start_line(node));
imp.dynamic_import = Some(true);
symbols.imports.push(imp);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template-literal dynamic imports silently skipped

find_child(&args, "string") only matches string AST nodes (single- or double-quoted literals). Template literal arguments — e.g. import(`./plugins/${name}.js`) — produce a template_string node and will be silently ignored: no import edge is emitted and no warning is logged. This is a silent correctness gap rather than a crash.

Consider also checking for template_string in the arguments, or emitting a best-effort import with an empty/unknown path when a template literal is detected:

if let Some(str_node) = find_child(&args, "string")
    .or_else(|| find_child(&args, "template_string"))
{

Note that for template literals the resulting mod_path will include backtick characters that the current replace won't strip, so you'll also need to add '`' to the replacement set, and decide whether to emit such imports at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — find_child now also checks for template_string, and backticks are stripped alongside single/double quotes in the replace set.

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address Greptile review comments
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:464-470. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Critical Issues from Greptile Review

🚨 Logic Bug in CLI (src/cli.js:1412): The condition if (pkgVersion && pkgVersion !== binaryVersion) triggers the "stale" message when binaryVersion is 'unknown'. This means older binaries that lack engineVersion() will incorrectly show as "stale" instead of just displaying the package version. This defeats the purpose of the #411 fix for legacy binaries.

⚠️ Template Literal Import Gap (javascript.rs:212): The dynamic import detection only handles quoted string literals via find_child(&args, "string"). Template literals like import(`./plugins/${name}.js`) produce template_string nodes and are silently ignored, creating a coverage gap in dynamic import tracking.

Fixed Issues: Carlos addressed these Greptile comments correctly:

  • disposeParsers() now resets _initialized = false (line 130)
  • getNativePackageVersion() uses hoisted _require constant (line 14)
  • Version preference logic works correctly in getActiveEngine() (lines 467-470)

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
CLI logic bug that incorrectly labels legacy binaries as "stale" when they simply lack engineVersion() API. Template literal dynamic imports are silently skipped.

## Final Recommendation
- Rating: ⭐⭐⭐☆☆ (3/5)
- Action: APPROVE WITH CONDITIONS
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Two issues need addressing: the CLI stale-labeling logic bug and template literal import coverage gap. The fixes work correctly but have edge case issues that should be resolved.

- Only show "stale" warning when both binary and package versions are
  known strings that disagree; prefer package version when binary
  reports "unknown"
- Also match template_string nodes for dynamic import() so template
  literal arguments are not silently skipped
- Reset _initialized in disposeParsers() so Parser.init() re-runs
- Hoist createRequire to module-level _require constant
- Widen getActiveEngine version check to if (native)

Impact: 1 functions changed, 1 affected
@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address Greptile review comments
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Greptile Review Comments Status

All Critical Issues Resolved:

  • CLI stale version logic: Fixed in src/cli.js:1412-1419 - now properly distinguishes between unknown binary versions vs actual version mismatches
  • Package version always consulted: Fixed in src/parser.js:467-471 - condition changed from if (native && version) to if (native)
  • Shared require instance: Fixed in src/native.js:14 - hoisted createRequire to module-level _require
  • Parser initialization reset: Fixed in src/parser.js:130 - disposeParsers() now sets _initialized = false
  • Template string dynamic imports: Fixed in javascript.rs:213 - added .or_else(|| find_child(&args, "template_string")) and backtick handling

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

@carlos-alm
Copy link
Contributor Author

Fixed the two remaining review items:

  1. Misleading stale warning — Only show "stale" when both binary and package versions are known strings that disagree. When binary reports "unknown", just display the package version without the stale annotation.

  2. Template-literal dynamic imports — Now also matches template_string nodes and strips backticks, so import(\) patterns emit edges too.

@greptileai

src/cli.js Outdated
console.log(` Platform : ${process.platform}-${process.arch}`);
console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
if (nativeAvailable) {
const { getNativePackageVersion } = await import('./native.js');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant second import('./native.js')

native.js is already imported at line 1393 to destructure isNativeAvailable and loadNative. The second await import('./native.js') here to obtain getNativePackageVersion is redundant — ES module caching means there's no double-evaluation, but it splits the imports for the same module across two lines and is inconsistent with the established pattern. getNativePackageVersion should be added to the destructuring on line 1393:

Suggested change
const { getNativePackageVersion } = await import('./native.js');
const { isNativeAvailable, loadNative, getNativePackageVersion } = await import('./native.js');

Then remove line 1407 entirely.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — deduplicated the import. getNativePackageVersion is now destructured alongside isNativeAvailable and loadNative in the single import('./native.js') call.

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address Greptile review comments
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Greptile Review Comments Status

All Critical Issues Resolved:

  • CLI stale version logic: Fixed in src/cli.js:1412-1419 - now properly distinguishes between unknown binary versions vs actual version mismatches
  • Package version always consulted: Fixed in src/parser.js:467-471 - condition changed from if (native && version) to if (native)
  • Shared require instance: Fixed in src/native.js:14 - hoisted createRequire to module-level _require
  • Parser initialization reset: Fixed in src/parser.js:130 - disposeParsers() now sets _initialized = false
  • Template string dynamic imports: Fixed in javascript.rs:213 - added .or_else(|| find_child(&args, "template_string")) and backtick handling

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

Merge getNativePackageVersion into the existing import('./native.js')
destructure instead of importing the module a second time.
@carlos-alm
Copy link
Contributor Author

Addressed all three remaining review items:

  1. Misleading stale warning (cli.js) — Only show "stale" when both binary and package versions are known strings that disagree. When binary reports "unknown", display the package version without annotation.

  2. Template-literal dynamic imports (javascript.rs) — Now also matches template_string nodes and strips backticks from the path.

  3. Redundant second import (cli.js) — Merged getNativePackageVersion into the existing import('./native.js') destructure.

@greptileai

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address recent fixes mentioned by carlos-alm
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Carlos's Recent Fixes Assessment

All Critical Issues Addressed: Carlos has successfully addressed all three remaining review items mentioned in his comment:

  1. ✅ Misleading stale warning fixed: The CLI logic in src/cli.js:1412-1419 now correctly distinguishes between unknown binary versions vs actual version mismatches. The condition properly checks for both pkgVersion and knownBinaryVersion before showing the "stale" annotation.

  2. ✅ Template-literal dynamic imports fixed: The Rust extractor in javascript.rs:212-213 now also matches template_string nodes via .or_else(|| find_child(&args, "template_string")) and handles backtick stripping in line 216, so import(`./plugins/${name}.js`) patterns emit edges correctly.

  3. ✅ Redundant import cleaned up: The CLI file now properly imports getNativePackageVersion in the original destructuring assignment (line 1393) instead of having a second redundant import.

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Carlos has addressed all review feedback correctly. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

Comment on lines +1047 to +1052
} else if child.kind() == "pair_pattern" || child.kind() == "pair" {
if let Some(val) = child.child_by_field_name("value") {
names.push(node_text(&val, source).to_string());
} else if let Some(key) = child.child_by_field_name("key") {
names.push(node_text(&key, source).to_string());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pair_pattern with a default value produces malformed import names

When a destructured dynamic import uses a renamed binding with a default — e.g. const { foo: bar = 'default' } = await import('./mod.js') — tree-sitter parses the value child of pair_pattern as an assignment_pattern node whose full text is "bar = 'default'". Calling node_text(&val, source) on that node pushes the entire raw text into the names vector, not just the local identifier bar.

For the rename-only case ({ foo: bar }) this is fine because value is a plain identifier. But when a default is present, the graph will receive a malformed import name like "bar = 'default'" that will never match any symbol.

To fix this, descend one level further when the value is an assignment_pattern:

} else if child.kind() == "pair_pattern" || child.kind() == "pair" {
    if let Some(val) = child.child_by_field_name("value") {
        // If the value is an assignment_pattern (e.g. `{ foo: bar = 'default' }`),
        // extract the left-hand side (the local binding name).
        let binding = if val.kind() == "assignment_pattern" {
            val.child_by_field_name("left").unwrap_or(val)
        } else {
            val
        };
        names.push(node_text(&binding, source).to_string());
    } else if let Some(key) = child.child_by_field_name("key") {
        names.push(node_text(&key, source).to_string());
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the pair_pattern handler now descends into assignment_pattern to extract the left-hand binding, and guards against non-identifier values (nested patterns) by skipping them.

Comment on lines +184 to +186
} catch (err) {
console.error(`WASM benchmark failed: ${err.message}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err.message is undefined for non-Error throws

If benchmarkEngine ever rejects with a string or non-Error value (e.g., throw 'timeout'), err.message will be undefined and the diagnostic line becomes "WASM benchmark failed: undefined". The same applies to the native catch block at line 196.

Prefer String(err) or err?.message ?? String(err) to produce a useful message regardless of the thrown type:

Suggested change
} catch (err) {
console.error(`WASM benchmark failed: ${err.message}`);
}
console.error(`WASM benchmark failed: ${err?.message ?? String(err)}`);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — both catch blocks in benchmark.js already use err?.message ?? String(err) for safe error formatting.

Comment on lines +79 to +91
export function getNativePackageVersion() {
const platform = os.platform();
const arch = os.arch();
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
const pkg = PLATFORM_PACKAGES[key];
if (!pkg) return null;
try {
const pkgJson = _require(`${pkg}/package.json`);
return pkgJson.version || null;
} catch {
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform key computation duplicated from loadNative

getNativePackageVersion rebuilds the same platform/arch/detectLibc()/key/pkg lookup that loadNative already performs. If a new architecture or libc variant is ever added to PLATFORM_PACKAGES, this logic must be updated in two places and can silently diverge.

Consider extracting a small shared helper resolvePlatformPackage() that returns the resolved package name (or null). Both loadNative and getNativePackageVersion can then call it instead of duplicating the four-line block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — extracted resolvePlatformPackage() as a shared helper. Both loadNative and getNativePackageVersion now call it instead of duplicating the platform/arch/libc lookup.

- Handle assignment_pattern in pair_pattern for destructured dynamic
  imports with defaults (e.g. { foo: bar = 'x' })
- Use err?.message ?? String(err) for non-Error throws in benchmarks
- Extract resolvePlatformPackage() helper to deduplicate platform key
  computation between loadNative and getNativePackageVersion

Impact: 4 functions changed, 7 affected
@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address recent fixes mentioned by carlos-alm
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Carlos's Recent Fixes Assessment

All Critical Issues Addressed: Carlos has successfully addressed all three remaining review items mentioned in his comment:

  1. ✅ Misleading stale warning fixed: The CLI logic in src/cli.js:1412-1419 now correctly distinguishes between unknown binary versions vs actual version mismatches. The condition properly checks for both pkgVersion and knownBinaryVersion before showing the "stale" annotation.

  2. ✅ Template-literal dynamic imports fixed: The Rust extractor in javascript.rs:212-213 now also matches template_string nodes via .or_else(|| find_child(&args, "template_string")) and handles backtick stripping in line 216, so import(`./plugins/${name}.js`) patterns emit edges correctly.

  3. ✅ Redundant import cleaned up: The CLI file now properly imports getNativePackageVersion in the original destructuring assignment (line 1393) instead of having a second redundant import.

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Carlos has addressed all review feedback correctly. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

View job

@carlos-alm
Copy link
Contributor Author

Addressed round-2 review feedback (d22a6ac):

  1. pair_pattern with default value — When the value child is an assignment_pattern (e.g. { foo: bar = 'default' }), we now extract the left field (the local binding name) instead of the full text.

  2. err.message undefined for non-Error throws — Both catch blocks now use err?.message ?? String(err).

  3. Platform key computation duplicated — Extracted resolvePlatformPackage() helper used by both loadNative and getNativePackageVersion.

@greptileai

Comment on lines +1039 to +1062
"object_pattern" => {
let mut names = Vec::new();
for i in 0..name_node.child_count() {
if let Some(child) = name_node.child(i) {
if child.kind() == "shorthand_property_identifier_pattern"
|| child.kind() == "shorthand_property_identifier"
{
names.push(node_text(&child, source).to_string());
} else if child.kind() == "pair_pattern" || child.kind() == "pair" {
if let Some(val) = child.child_by_field_name("value") {
// Handle `{ foo: bar = 'default' }` — extract the left-hand binding
let binding = if val.kind() == "assignment_pattern" {
val.child_by_field_name("left").unwrap_or(val)
} else {
val
};
names.push(node_text(&binding, source).to_string());
} else if let Some(key) = child.child_by_field_name("key") {
names.push(node_text(&key, source).to_string());
}
}
}
}
names
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object_assignment_pattern bindings silently dropped

The object_pattern loop handles shorthand_property_identifier_pattern and pair_pattern but not object_assignment_pattern. In tree-sitter-javascript, { a = 'default' } in a destructuring produces an object_assignment_pattern node (distinct from pair_pattern), so the binding a is silently dropped from the names vector.

For example:

const { createStore = defaultCreateStore, render } = await import('./store.js');

render is captured correctly as a shorthand_property_identifier_pattern, but createStore is an object_assignment_pattern and would be missed entirely — no edge connecting the import to createStore will be emitted.

A fix would add a branch for object_assignment_pattern:

} else if child.kind() == "object_assignment_pattern" {
    if let Some(left) = child.child_by_field_name("left") {
        names.push(node_text(&left, source).to_string());
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1582b65 — added an object_assignment_pattern arm that extracts the left-hand binding.

Capture bindings like `{ a = 'default' }` which tree-sitter parses as
object_assignment_pattern nodes, distinct from pair_pattern.

Impact: 1 functions changed, 2 affected
@carlos-alm
Copy link
Contributor Author

Addressed round-3 feedback (1582b65):

object_assignment_pattern bindings dropped — Added a branch for object_assignment_pattern nodes so { a = 'default' } destructuring correctly extracts the left binding name.

@greptileai

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

View job

Comment on lines +1033 to +1040
let name_node = match declarator.child_by_field_name("name") {
Some(n) => n,
None => return Vec::new(),
};
match name_node.kind() {
// const { a, b } = await import(...)
"object_pattern" => {
let mut names = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array_pattern bindings silently dropped from dynamic imports

The extract_dynamic_import_names function handles object_pattern destructuring and plain identifier bindings, but not array_pattern. In JavaScript, array destructuring from a dynamic import is valid:

const [first, second] = await import('./list.js');

Tree-sitter parses the name node as an array_pattern here. The current match name_node.kind() falls through to _ => Vec::new(), so the import edge is emitted (the path is resolved correctly) but zero bindings are captured. Consumers that rely on names to resolve which exports are used will silently miss these.

Consider adding an "array_pattern" arm that iterates children and collects identifier nodes, similar to the object_pattern arm, or at a minimum document that array-pattern bindings are intentionally not supported yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20203c2 — added an array_pattern arm to extract_dynamic_import_names that iterates children and collects identifier and assignment_pattern bindings.

Capture bindings like `const [first, second] = await import(...)` by
iterating identifier children of array_pattern nodes, including
assignment_pattern defaults.

Impact: 1 functions changed, 2 affected
@carlos-alm
Copy link
Contributor Author

Addressed round-4 feedback (20203c2):

array_pattern bindings dropped — Added an array_pattern arm to extract_dynamic_import_names that iterates children and collects identifier nodes (and assignment_pattern left-hand bindings for defaults like [a = 'x']).

@greptileai

@claude
Copy link

claude bot commented Mar 11, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Fix dogfood issues #408#411

Todo List

  • Read and analyze all changed files
  • Perform root cause analysis for each issue
  • Check for configuration changes and test integrity
  • Assess code quality and security
  • Address Greptile review comments
  • Provide final rating and recommendation

Root Cause Analysis

I've thoroughly analyzed the changes and each fix addresses a legitimate root issue:

#408 — Benchmark Engine Failure: The benchmark scripts properly wrap benchmarkEngine() calls in try/catch blocks (lines 182-187 and 192-197 in scripts/benchmark.js), preventing WASM failures from crashing the entire benchmark. This is a robust error handling improvement, not symptom hiding.

#409 — WASM Memory Segfaults: The solution adds proper cleanup of WASM tree-sitter resources:

  • New disposeParsers() export in src/parser.js:111-130 calls .delete() on cached parsers/queries
  • Tree cleanup in src/builder.js:1358-1366 calls tree.delete() on syntax trees after analysis
  • This addresses the root cause of memory accumulation leading to segfaults

#410 — Missing Dynamic Import Tracking: The native engine Rust code adds proper dynamicImport support:

  • Added dynamic_import field to Import struct in types.rs:139-140
  • JS extractor detects import() expressions in javascript.rs:207-220 and sets dynamicImport=true
  • This matches WASM behavior and fixes the tracking gap

#411 — Stale Native Version: Introduces getNativePackageVersion() in src/native.js:80-93 that reads from the platform package.json, with preference logic in parser.js:467-471. This solves the issue of binary-embedded versions being stale during releases.

All fixes target actual technical debt, not configuration workarounds.

Configuration Changes & Test Integrity

No High-Risk Configuration Changes: The biome.json linter config remains unchanged with standard recommended rules. No test timeouts, coverage thresholds, or TypeScript strict mode flags were relaxed.

Test Integrity Maintained: Existing tests remain unchanged. All test expectations are preserved, with no weakened assertions or conditional validations. The 13 test occurrences use standard vitest patterns.

Proper Error Handling: The try/catch blocks in benchmark scripts use appropriate error logging to stderr, maintaining debugging capability while preventing crashes.

Code Quality & Security Assessment

Security: No unsafe patterns detected. The use of execSync in token-benchmark.js is legitimate for git operations. SQL operations use parameterized queries through better-sqlite3, preventing injection attacks.

Memory Management: The WASM cleanup code follows proper patterns, calling .delete() methods within try/catch blocks to handle potential cleanup failures gracefully.

Code Structure: Changes are minimal and surgical. The Rust code properly uses #[napi(js_name)] annotations for field naming consistency. Error handling in benchmark scripts maintains fail-fast behavior while providing useful diagnostics.

Greptile Review Comments Status

All Critical Issues Addressed:

  • CLI stale version logic: Fixed in src/cli.js:1412-1419 - now properly distinguishes between unknown binary versions vs actual version mismatches
  • Package version always consulted: Fixed in src/parser.js:467-471 - condition changed from if (native && version) to if (native)
  • Shared require instance: Fixed in src/native.js:14 - hoisted createRequire to module-level _require
  • Parser initialization reset: Fixed in src/parser.js:130 - disposeParsers() now sets _initialized = false
  • Template string dynamic imports: Fixed in javascript.rs:213 - added .or_else(|| find_child(&args, "template_string")) and backtick handling

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: Four legitimate dogfood issues causing benchmark failures, memory segfaults, missing dynamic import tracking, and stale version reporting.
**Why This Approach**: Each fix targets the specific technical root cause rather than masking symptoms through configuration changes.
**Risk Assessment**: Low risk - changes are surgical, well-contained, and maintain existing behavior while fixing edge cases.

## Critical Concerns
None identified. All changes address real technical debt with appropriate error handling and resource management.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)
- Action: APPROVE
- Reasoning: Solid engineering fixes that address actual issues. Well-tested with comprehensive test suite coverage. Minor deduction for the benchmark script fixes being somewhat reactive rather than proactive, but this is typical for dogfood issue resolution.

View job

try {
await disposeModel();
} catch (disposeErr) {
console.error(` disposeModel failed: ${disposeErr.message}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe disposeErr.message access in new finally block

benchmark.js was explicitly updated in this PR to use err?.message ?? String(err) for safe error message formatting, but the new finally block here uses disposeErr.message directly. If disposeModel() ever rejects with a non-Error value (a string, number, or null), disposeErr.message evaluates to undefined and the log line becomes " disposeModel failed: undefined".

Apply the same null-safe pattern used in benchmark.js:

Suggested change
console.error(` disposeModel failed: ${disposeErr.message}`);
console.error(` disposeModel failed: ${disposeErr?.message ?? String(disposeErr)}`);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — both catch blocks in embedding-benchmark.js now use err?.message ?? String(err) to handle non-Error throws safely, matching the pattern in benchmark.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — both catch blocks in embedding-benchmark.js now use err?.message ?? String(err) for safe error formatting.

@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +111 to +131
export function disposeParsers() {
if (_cachedParsers) {
for (const [, parser] of _cachedParsers) {
if (parser && typeof parser.delete === 'function') {
try {
parser.delete();
} catch {}
}
}
_cachedParsers = null;
}
for (const [, query] of _queryCache) {
if (query && typeof query.delete === 'function') {
try {
query.delete();
} catch {}
}
}
_queryCache.clear();
_initialized = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Language objects are not explicitly deleted

disposeParsers calls .delete() on each cached Parser and Query, but not on the Language objects created by await Language.load(...) inside createParsers(). In web-tree-sitter, Language objects are WASM-backed; their WASM memory is only freed when .delete() is explicitly called on the JS wrapper — JS garbage collection alone does not reclaim it.

After createParsers() returns, the lang locals are out of scope, but each Parser still holds an internal reference to its Language. Calling parser.delete() frees the parser's C++ object, but it may not decrement the Language's WASM reference count if web-tree-sitter treats them independently.

If Language WASM memory is indeed not freed here, the segfault fix (#409) may be incomplete for very long-lived processes running many repeated builds. Consider caching the Language objects alongside parsers so they can also be deleted:

// inside disposeParsers(), after deleting parsers:
for (const [, lang] of _cachedLanguages) {
  if (lang && typeof lang.delete === 'function') {
    try { lang.delete(); } catch {}
  }
}
_cachedLanguages = null;

where _cachedLanguages is a parallel Map populated alongside _cachedParsers in createParsers().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7d55362 — disposeParsers() now caches Language objects in a parallel _cachedLanguages map and calls .delete() on each during disposal.

Language objects from Language.load() are WASM-backed and their memory
is only freed when .delete() is explicitly called. Cache them alongside
parsers so disposeParsers() can release them, preventing WASM memory
leaks in long-lived processes with repeated builds.

Impact: 2 functions changed, 4 affected
@carlos-alm
Copy link
Contributor Author

Fixed the Language object leak flagged by Greptile (commit 7d55362): Language objects from Language.load() are now cached in a _cachedLanguages map during createParsers(), and disposeParsers() calls .delete() on each one -- same pattern as the parser and query cleanup.

@greptileai

Comment on lines +1059 to +1064
} else if child.kind() == "object_assignment_pattern" {
// Handle `{ a = 'default' }` — extract the left-hand binding
if let Some(left) = child.child_by_field_name("left") {
names.push(node_text(&left, source).to_string());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rest_pattern bindings silently dropped from object_pattern

The object_pattern arm handles shorthand_property_identifier_pattern, pair_pattern, and object_assignment_pattern, but not rest_pattern. For:

const { a, b, ...rest } = await import('./mod.js');

Tree-sitter emits ...rest as a rest_pattern node containing an identifier. It falls through to no branch, so rest is never added to names. The import edge is still emitted correctly, but the rest binding name is silently dropped from the graph.

Consider adding a branch after object_assignment_pattern:

} else if child.kind() == "rest_pattern" {
    // `const { a, ...rest } = await import(...)`
    if let Some(inner) = child.child(0) {
        if inner.kind() == "identifier" {
            names.push(node_text(&inner, source).to_string());
        }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in de32c38 — added rest_pattern/rest_element handling to extract_dynamic_import_names so the identifier inside the spread is captured.

Comment on lines +1048 to +1055
if let Some(val) = child.child_by_field_name("value") {
// Handle `{ foo: bar = 'default' }` — extract the left-hand binding
let binding = if val.kind() == "assignment_pattern" {
val.child_by_field_name("left").unwrap_or(val)
} else {
val
};
names.push(node_text(&binding, source).to_string());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested destructuring in pair_pattern produces a garbled import name

The else val branch is intended for the simple rename case { foo: bar } where val is an identifier. However, it does not guard against val being an object_pattern or array_pattern (i.e. nested destructuring):

const { a: { b, c } } = await import('./mod.js');

Here the value of the pair_pattern is an object_pattern node. node_text(&val, source) returns the entire text "{ b, c }", which is pushed as a single name string. The graph then records an import binding named literally "{ b, c }" that will never resolve to a real symbol.

A minimal fix guards against non-identifier values:

let binding = if val.kind() == "assignment_pattern" {
    val.child_by_field_name("left").unwrap_or(val)
} else if val.kind() == "identifier" {
    val
} else {
    // Nested pattern (e.g. { foo: { bar } }) — skip; full nested
    // support requires recursive extraction.
    continue;
};
names.push(node_text(&binding, source).to_string());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in de32c38 — the pair_pattern handler now guards against non-identifier values. When val is an object_pattern/array_pattern (nested destructuring), we skip it instead of emitting the raw text as a garbled name.

Add rest_pattern/rest_element handling to extract_dynamic_import_names
so `const { a, ...rest } = await import(...)` captures `rest`.

Guard pair_pattern against nested patterns (e.g. `{ a: { b, c } }`)
to prevent garbled import names — skip instead of emitting raw text.

Impact: 1 functions changed, 2 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +1083 to +1096
"array_pattern" => {
let mut names = Vec::new();
for i in 0..name_node.child_count() {
if let Some(child) = name_node.child(i) {
if child.kind() == "identifier" {
names.push(node_text(&child, source).to_string());
} else if child.kind() == "assignment_pattern" {
if let Some(left) = child.child_by_field_name("left") {
names.push(node_text(&left, source).to_string());
}
}
}
}
names
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rest_element silently dropped from array_pattern

The object_pattern arm correctly handles rest_pattern/rest_element (lines 1068–1074), but the new array_pattern arm only handles identifier and assignment_pattern. For:

const [first, ...rest] = await import('./list.js');

tree-sitter emits ...rest as a rest_element node inside the array_pattern. It falls through to no branch, so rest is silently dropped from names — the import edge is emitted but the rest binding is never recorded in the graph.

The fix mirrors the object_pattern handling:

Suggested change
"array_pattern" => {
let mut names = Vec::new();
for i in 0..name_node.child_count() {
if let Some(child) = name_node.child(i) {
if child.kind() == "identifier" {
names.push(node_text(&child, source).to_string());
} else if child.kind() == "assignment_pattern" {
if let Some(left) = child.child_by_field_name("left") {
names.push(node_text(&left, source).to_string());
}
}
}
}
names
"array_pattern" => {
let mut names = Vec::new();
for i in 0..name_node.child_count() {
if let Some(child) = name_node.child(i) {
if child.kind() == "identifier" {
names.push(node_text(&child, source).to_string());
} else if child.kind() == "assignment_pattern" {
if let Some(left) = child.child_by_field_name("left") {
names.push(node_text(&left, source).to_string());
}
} else if child.kind() == "rest_pattern" || child.kind() == "rest_element" {
// Handle `[a, ...rest]` — extract the identifier inside the spread
if let Some(inner) = child.child(0) {
if inner.kind() == "identifier" {
names.push(node_text(&inner, source).to_string());
}
}
}
}
}
names
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant