Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixed
- **File watcher no longer marks edited files as fresh when another process holds the index lock.** When a second writer (concurrent `codegraph index`, a git hook, another MCP daemon) held `.codegraph/codegraph.lock`, `CodeGraph.sync()` returned a zero-shape no-op instead of throwing. The file watcher took that as a successful sync and cleared `pendingFiles` — so the per-file staleness signal MCP tools surface to agents (issue #403) dropped immediately, even though the edit was never indexed. `CodeGraph.watch()` now converts that no-op into a typed `LockUnavailableError` thrown into the watcher; the existing retry path preserves `pendingFiles` and reschedules until the lock becomes available. The error is logged at debug only (no `onSyncError` callback) so a long-running external indexer doesn't spam stderr every debounce cycle. Closes #449.
- **TS/JS top-level initializer calls and inline-object-method calls are no longer dropped.** Calls inside a top-level variable initializer (`const token = getTokenMp()`) and inside methods of an inline object literal (`{ methods: { save() { getTokenMp() } } }`) were never walked by the variable / method-definition extractors, so `getTokenMp` showed up nowhere in `codegraph_callers`. The variable extractor now walks any non-object initializer value for calls; the method-definition extractor still avoids creating synthetic nodes for inline-object methods (the noise reason is unchanged) but now walks their bodies so the calls inside aren't lost. Surfaces in plain `.ts`/`.js` files (top-level `const x = foo()`) and in Vue SFCs (`<script setup>` initializers + classic Options API `methods: {...}` / `setup()`), where the bug was originally reported. Closes #425.
- **Watch sync no longer aborts with `FOREIGN KEY constraint failed`.** PR #62 plugged this FK violation at the extraction layer (empty-named nodes whose containment edges had no target), but the same violation kept reappearing on v0.9.5 during the daemon's *watch sync* — not on initial index. Once an agent's daemon had been running long enough to accumulate edits, a resolver lookup that crossed a framework-specific cache could hand back a node whose row had been removed by a recent file rewrite, and the FK check then aborted the entire resolution batch, leaving the user's daemon log filling with `Watch sync failed { error: 'FOREIGN KEY constraint failed' }`. `QueryBuilder.insertEdges` now validates every batch's endpoints against the `nodes` table directly (one fresh `SELECT id IN (...)` per batch, no cache) and silently skips edges with missing source or target — so a stale lookup result drops one edge instead of aborting the whole sync. Surfaces as a fresh `codegraph init`/`index` cycle now surviving its first watch-sync cycle without the FK error, and the daemon recovering naturally instead of compounding into further failures. Closes #455.
- **Hermes Agent: `codegraph install --target hermes` no longer corrupts `~/.hermes/config.yaml`.** Hermes serializes its config with PyYAML's default block style, which writes list items at the *same* indent as the parent mapping key (`cli:` and `- hermes-cli` both at column 2). The previous line-based YAML patcher mistook that first ` - hermes-cli` for the next sibling key, truncated the `cli:` block, and then spliced `- mcp-codegraph` at indent 4 *before* the existing items — leaving subsequent entries (`- browser`, `- clarify`, …) and even other platforms (`telegram:`, `discord:`) appearing at the `platform_toolsets:` level, which is no longer parseable YAML. The installer now recognizes the same-indent list style, finds the real end of the block at the next sibling key, and appends `- mcp-codegraph` at whatever indent the existing items already use. Re-installing on an already-corrupted file (or a 4-space-nested config that worked before) still produces a clean, parseable result. Closes #456.
- **NestJS: `RouterModule.register([...])` route prefixes now propagate to controller routes.** Previously a controller declared inside a module wired through NestJS's `RouterModule` (a common pattern for modular apps with nested route prefixes) was indexed with its raw `@Controller(...) + @Get(...)` path — so `UsersController` under `RouterModule.register([{ path: 'admin', module: AdminModule, children: [{ path: 'users', module: UsersModule }] }])` showed up as `GET /` instead of `GET /admin/users`. The new cross-file pass walks every `*.module.{ts,js}` for `RouterModule.register/forRoot/forChild([...])` (recursive `children`) and `@Module({ controllers: [...] })`, then prepends the correct prefix to each affected route — including non-empty `@Controller` paths and method-level params (`/admin/users/:id`). The route node's `id` is preserved across the update so existing route→handler edges stay intact, and the pass is idempotent so incremental sync recovers when `app.module.ts` itself is edited. Closes #459.
Expand Down
61 changes: 61 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,20 @@ export const authMachine = createMachine({
expect(varNode).toBeDefined();
expect(varNode?.isExported).toBe(true);
});

it('should extract calls from a top-level variable initializer (issue #425)', () => {
const code = `
import { getTokenMp } from './api/upload';

const token = getTokenMp();
`;
const result = extractFromSource('app.ts', code);

const call = result.unresolvedReferences.find(
(ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
);
expect(call).toBeDefined();
});
});

describe('File Node Extraction', () => {
Expand Down Expand Up @@ -3568,6 +3582,53 @@ function increment(): void {
}
});

it('should extract calls from top-level <script setup> initializers', () => {
const code = `<template>
<div>{{ token }}</div>
</template>

<script setup lang="ts">
import { getTokenMp } from './api/upload';

const token = getTokenMp();
</script>
`;
const result = extractFromSource('Issue425Setup.vue', code);

const call = result.unresolvedReferences.find(
(ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
);
expect(call).toBeDefined();
});

it('should extract calls from Vue Options API object methods', () => {
const code = `<template>
<button @click="save">Save</button>
</template>

<script>
import { getTokenMp } from './api/upload';

export default {
methods: {
save() {
return getTokenMp();
}
},
setup() {
return getTokenMp();
}
}
</script>
`;
const result = extractFromSource('Issue425Options.vue', code);

const calls = result.unresolvedReferences.filter(
(ref) => ref.referenceKind === 'calls' && ref.referenceName === 'getTokenMp'
);
expect(calls).toHaveLength(2);
});

it('should extract from both <script> and <script setup> blocks', () => {
const code = `<template>
<div>{{ msg }}</div>
Expand Down
11 changes: 11 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,11 @@ export class TreeSitterExtractor {
// in inline objects). These are ephemeral and create noise (e.g., Svelte context
// objects: `ctx.set({ get view() { ... } })`).
if (node.parent?.type === 'object' || node.parent?.type === 'object_expression') {
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (body) {
this.visitFunctionBody(body, '');
}
return;
}
// Not inside a class-like node and no receiver type, treat as function
Expand Down Expand Up @@ -1080,6 +1085,12 @@ export class TreeSitterExtractor {
this.extractVariableTypeAnnotation(child, varNode.id);
}

if (valueNode &&
valueNode.type !== 'object' &&
valueNode.type !== 'object_expression') {
this.visitFunctionBody(valueNode, '');
}

// Exported const object-of-functions: `export const actions =
// { default: async () => {} }` (SvelteKit form actions / handler maps
// / route tables). Extract each function-valued property as a function
Expand Down