mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +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>
159 lines
5.8 KiB
TypeScript
159 lines
5.8 KiB
TypeScript
/**
|
|
* gbrain code-callees <symbol>
|
|
*
|
|
* v0.20.0 Cathedral II Layer 10 (C5) — "what does this symbol call?"
|
|
* Forward view of the A1 call graph. Matches `from_symbol_qualified`
|
|
* in both code_edges_chunk + code_edges_symbol.
|
|
*
|
|
* Source resolution: honors the full chain (incl. the `.gbrain-source` pin)
|
|
* via `resolveScopedSourceOrThrow` when --source/--all-sources are omitted.
|
|
* See code-callers.ts for the full rationale. Same behavior here. JSON
|
|
* envelope carries `source_id` + `scope`.
|
|
*
|
|
* Output: same JSON-on-non-TTY convention as code-callers / code-def /
|
|
* code-refs.
|
|
*/
|
|
|
|
import type { BrainEngine } from '../core/engine.ts';
|
|
import { errorFor, serializeError } from '../core/errors.ts';
|
|
import { resolveScopedSourceOrThrow, SourceResolutionError } from '../core/sources-ops.ts';
|
|
import { formatSoleNonDefaultNudge } from '../core/source-resolver.ts';
|
|
import { resolveCodeReadiness, readinessHint } from '../core/code-graph-readiness.ts';
|
|
|
|
/** A bad/invalid `.gbrain-source` pin or GBRAIN_SOURCE value surfaces from
|
|
* `resolveSourceWithTier`'s `assertSourceExists` as a plain Error with one of
|
|
* these message prefixes. Mirrors dream.ts:isResolverUserError. */
|
|
function isResolverUserError(e: unknown): boolean {
|
|
if (!(e instanceof Error)) return false;
|
|
const m = e.message;
|
|
return (m.startsWith('Source "') && m.includes(' not found.'))
|
|
|| m.startsWith('Invalid --source value')
|
|
|| m.startsWith('Invalid GBRAIN_SOURCE value');
|
|
}
|
|
|
|
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 runCodeCallees(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_callees_requires_symbol',
|
|
message: 'code-callees requires a symbol name',
|
|
hint: 'gbrain code-callees <symbol> [--source S | --all-sources] [--limit N] [--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') || '100', 10);
|
|
const allSources = args.includes('--all-sources');
|
|
let sourceId = parseFlag(args, '--source');
|
|
|
|
// Full source-resolution chain (honors .gbrain-source pin, env, local_path,
|
|
// brain_default, sole_non_default). Matches code-callers behavior.
|
|
if (!allSources && !sourceId) {
|
|
try {
|
|
const resolved = await resolveScopedSourceOrThrow(engine);
|
|
sourceId = resolved.source_id;
|
|
if (resolved.tier === 'sole_non_default') {
|
|
const nudge = formatSoleNonDefaultNudge(resolved.source_id);
|
|
if (nudge) console.error(nudge);
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof SourceResolutionError) {
|
|
const env = errorFor({
|
|
class: 'UsageError',
|
|
code: e.code,
|
|
message: e.message,
|
|
hint: 'pass --source <id> for one source, or --all-sources to search every source',
|
|
}).envelope;
|
|
if (shouldEmitJson(args)) {
|
|
console.log(JSON.stringify({ error: env }));
|
|
} else {
|
|
console.error(e.message);
|
|
}
|
|
process.exit(2);
|
|
}
|
|
if (isResolverUserError(e)) {
|
|
const env = errorFor({
|
|
class: 'UsageError',
|
|
code: 'invalid_source_pin',
|
|
message: (e as Error).message,
|
|
hint: 'fix the .gbrain-source pin / GBRAIN_SOURCE value, or pass --source <id> / --all-sources',
|
|
}).envelope;
|
|
if (shouldEmitJson(args)) {
|
|
console.log(JSON.stringify({ error: env }));
|
|
} else {
|
|
console.error((e as Error).message);
|
|
}
|
|
process.exit(2);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const edges = await engine.getCalleesOf(sym, {
|
|
limit,
|
|
allSources,
|
|
sourceId: sourceId ?? undefined,
|
|
});
|
|
|
|
const scope = allSources ? 'all' : 'single';
|
|
const envelopeSourceId = allSources ? null : (sourceId ?? null);
|
|
|
|
// Call-graph readiness ('edge' grain): distinguishes "graph not built / still
|
|
// indexing" from "genuinely no callees" when count === 0.
|
|
const readiness = await resolveCodeReadiness(engine, {
|
|
kind: 'edge', count: edges.length, sourceId: sourceId ?? undefined, allSources,
|
|
});
|
|
|
|
if (shouldEmitJson(args)) {
|
|
const out: Record<string, unknown> = {
|
|
symbol: sym, source_id: envelopeSourceId, scope, count: edges.length,
|
|
status: readiness.status, ready: readiness.ready, callees: edges,
|
|
};
|
|
if (edges.length === 0 && !allSources && sourceId) {
|
|
out.hint = `No callees in source '${sourceId}'. Try --all-sources to search every source.`;
|
|
}
|
|
console.log(JSON.stringify(out, null, 2));
|
|
} else if (edges.length === 0) {
|
|
if (!allSources && sourceId) {
|
|
console.log(`No callees found for "${sym}" in source '${sourceId}'. Try --all-sources to search every source.`);
|
|
} else {
|
|
console.log(`No callees found for "${sym}".`);
|
|
}
|
|
const hint = readinessHint(readiness);
|
|
if (hint) console.log(hint);
|
|
} else {
|
|
console.log(`${edges.length} callee(s) for "${sym}":`);
|
|
for (const e of edges) {
|
|
const res = e.resolved ? 'resolved' : 'unresolved';
|
|
console.log(` ${e.from_symbol_qualified} → ${e.to_symbol_qualified} [${res}]`);
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
const env = serializeError(e);
|
|
if (shouldEmitJson(args)) {
|
|
console.log(JSON.stringify({ error: env }));
|
|
} else {
|
|
console.error(`code-callees failed: ${env.message}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|