Skip to content

refactor: decompose AST analysis visitors and domain builder stages#491

Merged
carlos-alm merged 18 commits intomainfrom
refactor/titan-ast-builder
Mar 17, 2026
Merged

refactor: decompose AST analysis visitors and domain builder stages#491
carlos-alm merged 18 commits intomainfrom
refactor/titan-ast-builder

Conversation

@carlos-alm
Copy link
Contributor

Summary

Titan quality sweep — phase 5a: decompose the two highest-complexity subsystems.

AST analysis visitors

  • createCfgVisitor (worst in codebase: cog 337, 9.39 estimated bugs) → extract per-statement handlers (processIf, processSwitch, processFor, etc.) — cog 337→18
  • createDataflowVisitor → extract per-expression handlers — cog 215→12
  • createComplexityVisitor → extract per-metric handlers — cog 156→8
  • runAnalyses → decompose engine orchestrator — cog 212→15

Domain builder stages

  • buildEdges (cog 351, 3.84 bugs) → split by edge type (import, call, hierarchy)
  • insertNodes → split by node processing phase
  • detectChanges → separate discovery from hashing
  • Pipeline + incremental → simplify orchestration

Stacked on

Files changed (9)

  • src/ast-analysis/visitors/cfg-visitor.js
  • src/ast-analysis/visitors/dataflow-visitor.js
  • src/ast-analysis/visitors/complexity-visitor.js
  • src/ast-analysis/engine.js
  • src/domain/graph/builder/stages/build-edges.js
  • src/domain/graph/builder/stages/insert-nodes.js
  • src/domain/graph/builder/stages/detect-changes.js
  • src/domain/graph/builder/incremental.js
  • src/domain/graph/builder/pipeline.js

Test plan

  • All 1883 tests pass
  • Lint clean
  • No new cycles (CFG visitor cycle is inherent to recursive CFG construction)

- 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
Split the monolithic walkJavaScriptNode switch (13 cases, cognitive 228)
into 11 focused handler functions. The dispatcher is now a thin switch
that delegates to handleFunctionDecl, handleClassDecl, handleMethodDef,
handleInterfaceDecl, handleTypeAliasDecl, handleVariableDecl,
handleEnumDecl, handleCallExpr, handleImportStmt, handleExportStmt,
and handleExpressionStmt.

The expression_statement case now reuses the existing
handleCommonJSAssignment helper, eliminating ~50 lines of duplication.

Worst handler complexity: handleVariableDecl (cognitive 20), down from
the original monolithic function (cognitive 279).

Impact: 13 functions changed, 3 affected
Split walkPythonNode switch into 7 focused handlers: handlePyFunctionDef,
handlePyClassDef, handlePyCall, handlePyImport, handlePyExpressionStmt,
handlePyImportFrom, plus the decorated_definition inline dispatch.

Moved extractPythonParameters, extractPythonClassProperties, walkInitBody,
and findPythonParentClass from closures to module-scope functions.

Impact: 12 functions changed, 5 affected
Split walkJavaNode switch into 8 focused handlers plus an
extractJavaInterfaces helper. Moved findJavaParentClass to module scope.
The class_declaration case (deepest nesting in the file) is now split
between handleJavaClassDecl and extractJavaInterfaces.

Impact: 12 functions changed, 5 affected
Apply the same per-category handler decomposition to all remaining
language extractors: Go (6 handlers), Ruby (8 handlers), PHP (11
handlers), C# (11 handlers), Rust (9 handlers), HCL (4 handlers).

Each extractor now follows the template established by the JS extractor:
- Thin entry function creates ctx, delegates to walkXNode
- walkXNode is a thin dispatcher switch
- Each case is a named handler function at module scope
- Helper functions (findParentClass, etc.) moved to module scope

Impact: 66 functions changed, 23 affected
…pers

Move nested handler functions to module level in cfg-visitor.js,
dataflow-visitor.js, and complexity-visitor.js — reducing cognitive
complexity of each factory function from 100-337 down to thin
coordinators. Extract WASM pre-parse, visitor setup, result storage,
and build delegation from runAnalyses into focused helper functions.

Impact: 66 functions changed, 43 affected
Extract edge-building by type (import, call-native, call-JS, class
hierarchy) from buildEdges. Extract per-phase insertion logic from
insertNodes. Extract scoped/incremental/full-build paths and
reverse-dep cascade from detectChanges. Extract setup, engine init,
alias loading from pipeline.js. Extract node/edge-building helpers
from incremental.js rebuildFile.

