mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 19:01:39 +00:00
* 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>
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
/**
|
|
* gbrain code-def <symbol>
|
|
*
|
|
* v0.19.0 Layer 7 — look up the definition site(s) of a named symbol
|
|
* (function, class, type, interface, enum) across every code page the
|
|
* brain has indexed.
|
|
*
|
|
* Output:
|
|
* - TTY or --pretty: human-readable list of matches, one per line.
|
|
* - non-TTY or --json: JSON array the agent consumes.
|
|
*
|
|
* Uses the content_chunks.symbol_name column (v0.19.0 migration v26).
|
|
* No tree-sitter re-parsing needed — the metadata is already there.
|
|
*/
|
|
|
|
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 CodeDefResult {
|
|
slug: string;
|
|
file: string | null;
|
|
language: string | null;
|
|
symbol_type: string | null;
|
|
start_line: number | null;
|
|
end_line: number | null;
|
|
snippet: string;
|
|
}
|
|
|
|
export async function findCodeDef(
|
|
engine: BrainEngine,
|
|
symbol: string,
|
|
opts: { limit?: number; language?: string } = {},
|
|
): Promise<CodeDefResult[]> {
|
|
const limit = opts.limit ?? 20;
|
|
// v0.41 D2: SQL DDL targets (table/view/index/procedure/schema/database/
|
|
// trigger) are first-class definitions in the SQL sense. The chunker's
|
|
// normalizeSymbolType maps create_table → 'table' etc, so adding the SQL
|
|
// kinds here is what makes `gbrain code-def users` work against SQL.
|
|
const DEF_TYPES = [
|
|
'function', 'class', 'interface', 'type', 'enum', 'struct', 'trait', 'module', 'contract',
|
|
'table', 'view', 'index', 'procedure', 'schema', 'database', 'trigger',
|
|
];
|
|
const params: unknown[] = [symbol, limit];
|
|
let whereLang = '';
|
|
if (opts.language) {
|
|
params.splice(1, 0, opts.language);
|
|
whereLang = 'AND cc.language = $2';
|
|
}
|
|
// Deterministic ordering: exact type matches first (functions before
|
|
// export_statement wrappers), then page slug, then line number.
|
|
const rows = await engine.executeRaw<{
|
|
slug: string; file: string | null; language: 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_type,
|
|
cc.start_line, cc.end_line, cc.chunk_text
|
|
FROM content_chunks cc
|
|
JOIN pages p ON p.id = cc.page_id
|
|
WHERE cc.symbol_name = $1
|
|
${whereLang}
|
|
AND p.page_kind = 'code'
|
|
AND cc.symbol_type IN ('${DEF_TYPES.join("','")}', 'export statement')
|
|
ORDER BY
|
|
CASE cc.symbol_type
|
|
WHEN 'function' THEN 1 WHEN 'class' THEN 2 WHEN 'interface' THEN 3
|
|
WHEN 'type' THEN 4 WHEN 'enum' THEN 5 WHEN 'struct' THEN 6
|
|
ELSE 7
|
|
END,
|
|
p.slug, cc.start_line
|
|
LIMIT $${params.length}`,
|
|
params,
|
|
);
|
|
return rows.map((r) => ({
|
|
slug: r.slug,
|
|
file: r.file,
|
|
language: r.language,
|
|
symbol_type: r.symbol_type,
|
|
start_line: r.start_line,
|
|
end_line: r.end_line,
|
|
// First 500 chars of chunk — enough for a preview without flooding output.
|
|
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;
|
|
// Auto-detect: non-TTY stdout means an agent is piping us — default to JSON.
|
|
return !process.stdout.isTTY;
|
|
}
|
|
|
|
export async function runCodeDef(engine: BrainEngine, args: string[]): Promise<void> {
|
|
const symbol = args.find((a) => !a.startsWith('--') && args.indexOf(a) > 0);
|
|
// args[0] is the symbol when invoked as `gbrain code-def <symbol>`
|
|
const positional = args.filter((a) => !a.startsWith('--'));
|
|
const sym = positional[0];
|
|
if (!sym) {
|
|
const err = errorFor({
|
|
class: 'UsageError',
|
|
code: 'code_def_requires_symbol',
|
|
message: 'code-def requires a symbol name',
|
|
hint: 'gbrain code-def <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') || '20', 10);
|
|
const language = parseFlag(args, '--lang');
|
|
try {
|
|
const results = await findCodeDef(engine, sym, { limit, language });
|
|
// code-def 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 definitions found for "${sym}"`);
|
|
const hint = readinessHint(readiness);
|
|
if (hint) console.log(hint);
|
|
} else {
|
|
console.log(`Found ${results.length} definition(s) for "${sym}":`);
|
|
for (const r of results) {
|
|
const loc = r.start_line != null ? `:${r.start_line}` : '';
|
|
console.log(` ${r.file || r.slug}${loc} (${r.symbol_type})`);
|
|
}
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
const env = serializeError(e);
|
|
if (shouldEmitJson(args)) {
|
|
console.log(JSON.stringify({ error: env }));
|
|
} else {
|
|
console.error(`code-def failed: ${env.message}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|