mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
Four review findings on the wave's own Phase 1 port, all red-first: 'consent <repo> no' recorded consent GRANTED (the CLI ignored the argument and always wrote true) — yes|no is now required and garbage records nothing; Sourcebot egress receipts claimed consented=true on paths that never checked consent — the actual consent state is threaded into every receipt, search is fail-closed on non-loopback, and the liveness probe's receipt says truthfully that it sends no repo content; repoPolicyVeto only honored the deny tier while gbrain refresh writes pages — write-class ops now veto on read-only too, matching the sync chokepoint, via one shared lib/gbrain-repo-policy-client.ts (win32 bash invocation, spawn-vs-unreadable error distinction) used by both call sites. Also: source ids get a host+path hash (same-name repos no longer collide), refresh timeout raised to 120s, availability probes run concurrently at 3s, graphify status stops JSON.parsing 100MB graphs for a count, and every ported file carries the fork MIT notice. +15 tests across the two suites.
58 lines
2.2 KiB
TypeScript
58 lines
2.2 KiB
TypeScript
/**
|
|
* suggest — should the session-start indexing offer be made for this repo?
|
|
*
|
|
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
|
|
*
|
|
* 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";
|
|
|
|
// 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 };
|
|
}
|