Impact: 44 functions changed, 19 affected
@claude
Copy link

claude bot commented Mar 17, 2026

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


PR Review: AST Analysis Visitors and Domain Builder Decomposition

Reviewing this refactoring PR that claims to decompose high-complexity subsystems.

Review Checklist

  • Phase 1: Root Cause Analysis - Understand problem being solved
  • Phase 2: Configuration Change Detection - Check for rule relaxations
  • Phase 3: Test Integrity - Verify no weakened assertions
  • Phase 4: Code Quality - Analyze refactored code structure
  • Phase 5: Backlog Compliance - Rate against foundation principles
  • Final Assessment - Provide rating and recommendation

Root Cause Analysis

Problem Being Solved: This PR addresses genuine complexity issues in the worst subsystems in the codebase:

  • CFG visitor: Claims cog 337→18 (massive switch statement → focused per-statement handlers)
  • AST engine: Claims cog 212→15 (orchestrator decomposition)
  • Domain builder: Claims cog 351→reduced (edge building split by type)

Why This Approach: The decomposition strategy is sound:

  • Extract pure functions for each control flow statement type (processIf, processSwitch, etc.)
  • Separate concerns: import edges vs call edges vs hierarchy edges
  • Factor out common patterns and eliminate redundant conditionals

Risk Assessment: LOW. This is pure refactoring with no functional changes.

Configuration & Test Analysis

No configuration relaxation - biome.json unchanged
No test modifications - all tests preserved unchanged (good practice)
Claims maintained - "All 1883 tests pass, lint clean, no new cycles"

Code Quality Analysis

Strengths:

  • CFG visitor (778 lines): Well-structured decomposition into focused handlers

    • processIf(), processSwitch(), processForLoop() etc. are clean single-purpose functions
    • State management properly isolated in makeFuncState()
    • Error-prone nested conditionals replaced with dispatch pattern
  • AST engine: Clean separation of visitor setup, result collection, and delegation

    • Unified walk framework eliminates redundant tree traversals
    • Proper lazy loading and extension-based feature gates
  • Domain builder stages: Good separation of concerns

    • Import resolution separate from call resolution
    • Native engine delegation cleanly isolated
    • Incremental hash updates properly handled

Concerns:

  • No verification of complexity claims - Cannot independently verify cog 337→18 reduction
  • Large function count - CFG visitor now has 20+ functions but this improves readability vs monolithic switch

Backlog Compliance

  • Zero-dep: ✓ — no new runtime dependencies
  • Foundation-aligned: ✓ — pure refactoring, no cloud calls or agent behavior
  • Problem-fit: 4/5 — directly improves codebase maintainability for AI analysis accuracy
  • Breaking: No — purely internal refactoring, no API changes

Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Critical Concerns

None identified. This is high-quality refactoring work that:

  • Addresses real complexity hotspots
  • Maintains test coverage
  • Preserves functionality
  • Improves maintainability

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: Excellent refactoring that tackles genuine complexity debt. The decomposition patterns are sound, no configuration rules were relaxed, and all functionality is preserved. Missing 5th star only due to inability to verify the claimed dramatic complexity reductions (cog 337→18), but the code structure clearly supports significant improvement.

This is the kind of disciplined refactoring work that makes codebases more maintainable without introducing risk.


@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR is a large structural refactoring — Titan quality sweep phase 5a — that decomposes two high-complexity subsystems (AST analysis visitors and domain builder stages) into focused, independently testable helpers. No new features are introduced; the public API surface (runAnalyses, buildGraph, stage functions) is unchanged.

Key changes:

  • createCfgVisitor: per-statement handlers (processIf, processForLoop, processWhileLoop, processSwitch, processTryCatch, etc.) extracted — complexity 337 → 18
  • createDataflowVisitor: per-expression handlers (handleVarDeclarator, handleAssignment, handleCallExpr, handleExprStmtMutation, handleReturn) extracted — complexity 215 → 12
  • createComplexityVisitor: metric classifiers (classifyHalstead, classifyBranchNode, classifyPlainElse) extracted — complexity 156 → 8
  • runAnalyses in engine.js: split into ensureWasmTreesIfNeeded, setupVisitors, storeComplexityResults, storeCfgResults, delegateToBuildFunctions
  • buildEdges, insertNodes, detectChanges: split by edge type and processing phase; pipeline.js and incremental.js simplified accordingly
  • Extractor files (javascript.js, csharp.js, etc.): inner walkXxxNode closures lifted to module-level functions with a shared ctx object

