mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-11 07:29:00 +02:00
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>
This commit is contained in:
co-authored by
Sina Matian
Claude Fable 5
parent
53deeeb116
commit
3aba218303
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* suggest — should the session-start indexing offer be made for this repo?
|
||||
*
|
||||
* The offer fires at most once per machine: never when a provider is already
|
||||
* selected, never after an explicit decline (`select none`), and never for
|
||||
* small repos where grep is already fast. Detection is cheap and local
|
||||
* (`git ls-files` count); a non-repo directory never triggers the offer.
|
||||
*/
|
||||
|
||||
import { spawnSync } from "child_process";
|
||||
import { resolve } from "path";
|
||||
import { readSelection } from "./selection";
|
||||
|
||||
// ponytail: single tracked-file-count knob for "large"; add a LOC signal if it misfires
|
||||
/** Tracked-file count at which indexing starts paying for itself. */
|
||||
export const LARGE_REPO_FILE_THRESHOLD = 1000;
|
||||
|
||||
export type SuggestReason =
|
||||
| "provider-selected"
|
||||
| "declined"
|
||||
| "not-a-repo"
|
||||
| "small-repo"
|
||||
| "large-repo";
|
||||
|
||||
export interface Suggestion {
|
||||
offer: boolean;
|
||||
reason: SuggestReason;
|
||||
fileCount: number | null;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
/** Count of git-tracked files, or null when the path is not a git repo. */
|
||||
export function trackedFileCount(repoPath: string): number | null {
|
||||
const result = spawnSync("git", ["-C", resolve(repoPath), "ls-files"], {
|
||||
encoding: "utf-8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
if (result.status !== 0 || typeof result.stdout !== "string") return null;
|
||||
const out = result.stdout.trim();
|
||||
return out ? out.split("\n").length : 0;
|
||||
}
|
||||
|
||||
export function shouldOfferIndexing(
|
||||
repoPath: string,
|
||||
opts: { env?: NodeJS.ProcessEnv; threshold?: number } = {},
|
||||
): Suggestion {
|
||||
const threshold = opts.threshold ?? LARGE_REPO_FILE_THRESHOLD;
|
||||
const selection = readSelection(opts.env);
|
||||
if (selection.provider) return { offer: false, reason: "provider-selected", fileCount: null, threshold };
|
||||
if (selection.declined) return { offer: false, reason: "declined", fileCount: null, threshold };
|
||||
const fileCount = trackedFileCount(repoPath);
|
||||
if (fileCount === null) return { offer: false, reason: "not-a-repo", fileCount, threshold };
|
||||
if (fileCount < threshold) return { offer: false, reason: "small-repo", fileCount, threshold };
|
||||
return { offer: true, reason: "large-repo", fileCount, threshold };
|
||||
}
|
||||
Reference in New Issue
Block a user