Files
gstack/lib/code-intelligence/picker.ts
T
3aba218303 feat(code-intelligence): provider contract Phase 1 — GBrain, Sourcebot, Graphify behind one ask-once offer
Open a large repo (1,000+ tracked files) and gstack can offer code
intelligence ONCE, with the trade-offs stated: GBrain (semantic memory +
code, sends content to YOUR gbrain DB, per-repo consent), Sourcebot
(self-hosted whole-repo search, local on localhost), Graphify (local
tree-sitter graph, nothing leaves the machine, user-installed), or No
indexing — a decline persists machine-wide so no skill ever asks again.
Small repos never see the question; grep stays the always-working default
and provider-OFF degrades silently (PROVIDER_UNAVAILABLE -> file-only).

Ported: lib/code-intelligence/ (contract + 3 verified adapters + picker +
selection + suggest, MIT headers), the gstack-code-intelligence CLI
(suggest/select/consent/index/search/status), 31 offline tests (fake CLI
shims + injected fetch), and the provider-contract design doc. Verified
live on this repo: suggest fires at 1,233 files with real availability
detail per provider.

Hardened per review: the per-remote trust store is the SINGLE consent
authority — a gstack-gbrain-repo-policy deny tier vetoes any recorded
code-intelligence consent (fail-closed on an unreadable store, pinned by
three tests); both send-capable adapters are registered as fail-closed
MODULE_SINKS in the egress tripwire so a refactor can't drop their
receipts; and local-compute vs remote-send consents are never bundled.
setup-gbrain gains the provider-choice Step 0. The fork's Phases 2-4
glue-collapse is explicitly NOT ported.

Ported from time-attack/gstack (GStack 2); consent unification ours.

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:15:03 -07:00

101 lines
4.1 KiB
TypeScript

/**
* Picker — constructs the code-intelligence provider the user selected, and
* offers the recommendation order (GBrain first) for the selection UX.
*
* `resolveSelectedProvider()` reads the persisted selection and constructs that
* provider, or returns null when nothing is selected — the provider-OFF path,
* where callers degrade to grep / the file-only decision store. Availability is
* proven at call time: a selected provider whose tool/server is absent throws
* PROVIDER_UNAVAILABLE from its ops, which callers catch and degrade on. The
* `detectAvailable()` probe drives the `options`/`status` display.
*
* GBrain is recommended first. Graphify is NEVER auto-installed — it appears in
* the options only once its CLI is present (a user install).
*/
import { localEngineStatus } from "../gbrain-local-status";
import { GbrainProvider } from "./gbrain-adapter";
import { GraphifyProvider, graphifyInstalled, type GraphifyOptions } from "./graphify-adapter";
import { SourcebotProvider, type SourcebotOptions } from "./sourcebot-adapter";
import { readSelection, getRoot } from "./selection";
import type { CodeProvider, CodeProviderId } from "./contract";
/** Recommendation order — GBrain first. */
export const RECOMMENDED_ORDER: readonly CodeProviderId[] = ["gbrain", "sourcebot", "graphify"];
export interface PickerOptions {
env?: NodeJS.ProcessEnv;
graphify?: GraphifyOptions;
sourcebot?: SourcebotOptions;
}
/** Construct a provider by id (no availability check — ops degrade at call time). */
export function providerById(id: CodeProviderId, opts: PickerOptions = {}): CodeProvider {
switch (id) {
case "gbrain":
return new GbrainProvider();
case "graphify": {
// Default the graph root to the repo Graphify last indexed, so `search`
// reads the same graph `index` built (not whatever cwd happens to be).
const root = opts.graphify?.root ?? getRoot("graphify", opts.env);
return new GraphifyProvider({ env: opts.env, ...opts.graphify, ...(root ? { root } : {}) });
}
case "sourcebot":
return new SourcebotProvider({ env: opts.env, ...opts.sourcebot });
}
}
/**
* The provider the user selected, constructed, or null when none is selected.
* Null is the provider-OFF path: callers MUST degrade to grep / file-only.
*/
export function resolveSelectedProvider(opts: PickerOptions = {}): CodeProvider | null {
const { provider } = readSelection(opts.env);
return provider ? providerById(provider, opts) : null;
}
export interface Availability {
id: CodeProviderId;
available: boolean;
detail: string;
}
/**
* Probe which providers are usable right now, in recommendation order. Used by
* the `options`/`status` display. GBrain via the real localEngineStatus();
* Graphify via its CLI status; Sourcebot via an HTTP liveness probe.
*/
export async function detectAvailable(opts: PickerOptions = {}): Promise<Availability[]> {
const gbrainStatus = localEngineStatus({ env: opts.env });
const gbrainOk = gbrainStatus === "ok" || gbrainStatus === "timeout";
// Available = the CLI is installed and selectable (NOT "a graph already exists
// here"). A freshly installed Graphify with no graph yet is still available.
const graphifyOk = graphifyInstalled(opts.env);
let graphifyDetail = "graphify CLI not installed (pip install graphifyy, Python >= 3.10)";
if (graphifyOk) {
try {
const s = await new GraphifyProvider({ env: opts.env, ...opts.graphify }).status();
graphifyDetail = s.state === "ready" ? "installed; graph built in this repo" : "installed; run `index` to build a graph";
} catch {
graphifyDetail = "installed";
}
}
let sourcebotOk = false;
let sourcebotDetail = "server unreachable";
try {
const s = await new SourcebotProvider({ env: opts.env, ...opts.sourcebot }).status();
sourcebotOk = s.state === "ready";
sourcebotDetail = s.detail ?? "";
} catch {
sourcebotOk = false;
}
return [
{ id: "gbrain", available: gbrainOk, detail: `gbrain engine: ${gbrainStatus}` },
{ id: "sourcebot", available: sourcebotOk, detail: sourcebotDetail },
{ id: "graphify", available: graphifyOk, detail: graphifyDetail },
];
}