Files
gbrain/src/commands/code-refs.ts
T
1036f8f752 v0.42.14.0 fix(zero-config): code-* readiness signal + init embedding-key validation + lock self-heal (#1780) (#1804)
* feat(code-intel): readiness signal on code-def/refs/callers/callees (#1780 Gap 1)

New src/core/code-graph-readiness.ts: resolveCodeReadiness() returns a typed
status (not_built | indexing | ready | unknown) + ready boolean so callers can
tell "graph not built / still indexing" apart from "genuinely no match" when
count===0. EXISTS-based (cheap), chunk-grain, resolver-version-matching pending
predicate, fail-open. Wired into the 4 CLI envelopes (+ human hint) and the 4
MCP op handlers. def/refs are 2-state brain-wide; callers/callees 3-state scoped.

* feat(db-lock): automatic same-host dead-pid cycle-lock takeover (#1780 Gap 3)

tryAcquireDbLock now reclaims a held, not-TTL-expired lock when the same-host
holder is provably dead (process.kill ESRCH) past a 60s grace, via guarded
DELETE + one normal-upsert retry returning the normal handle. New shared
injectable classifyHolderLiveness/isHolderDeadLocally (EPERM treated as ALIVE
— never steals a live lock). runBreakLock's safe path consumes the shared
predicate, fixing its prior EPERM-as-dead bug. Cross-host stays TTL-only.

* feat(init): validate the embedding key at gbrain init (#1780 Gap 2)

New src/core/init-embed-check.ts: config-only diagnoseEmbedding (missing key,
all providers) + best-effort 1-token live test-embed (invalid/expired key, 5s
timeout, never blocks). Loud warning to stderr, init still exits 0; skipped by
--no-embedding / --skip-embed-check / GBRAIN_INIT_SKIP_EMBED_CHECK=1. Builds the
effective env (process.env + file-plane keys + --key) via buildGatewayConfig,
extracted to src/core/ai/build-gateway-config.ts (cli.ts re-exports) so the
check sees the same keys + provider base URLs as runtime. embedding_check added
to --json.

* chore: bump version and changelog (v0.42.14.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document the #1780 zero-config gaps for v0.42.14.0

CLAUDE.md Key Files: add src/core/code-graph-readiness.ts, init-embed-check.ts,
ai/build-gateway-config.ts, and the db-lock auto-takeover + code-* readiness
field behaviors. Regenerate llms-full.txt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:43:11 -07:00

145 lines
4.8 KiB
TypeScript

/**
* gbrain code-refs <symbol>
*
* v0.19.0 Layer 7 — find all usage sites of a named symbol across the
* brain's code pages. The DX "magical moment" for v0.19.0: an agent
* asks "what uses BrainEngine" and gets back a JSON array of
* {file, line, snippet} tuples in one CLI call.
*
* Implementation: bypasses the standard searchKeyword path (which uses
* DISTINCT ON (slug) to collapse to one result per page — wrong for
* code-refs where a single file typically has many usage sites). Uses
* a direct ILIKE scan over content_chunks + JOIN pages, returning every
* matching chunk.
*
* Scope: simple substring match. Word-boundary precision is a follow-up
* (would require either tsvector or regex). For v0.19.0 the heuristic
* is good enough: symbol names are distinctive by design, and noisy
* matches (e.g. 'foo' matching 'food') are rare in well-written code.
*/
import type { BrainEngine } from '../core/engine.ts';
import { errorFor, serializeError } from '../core/errors.ts';
import { resolveCodeReadiness, readinessHint } from '../core/code-graph-readiness.ts';
export interface CodeRefResult {
slug: string;
file: string | null;
language: string | null;
symbol_name: string | null;
symbol_type: string | null;
start_line: number | null;
end_line: number | null;
snippet: string;
}
export async function findCodeRefs(
engine: BrainEngine,
symbol: string,
opts: { limit?: number; language?: string } = {},
): Promise<CodeRefResult[]> {
const limit = opts.limit ?? 50;
const params: unknown[] = [`%${symbol}%`];
let whereLang = '';
if (opts.language) {
params.push(opts.language);
whereLang = `AND cc.language = $${params.length}`;
}
params.push(limit);
const rows = await engine.executeRaw<{
slug: string; file: string | null; language: string | null;
symbol_name: string | null; symbol_type: string | null;
start_line: number | null; end_line: number | null;
chunk_text: string;
}>(
`SELECT p.slug, (p.frontmatter->>'file') AS file, cc.language,
cc.symbol_name, cc.symbol_type, cc.start_line, cc.end_line,
cc.chunk_text
FROM content_chunks cc
JOIN pages p ON p.id = cc.page_id
WHERE p.page_kind = 'code'
AND cc.chunk_text ILIKE $1
${whereLang}
ORDER BY p.slug, cc.start_line NULLS LAST
LIMIT $${params.length}`,
params,
);
return rows.map((r) => ({
slug: r.slug,
file: r.file,
language: r.language,
symbol_name: r.symbol_name,
symbol_type: r.symbol_type,
start_line: r.start_line,
end_line: r.end_line,
snippet: r.chunk_text.slice(0, 500),
}));
}
function parseFlag(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
}
function shouldEmitJson(args: string[]): boolean {
if (args.includes('--json')) return true;
if (args.includes('--no-json')) return false;
return !process.stdout.isTTY;
}
export async function runCodeRefs(engine: BrainEngine, args: string[]): Promise<void> {
const positional = args.filter((a) => !a.startsWith('--'));
const sym = positional[0];
if (!sym) {
const err = errorFor({
class: 'UsageError',
code: 'code_refs_requires_symbol',
message: 'code-refs requires a symbol name',
hint: 'gbrain code-refs <symbol> [--lang <language>] [--json]',
});
if (shouldEmitJson(args)) {
console.log(JSON.stringify({ error: err.envelope }));
} else {
console.error(err.message);
}
process.exit(2);
}
const limit = parseInt(parseFlag(args, '--limit') || '50', 10);
const language = parseFlag(args, '--lang');
try {
const results = await findCodeRefs(engine, sym, { limit, language });
// code-refs is brain-wide (not source-scoped); readiness is 'symbol' grain.
const readiness = await resolveCodeReadiness(engine, { kind: 'symbol', count: results.length });
if (shouldEmitJson(args)) {
console.log(JSON.stringify({
symbol: sym,
count: results.length,
status: readiness.status,
ready: readiness.ready,
results,
}, null, 2));
} else {
if (results.length === 0) {
console.log(`No references found for "${sym}"`);
const hint = readinessHint(readiness);
if (hint) console.log(hint);
} else {
console.log(`Found ${results.length} reference(s) to "${sym}":`);
for (const r of results) {
const loc = r.start_line != null ? `:${r.start_line}` : '';
const sig = r.symbol_name ? ` in ${r.symbol_name}` : '';
console.log(` ${r.file || r.slug}${loc}${sig}`);
}
}
}
} catch (e: unknown) {
const env = serializeError(e);
if (shouldEmitJson(args)) {
console.log(JSON.stringify({ error: env }));
} else {
console.error(`code-refs failed: ${env.message}`);
}
process.exit(1);
}
}