Issues found:

  • storeCfgResults in engine.js uses !def.cfg?.blocks?.length to decide whether to store visitor results, but setupVisitors (and cfg.js) use the stricter d.cfg !== null guard to respect a null sentinel that marks functions as explicitly excluded from CFG computation. When a file contains both a skip-sentinel function and a function that needs CFG, the visitor runs for the file and produces results for all functions; storeCfgResults then incorrectly writes blocks back to the sentinel function.
  • handleVarDeclarator in dataflow-visitor.js contains a tautological ternary — both branches return nameNode.text — suggesting an intended per-type name extraction was accidentally collapsed during extraction.
  • handleReturn in dataflow-visitor.js contains a dead guard (node.parent?.type === rules.returnNode) that was valid in the original flat traversal but is unreachable in the filtered visitor dispatch.

Confidence Score: 3/5

  • Safe to merge for most codebases; two logic issues in the refactored visitor code warrant a fix before merging if functions with the cfg=null sentinel are used in practice.
  • 1883 tests pass and the decomposition is structurally clean. However, the cfg=null sentinel inconsistency in storeCfgResults could silently override explicitly excluded functions' CFG data in files that contain a mix of sentinel and non-sentinel functions — a subtle behavioral regression from the original. The tautological ternary in handleVarDeclarator suggests an intended name-extraction path was lost during extraction. Neither issue is caught by tests if the relevant code paths aren't exercised.
  • src/ast-analysis/visitors/dataflow-visitor.js (tautological ternary + dead guard), src/ast-analysis/engine.js (cfg=null sentinel inconsistency in storeCfgResults)

Important Files Changed

Filename Overview
src/ast-analysis/engine.js Refactored into focused helpers (ensureWasmTreesIfNeeded, setupVisitors, storeComplexityResults, storeCfgResults, delegateToBuildFunctions). Minor issue: storeCfgResults uses !def.cfg?.blocks?.length instead of the cfg !== null sentinel guard used elsewhere in setupVisitors and cfg.js, which could incorrectly apply CFG data to functions marked with the null sentinel.
src/ast-analysis/visitors/cfg-visitor.js Large, well-structured decomposition: per-statement handlers (processIf, processSwitch, processForLoop, processWhileLoop, processDoWhileLoop, processInfiniteLoop, processTryCatch) extracted cleanly. Logic for labeled break/continue, elif siblings, and try/catch/finally handlers is correct. Infinite loop correctly omits the header→exit edge, relying only on break edges.
src/ast-analysis/visitors/dataflow-visitor.js Two issues found: (1) Tautological ternary at line 119–123 in handleVarDeclarator — both branches return nameNode.text, so intended per-type transformation is lost. (2) Dead guard at line 265 in handleReturn — checks if a return_statement is nested inside another return_statement (impossible in valid syntax); this is a leftover from a lower-level traversal pattern.
src/ast-analysis/visitors/complexity-visitor.js Clean decomposition into classifyHalstead, classifyBranchNode, classifyPlainElse, collectResult, and resetAccumulators helpers. fileLevelWalk mode correctly scopes per-function accumulators via enterFunction/exitFunction. exitNode halsteadSkipDepth decrement is balanced with enterNode increment within active-function scope.
src/domain/graph/builder/stages/build-edges.js Well-decomposed into buildImportEdges, buildBarrelEdges, buildCallEdgesNative, buildCallEdgesJS, buildClassHierarchyEdges, and supporting helpers. Node lookup maps are set up once before the transaction. Logic for barrel resolution, receiver edges, and class hierarchy edges is intact.
src/domain/graph/builder/stages/detect-changes.js Three-tier change detection (journal, mtime+size, hash) decomposed into focused helpers. Previously-reviewed issues (dropped rootDir, missing log, duplicated hasEmbeddings detection) confirmed fixed. handleScopedBuild, handleFullBuild, and handleIncrementalBuild are clean and use shared detectHasEmbeddings.
src/domain/graph/builder/stages/insert-nodes.js Four-phase decomposition (insertDefinitionsAndExports, insertChildren, insertContainmentEdges, updateFileHashes) is correct. bulkNodeIdsByFile is called in each phase that needs IDs — this is necessary since phase 1 inserts nodes that phase 2/3 need to reference. Hash updates and metadata-only healing are preserved.
src/domain/graph/builder/pipeline.js Clean orchestration: setupPipeline, runPipelineStages, and formatTimingResult are well-separated. Stage ordering is correct (collect → detect → parse → insert → resolve → edges → structure → analyses → finalize). earlyExit is checked after detectChanges and after the full pipeline.
src/domain/graph/builder/incremental.js rebuildFile decomposed into insertFileNodes, buildImportEdges, buildImportedNamesMap, and buildCallEdges helpers. Logic is correct; aliases object is initialized to empty defaults since incremental rebuilds don't have path-alias context at the call site.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildGraph] --> B[setupPipeline]
    B --> C[runPipelineStages]
    C --> D[collectFiles]
    D --> E[detectChanges]
    E -->|earlyExit| Z[return]
    E -->|scoped| ES[handleScopedBuild]
    E -->|full| EF[handleFullBuild]
    E -->|incremental| EI[handleIncrementalBuild]
    ES & EF & EI --> F[parseFiles]
    F --> G[insertNodes]
    G --> G1[insertDefinitionsAndExports]
    G1 --> G2[insertChildren]
    G2 --> G3[insertContainmentEdges]
    G3 --> G4[updateFileHashes]
    G4 --> H[resolveImports]
    H --> I[buildEdges]
    I --> I1[buildImportEdges]
    I1 --> I2{native engine?}
    I2 -->|yes| I3[buildCallEdgesNative]
    I2 -->|no| I4[buildCallEdgesJS]
    I3 & I4 --> J[buildStructure]
    J --> K[runAnalyses / engine.js]
    K --> K1[ensureWasmTreesIfNeeded]
    K1 --> K2[setupVisitors per file]
    K2 --> K3[walkWithVisitors]
    K3 --> K4[storeComplexityResults]
    K4 --> K5[storeCfgResults]
    K5 --> K6[delegateToBuildFunctions]
    K6 --> L[finalize]
