Skip to content

refactor: dead code cleanup, shared abstractions, and empty catch replacement#489

Merged
carlos-alm merged 11 commits intomainfrom
refactor/titan-cleanup
Mar 17, 2026
Merged

refactor: dead code cleanup, shared abstractions, and empty catch replacement#489
carlos-alm merged 11 commits intomainfrom
refactor/titan-cleanup

Conversation

@carlos-alm
Copy link
Contributor

Summary

Titan quality sweep — phases 1–3: foundational cleanup before decomposition.

  • Dead code removal: Remove unused truncate and BATCH_CHUNK exports, un-export internal truncStart
  • Shared abstractions: Extract duplicate findNodes from cfg.js and dataflow.js into features/shared/find-nodes.js
  • Empty catch replacement: Replace silent catch {} blocks with explicit error handling across 4 layers:
    • DB connection and migrations (8 catches → explicit fallbacks)
    • Domain analysis (context, symbol-lookup, exports, impact, module-map)
    • Parser (6 catches → logged warnings)
    • Features (cfg, complexity, dataflow)

Files changed (15)

  • src/ast-analysis/shared.js — remove dead truncate export
  • src/domain/graph/builder/helpers.js — remove dead BATCH_CHUNK export
  • src/presentation/table.js — un-export internal truncStart
  • src/features/cfg.js, src/features/dataflow.js — extract shared findNodes
  • src/features/shared/find-nodes.js — new shared utility
  • src/db/connection.js, src/db/migrations.js — empty catch fixes
  • src/domain/analysis/*.js — empty catch fixes
  • src/domain/parser.js — empty catch fixes
  • src/features/complexity.js — empty catch fixes

Test plan

  • All 1883 tests pass
  • Lint clean
  • No new cycles introduced
  • Blast radius ≤ 5 transitive callers per target

- Remove dead `truncate` function from ast-analysis/shared.js (0 consumers)
- Remove dead `truncStart` function from presentation/table.js (0 consumers)
- Un-export `BATCH_CHUNK` in builder/helpers.js (only used internally)

Skipped sync.json targets that were false positives:
- BUILTIN_RECEIVERS: used by incremental.js + build-edges.js
- TRANSIENT_CODES/RETRY_DELAY_MS: internal to readFileSafe
- MAX_COL_WIDTH: internal to printAutoTable
- findFunctionNode: re-exported from index.js, used in tests

Impact: 1 functions changed, 32 affected
…ures

Impact: 5 functions changed, 7 affected
connection.js: add debug() logging to all 8 catch-with-fallback blocks
so failures are observable without changing behavior.

migrations.js: replace 14 try/catch blocks in initSchema with hasColumn()
and hasTable() guards. CREATE INDEX calls use IF NOT EXISTS directly.
getBuildMeta uses hasTable() check instead of try/catch.

Impact: 10 functions changed, 19 affected
Add debug() logging to 10 empty catch blocks across context.js,
symbol-lookup.js, exports.js, impact.js, and module-map.js.
All catches retain their fallback behavior but failures are now
observable via debug logging.

Impact: 6 functions changed, 18 affected
Add debug() logging to 6 empty catch blocks: 3 in disposeParsers()
for WASM resource cleanup, 2 in ensureWasmTrees() for file read and
parse failures, and 1 in getActiveEngine() for version lookup.

Impact: 3 functions changed, 0 affected
Add debug() logging to 9 empty catch blocks across complexity.js (5),
cfg.js (2), and dataflow.js (2). All catches for file read and parse
failures now log the error message before continuing.

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

claude bot commented Mar 17, 2026

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


Reviewing PR: Dead Code Cleanup and Empty Catch Replacement

Todo List:

  • Phase 1: Root cause analysis - understand the problem being solved
  • Phase 2: Configuration change detection - check for relaxed rules
  • Phase 3: Test integrity verification
  • Phase 4: Code quality review of actual changes
  • Phase 5: Backlog compliance assessment
  • Provide final rating and recommendation

Phases 1-4 Summary: This PR addresses legitimate issues with no configuration relaxation or test integrity concerns, and implements high-quality refactoring.

Root Cause Analysis

Problem Being Solved: Code quality cleanup in preparation for future work — removing dead exports, extracting shared abstractions, replacing silent error handling with observable debugging.
Why This Approach: Systematic cleanup across 4 layers (db, domain, parser, features) using established patterns (debug logging, hasTable/hasColumn guards).
Risk Assessment: Very low risk — purely internal changes, preserves all existing behavior.

Backlog Compliance

  • Zero-dep: ✓ — No new runtime dependencies added
  • Foundation-aligned: ✓ — Aligns with P1 (graph stays current via better error visibility) and P8 (focuses on development quality, not linting rules)
  • Problem-fit: 3 — Useful refactoring but doesn't directly address the core "lost AI agents" problem
  • Breaking: No — Purely internal changes, no API/CLI/MCP contract modifications
  • Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Critical Concerns

None identified. This is a textbook quality refactoring:

  • Dead code removal verified through grep analysis
  • Shared abstractions properly extracted with clean interfaces
  • Empty catch blocks replaced with debug logging (preserves behavior, adds observability)
  • migrations.js significantly improved by replacing try/catch with proper guards

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: High-quality foundational cleanup that improves code maintainability and debugging without any risk. The systematic approach across all layers and careful preservation of existing behavior demonstrates strong engineering discipline. Slightly short of 5 stars only because it's incremental quality work rather than a feature that transforms user experience.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR is a well-scoped foundational cleanup covering three areas: dead-code removal (truncate, BATCH_CHUNK, truncStart), extraction of duplicate findNodes helpers into a shared utility, and replacement of silent catch {} blocks with debug()-logged fallbacks across DB, domain, parser, and feature layers. All four issues raised in prior review rounds have been correctly addressed in the latest commits.

Key changes:

  • src/features/shared/find-nodes.js — new shared helper consolidating two near-identical findNodes implementations; includes a default-value fix and an explicit guard (from the 566f318 follow-up) for the kinds.length === 0 case
  • src/db/migrations.jsinitSchema now uses hasTable/hasColumn helpers instead of try-catch-ignore for all legacy ALTER TABLE and CREATE INDEX calls; getBuildMeta null-on-failure contract restored with an outer try-catch
  • src/db/connection.js and domain/feature files — 20+ empty catches now log the error message via debug() while preserving every original fallback value, making failures visible without changing observable behaviour
  • Dead exports (truncate, BATCH_CHUNK, truncStart) confirmed unused and cleanly removed

One minor style note: the shared findNodes SQL silently expands from the specific column projection used by the old cfg.js version to SELECT *, and adds an ORDER BY file, line that wasn't present in the original CFG query. Both changes are harmless (all needed columns are still returned, ordering is now consistent), but the projection difference slightly increases the data fetched for CFG callers.

Confidence Score: 5/5

  • Safe to merge — all prior review issues resolved, no logic regressions, and the one noted SQL difference is benign.
  • All 15 changed files are refactors with no new logic; dead-code removals are confirmed free of callers; empty-catch replacements preserve original fallback values; the four prior review issues are demonstrably fixed in the latest commits; the only impurity is a minor SELECT * vs specific-column projection difference in the shared findNodes, which does not break any callers and all 1883 tests pass.
  • src/features/shared/find-nodes.js — minor SELECT * vs specific-column difference worth tracking if query performance becomes a concern on large graphs.

Important Files Changed

Filename Overview
src/features/shared/find-nodes.js New shared utility extracting duplicate findNodes logic; uses SELECT * instead of the specific column list the cfg.js version had, adding implicit ORDER BY and fetching extra columns — minor performance and ordering difference vs the original.
src/db/migrations.js All four previously-flagged issues resolved: getBuildMeta null-on-failure contract restored, ALTER TABLE calls wrapped in hasTable guards, UPDATE/index statements consistently guarded, and hasColumn/hasTable helpers introduced cleanly.
src/db/connection.js Eight silent catch blocks replaced with debug-logged fallbacks; all fallback return values preserved, no behavioral regressions.
src/features/cfg.js Local findNodes removed in favour of shared import; isTestFile import correctly dropped; empty catches replaced with debug-logged continuations.
src/features/dataflow.js Local findNodes removed in favour of shared import; isTestFile retained as it's still used at multiple other call sites; empty catches replaced with debug logs.
src/features/complexity.js Six empty catches replaced with debug-logged fallbacks; nested catch correctly uses e2 to avoid shadowing the outer e; all original fallback values preserved.
src/domain/parser.js Dispose loop variables renamed from _ to id for use in debug messages; six empty catches replaced with debug logs; no logic changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["cfg.js / dataflow.js\n(callers)"] -->|"import findNodes"| B["features/shared/find-nodes.js\nfindNodes(db, name, opts, defaultKinds)"]
    B -->|"SELECT * FROM nodes\nORDER BY file, line"| C[(SQLite nodes table)]

    D["initSchema(db)"] --> E{"hasTable nodes?"}
    E -->|"yes"| F["hasColumn checks\n→ ALTER TABLE if missing\n→ CREATE INDEX IF NOT EXISTS\n→ UPDATE qualified_name"]
    E -->|"no"| G[skip]
    D --> H{"hasTable edges?"}
    H -->|"yes"| I["hasColumn checks\n→ ALTER TABLE if missing"]
    H -->|"no"| J[skip]

    K["getBuildMeta(db, key)"] --> L{"hasTable build_meta?"}
    L -->|"no"| M["return null"]
    L -->|"yes"| N["prepare/get\ntry-catch → debug + null"]
Loading

Last reviewed commit: 726323c

Comment on lines +12 to +13
export function findNodes(db, name, opts = {}, defaultKinds) {
const kinds = opts.kind ? [opts.kind] : defaultKinds;
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 defaultKinds crash when omitted

defaultKinds has no default value and no guard. If a caller passes opts without a kind property and forgets the fourth argument, kinds resolves to undefined, causing kinds.map(() => '?') to throw a TypeError: Cannot read properties of undefined (reading 'map'). All current call sites pass it correctly, but the function silently allows omission at the signature level, making it a footgun for future callers.

Add a default or a guard:

Suggested change
export function findNodes(db, name, opts = {}, defaultKinds) {
const kinds = opts.kind ? [opts.kind] : defaultKinds;
export function findNodes(db, name, opts = {}, defaultKinds = []) {
const kinds = opts.kind ? [opts.kind] : defaultKinds;
if (kinds.length === 0) throw new Error('findNodes: no kinds specified');

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 566f318 — added default value \ and an explicit guard that throws \ when no kinds are resolved.

Comment on lines +318 to +322
if (hasTable(db, 'nodes')) {
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
} catch {
/* nodes table may not exist yet */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
} catch {
/* already exists */
}
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 hasTable guard inconsistency: UPDATE is guarded, indexes are not

The UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL (line 319) is now wrapped in if (hasTable(db, 'nodes')), protecting against the case where the table doesn't exist. However, the two CREATE INDEX statements immediately below (lines 321–322) are unconditional: they will throw if the nodes table is somehow absent, regardless of the guard above.

In practice the migrations block always creates nodes, so this is unlikely to trigger. But the guard on the UPDATE should be extended to cover the indexes for consistency — or the guard should be dropped entirely since initSchema always runs migrations first and nodes is guaranteed to exist at that point:

Suggested change
if (hasTable(db, 'nodes')) {
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
} catch {
/* nodes table may not exist yet */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
} catch {
/* already exists */
}
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
if (hasTable(db, 'nodes')) {
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
}

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 7c30d49 — moved both CREATE INDEX statements inside the existing hasTable(db, 'nodes') guard for consistency.

Comment on lines +255 to +258
export function getBuildMeta(db, key) {
try {
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
return row ? row.value : null;
} catch {
return null;
}
if (!hasTable(db, 'build_meta')) return null;
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
return row ? row.value : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 getBuildMeta now propagates unexpected DB errors

The old implementation swallowed every exception and returned null. The new version only guards against the build_meta table not existing — any other failure (disk error, corrupted SQLite file, etc.) will now propagate as an unhandled exception.

All known callers (pipeline.js:65, pipeline.js:70, finalize.js:44–45, info.js:45–47) do not wrap getBuildMeta in a try-catch, meaning these sites will now crash on unexpected DB errors instead of silently returning null.

If the intent is to keep the "always return null on failure" contract, a general catch around the prepare().get() call should be retained:

Suggested change
export function getBuildMeta(db, key) {
try {
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
return row ? row.value : null;
} catch {
return null;
}
if (!hasTable(db, 'build_meta')) return null;
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
return row ? row.value : null;
export function getBuildMeta(db, key) {
if (!hasTable(db, 'build_meta')) return null;
try {
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
return row ? row.value : null;
} catch (e) {
debug(`getBuildMeta failed for key "${key}": ${e.message}`);
return null;
}
}

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 7c30d49 — restored try-catch around the prepare/get call to preserve the original null-on-failure contract. Unexpected DB errors are now logged via debug() and return null.

Prevents TypeError when callers omit the fourth argument by defaulting
to an empty array and throwing a clear error when no kinds are specified.
… to getBuildMeta

Move CREATE INDEX statements inside the existing hasTable guard for
consistency. Restore try-catch in getBuildMeta to preserve the original
"return null on failure" contract for all callers.
@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines 296 to 322
if (!hasColumn(db, 'nodes', 'end_line')) {
db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'edges', 'confidence')) {
db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'edges', 'dynamic')) {
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'role')) {
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
if (!hasColumn(db, 'nodes', 'parent_id')) {
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
if (!hasColumn(db, 'nodes', 'qualified_name')) {
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'scope')) {
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'visibility')) {
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
} catch {
/* already exists */
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 ALTER TABLE calls unguarded while hasTable guard exists below

Lines 296–322 call hasColumn(db, 'nodes', ...) and hasColumn(db, 'edges', ...). When a table does not exist, db.pragma('table_info(<table>)') returns an empty array — so hasColumn returns false — and the db.exec('ALTER TABLE nodes ADD COLUMN ...') will throw an unhandled exception. The old try-catch blocks silently absorbed this (and the "already exists" error) for both cases.

The hasTable(db, 'nodes') guard at line 323 implicitly acknowledges that nodes could be absent in some edge case, yet the ALTER TABLE calls above have no such protection. In the common case migrations always create nodes first, so this is benign today — but the guarding is logically inconsistent: either nodes is guaranteed to exist (and the hasTable check at 323 should be dropped), or it may not (and the ALTER TABLE calls also need a guard).

Suggested change
if (!hasColumn(db, 'nodes', 'end_line')) {
db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'edges', 'confidence')) {
db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'edges', 'dynamic')) {
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'role')) {
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
if (!hasColumn(db, 'nodes', 'parent_id')) {
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
} catch {
/* already exists */
}
try {
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
if (!hasColumn(db, 'nodes', 'qualified_name')) {
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'scope')) {
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
} catch {
/* already exists */
}
try {
if (!hasColumn(db, 'nodes', 'visibility')) {
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
} catch {
/* already exists */
}
if (hasTable(db, 'nodes')) {
if (!hasColumn(db, 'nodes', 'end_line')) {
db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
}
if (!hasColumn(db, 'nodes', 'role')) {
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
}
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
if (!hasColumn(db, 'nodes', 'parent_id')) {
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
}
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
if (!hasColumn(db, 'nodes', 'qualified_name')) {
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
}
if (!hasColumn(db, 'nodes', 'scope')) {
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
}
if (!hasColumn(db, 'nodes', 'visibility')) {
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
}
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
}
if (hasTable(db, 'edges')) {
if (!hasColumn(db, 'edges', 'confidence')) {
db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
}
if (!hasColumn(db, 'edges', 'dynamic')) {
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
}
}

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 726323c — wrapped all legacy ALTER TABLE calls in hasTable guards. Nodes-related operations are inside hasTable(db, 'nodes') and edges-related ones inside hasTable(db, 'edges'), making the guarding consistent throughout initSchema.

@carlos-alm
Copy link
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit c9f3460 into main Mar 17, 2026
18 checks passed
@carlos-alm carlos-alm deleted the refactor/titan-cleanup branch March 17, 2026 10:56
@github-actions github-actions bot locked and limited conversation to collaborators Mar 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant