Files
gbrain/src/commands/code-callees.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

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);
}
}