Loading

Comments Outside Diff (1)

  1. src/ast-analysis/engine.js, line 234-248 (link)

    cfg === null sentinel not respected when storing results

    setupVisitors decides whether to create a CFG visitor using d.cfg !== null, treating null as a deliberate "skip this function" sentinel:

    const needsWasmCfg = defs.some(
      (d) => (d.kind === 'function' || d.kind === 'method') &&
        d.line &&
        d.cfg !== null &&                 // ← null = skip sentinel
        !Array.isArray(d.cfg?.blocks),
    );

    But storeCfgResults stores results using the looser check !def.cfg?.blocks?.length, which is also true when def.cfg === null:

    for (const def of defs) {
      if (
        (def.kind === 'function' || def.kind === 'method') &&
        def.line &&
        !def.cfg?.blocks?.length          // ← passes when cfg is null
      ) {

    In a file where one function has cfg === null (skip) and another has cfg undefined (needs computation), the visitor is created for the file and will produce results for all functions — including the one with the null sentinel. storeCfgResults then overrides the sentinel by writing blocks back to that definition. src/features/cfg.js (lines 185-186) correctly preserves the sentinel using the same d.cfg !== null guard.

    To align the storage logic with the skip convention:

    if (
      (def.kind === 'function' || def.kind === 'method') &&
      def.line &&
      def.cfg !== null &&
      !def.cfg?.blocks?.length
    ) {

Last reviewed commit: b939341

if (needsWasmTrees) {
try {
const { ensureWasmTrees } = await getParserModule();
await ensureWasmTrees(fileSymbols);
Copy link
Contributor

Choose a reason for hiding this comment

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

P0 Missing rootDir argument dropped during extraction

ensureWasmTrees requires two arguments — fileSymbols and rootDir — as confirmed by its signature in src/domain/parser.js:

export async function ensureWasmTrees(fileSymbols, rootDir) {
  // ...
  const absPath = path.join(rootDir, relPath); // ← rootDir used here

When rootDir is undefined, path.join(undefined, relPath) will throw a TypeError (Node ≥ 20 is strict about this), meaning WASM pre-parsing silently fails on the native engine path. This directly reverts the fix for bug #468 ("incremental rebuilds on the native engine silently lose these analyses for changed files").

The ensureWasmTreesIfNeeded helper needs to accept rootDir and pass it through:

Suggested change
await ensureWasmTrees(fileSymbols);
await ensureWasmTrees(fileSymbols, rootDir);

The helper's signature should also be updated:

async function ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir) {

And the call in runAnalyses updated:

await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);

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 8ce02f1 — added rootDir parameter to ensureWasmTreesIfNeeded and passed it through to ensureWasmTrees. This restores the fix from #468.

Comment on lines 278 to 282
}
}
return reverseDeps;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Scoped build log silently dropped for zero-reverse-dep case

In the original detectChanges, the info(...) rebuild log was always emitted for scoped builds:

// original — unconditional
info(`Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`);

The refactoring moved the log inside if (reverseDeps.size > 0), so it is now silently skipped when there are no reverse-dependency files. A scoped rebuild with only direct changes (the common case) produces no log output at all, making builds harder to diagnose.

Suggested change
}
}
return reverseDeps;
}
if (reverseDeps.size > 0) {
info(
`Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`,
);
} else {
info(
`Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, 0 reverse-deps`,
);
}

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 f6488f1 — moved the info() log outside the reverseDeps.size > 0 check so scoped builds always emit a log line.

Comment on lines +286 to +330
if (changePaths.length > 0 || ctx.removed.length > 0) {
purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
}

if (reverseDeps.size > 0) {
const deleteOutgoingEdgesForFile = db.prepare(
'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
);
for (const relPath of reverseDeps) {
deleteOutgoingEdgesForFile.run(relPath);
}
for (const relPath of reverseDeps) {
const absPath = path.join(rootDir, relPath);
ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
}
return;
}
}

const increResult =
incremental && !forceFullRebuild
? getChangedFiles(db, allFiles, rootDir)
: { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
// ── Scoped build path ───────────────────────────────────────────────────

ctx.removed = increResult.removed;
ctx.isFullBuild = increResult.isFullBuild;
ctx.parseChanges = increResult.changed.filter((c) => !c.metadataOnly);
ctx.metadataUpdates = increResult.changed.filter((c) => c.metadataOnly);
function handleScopedBuild(ctx) {
const { db, rootDir, opts } = ctx;

// Early exit: no changes detected
if (!ctx.isFullBuild && ctx.parseChanges.length === 0 && ctx.removed.length === 0) {
const ranAnalysis = await runPendingAnalysis(ctx);
if (ranAnalysis) {
closeDb(db);
writeJournalHeader(rootDir, Date.now());
ctx.earlyExit = true;
return;
}
let hasEmbeddings = false;
try {
db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
hasEmbeddings = true;
} catch {
/* table doesn't exist */
}
ctx.hasEmbeddings = hasEmbeddings;

healMetadata(ctx);
info('No changes detected. Graph is up to date.');
closeDb(db);
writeJournalHeader(rootDir, Date.now());
ctx.earlyExit = true;
return;
const changePaths = ctx.parseChanges.map(
(item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
);

let reverseDeps = new Set();
if (!opts.noReverseDeps) {
const changedRelPaths = new Set([...changePaths, ...ctx.removed]);
reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
}

// Purge changed + removed files, then add reverse-deps
purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);

Copy link
Contributor

Choose a reason for hiding this comment

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

P2 hasEmbeddings detection duplicated across three extracted helpers

handleScopedBuild, handleFullBuild, and handleIncrementalBuild all contain an identical try/catch block that detects whether the embeddings table exists and sets ctx.hasEmbeddings. The original code had this logic once. Consider extracting it:

function detectHasEmbeddings(db) {
  try {
    db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
    return true;
  } catch {
    return false;
  }
}

Then call ctx.hasEmbeddings = detectHasEmbeddings(ctx.db); once before dispatching to the three helpers, instead of repeating the block in each.

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 in f6488f1 — extracted a shared detectHasEmbeddings(db) helper and replaced the duplicated try-catch blocks in all three build handlers.

Without rootDir, path.join(undefined, relPath) throws a TypeError,
silently breaking WASM pre-parsing on the native engine path.
…dings helper

The scoped rebuild info log was only emitted when reverse-deps existed,
hiding output for the common zero-reverse-dep case. Also deduplicate
the hasEmbeddings try-catch across three build handlers into a shared
detectHasEmbeddings helper.
@carlos-alm
Copy link
Contributor Author

@greptileai

Base automatically changed from refactor/titan-extractors to main March 17, 2026 11:19
# Conflicts:
#	src/db/migrations.js
#	src/features/shared/find-nodes.js

Impact: 3 functions changed, 7 affected
@carlos-alm
Copy link
Contributor Author

Resolved merge conflicts with main — same pattern as #490 (hasTable guards in migrations.js, findNodes validation in find-nodes.js). All 1883 tests pass.

@greptileai

@carlos-alm carlos-alm merged commit ded1982 into main Mar 17, 2026
14 checks passed
@carlos-alm carlos-alm deleted the refactor/titan-ast-builder branch March 17, 2026 11:57
@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