mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 20:30:47 +02:00
fix: correct adapters against real running providers (round 2)
Ran real backends in isolated environments (real Postgres-backed gbrain, live Sourcebot v6.5.0 with anonymous access, real graphify 0.9.23). All three now index + search end-to-end. Fixes: - gbrain: RESTORE `--strategy code` in refresh — round 1 removed it on a --help misread, which silently stopped code from ever being indexed. refresh now runs the verified two-pass (`sync`, then `sync --strategy code --full`). Pinned by a new test so the regression can't return. - sourcebot: an API key is NOT required for local use — anonymous access (FORCE_ENABLE_ANONYMOUS_ACCESS=true) serves /api/search keyless (verified). Key stays optional; only the messaging changed (anonymous-access first, key as fallback) plus a note that a local repo needs remote.origin.url to index. - graphify: correct the docstring — for CODE both `graphify <dir>` and `graphify update` are AST-only (no LLM); the LLM only renames clusters and ingests non-code, which our parser ignores. No LLM mode; local=true is correct. Docs: capability matrix + "Verified against real environments" updated to record all three proven end-to-end and to correct the two first-round mistakes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a524d860a8
commit
ab7989c6c1
@@ -123,9 +123,9 @@ consent; egress requires a separate explicit step.
|
|||||||
|
|
||||||
| Op | GBrain (recommend first) | Sourcebot | Graphify |
|
| Op | GBrain (recommend first) | Sourcebot | Graphify |
|
||||||
|----|--------------------------|-----------|----------|
|
|----|--------------------------|-----------|----------|
|
||||||
| `register_source` | ✓ `sources add` | ✓ local `git` connection in config.json | ✓ `graphify update <dir>` (local, no LLM) |
|
| `register_source` | ✓ `sources add --federated` | ✓ local `git` connection in config.json | ✓ `graphify update <dir>` (local, no LLM) |
|
||||||
| `refresh` | ✓ `sync --source` | ✓ auto (config change + reindexIntervalMs) | ✓ `graphify update <dir>` |
|
| `refresh` | ✓ `sync` + `sync --strategy code --full` | ✓ auto (config change + reindexIntervalMs) | ✓ `graphify update <dir>` |
|
||||||
| `search` | ✓ `gbrain search` (federated corpora) | ✓ `POST /api/search` + Bearer key (v5) | ✓ `graphify query "<q>" --graph <graph.json>` |
|
| `search` | ✓ `gbrain search` (federated corpora) | ✓ `POST /api/search` (keyless w/ anonymous access; Bearer key optional) | ✓ `graphify query "<q>" --graph <graph.json>` |
|
||||||
| `status` | ✓ `sources list` + page_count | ~ partial (server liveness) | ~ partial (graph.json present + node count) |
|
| `status` | ✓ `sources list` + page_count | ~ partial (server liveness) | ~ partial (graph.json present + node count) |
|
||||||
| `add` | ✓ `put <slug>` | ✗ declines | ✗ declines |
|
| `add` | ✓ `put <slug>` | ✗ declines | ✗ declines |
|
||||||
| `delete` | ✓ `delete <slug>` | ✗ declines | ✗ declines |
|
| `delete` | ✓ `delete <slug>` | ✗ declines | ✗ declines |
|
||||||
@@ -140,13 +140,17 @@ All three are driven directly from the runtime — no MCP client:
|
|||||||
(`sources add`/`sync`). Advertises all seven capabilities. **Recommended
|
(`sources add`/`sync`). Advertises all seven capabilities. **Recommended
|
||||||
first.**
|
first.**
|
||||||
- **Sourcebot** (`github.com/sourcebot-dev/sourcebot`, YC Fall 2025): self-hosted
|
- **Sourcebot** (`github.com/sourcebot-dev/sourcebot`, YC Fall 2025): self-hosted
|
||||||
whole-repo regex search. `register_source` adds a local `{ "type": "git", "url":
|
whole-repo regex search, deployed via Docker Compose (bundled server + Postgres
|
||||||
"file:///path" }` connection to the server's `config.json` (it re-indexes on
|
+ Redis; no supported non-Docker path). `register_source` adds a local `{ "type":
|
||||||
config change); `search` is `POST {baseUrl}/api/search`; `status` probes that
|
"git", "url": "file:///path" }` connection to the server's `config.json` (it
|
||||||
endpoint. Declines `add`/`delete`/`export`. **Sourcebot v5 gates `/api/search`
|
re-indexes on config change; a local repo needs a `remote.origin.url` or it is
|
||||||
behind auth**, so the adapter sends `Authorization: Bearer <SOURCEBOT_API_KEY>`.
|
skipped); `search` is `POST {baseUrl}/api/search`; `status` probes that endpoint.
|
||||||
A loopback `baseUrl` keeps content on the machine (local); a remote one requires
|
Declines `add`/`delete`/`export`. It is a **local** tool — indexed code stays on
|
||||||
egress consent.
|
your machine — and an **API key is optional**: a local instance with anonymous
|
||||||
|
access (`FORCE_ENABLE_ANONYMOUS_ACCESS=true`) serves `/api/search` keyless. The
|
||||||
|
adapter sends `Authorization: Bearer <SOURCEBOT_API_KEY>` only when a key is set.
|
||||||
|
A loopback `baseUrl` keeps content on the machine (local=true); a remote one
|
||||||
|
requires egress consent.
|
||||||
- **Graphify** (`github.com/Graphify-Labs/graphify`, YC-backed): local
|
- **Graphify** (`github.com/Graphify-Labs/graphify`, YC-backed): local
|
||||||
tree-sitter code graph via the `graphify` CLI. The adapter uses **`graphify
|
tree-sitter code graph via the `graphify` CLI. The adapter uses **`graphify
|
||||||
update <dir>`** — the local, no-LLM build (writes `graphify-out/graph.json`);
|
update <dir>`** — the local, no-LLM build (writes `graphify-out/graph.json`);
|
||||||
@@ -236,30 +240,45 @@ a later phase precisely so the first slices do not trigger the
|
|||||||
|
|
||||||
## Verified against real environments
|
## Verified against real environments
|
||||||
|
|
||||||
The three adapters were tested against the real tools in isolated environments
|
All three adapters were driven against the real tools in isolated environments
|
||||||
(parallel agents, one worktree each), not just unit fakes. What that surfaced and
|
(parallel agents, one worktree each), and all three now index + search a real repo
|
||||||
fixed:
|
end-to-end. Two rounds ran, because the first round's fixes included a mistake that
|
||||||
|
only real execution caught — recorded here honestly.
|
||||||
|
|
||||||
- **Graphify (real install, graphify 0.9.23).** The first cut ran `graphify <dir>`
|
- **GBrain — real Postgres+pgvector (Docker), gbrain 0.42.56 — PROVEN.** The
|
||||||
— which invokes an LLM extraction backend (needs a key + network), breaking the
|
default pglite/WASM engine is broken on macOS (upstream garrytan/gbrain#223), so
|
||||||
"local, no egress" promise — and parsed a made-up output format. Fixed to the
|
the working recipe points gbrain at a real Postgres via `DATABASE_URL`. Real
|
||||||
real local `graphify update` build and a parser written against the real
|
end-to-end search returned the actual code definition
|
||||||
`NODE`/`EDGE` output (file at `src=`/`at=`). Also fixed `search` ignoring the
|
(`[0.88] src-checksum-ts … export statement computeChecksum`). Real execution
|
||||||
indexed repo (now persists the indexed root) and `options` mislabeling an
|
caught a **regression I had introduced**: I removed `--strategy code` from
|
||||||
installed-but-unindexed provider as unavailable.
|
`refresh` based on a `--help` misread, which silently stopped code from ever
|
||||||
- **Sourcebot (live v5 in Docker).** Endpoint, request body, and response parsing
|
being indexed (only docs were). Restored to the verified two-pass
|
||||||
were correct against the real server. But v5 gates `/api/search` behind auth, so
|
(`sync`, then `sync --strategy code --full`); `--federated` registration is
|
||||||
the adapter got HTTP 401; added `Authorization: Bearer` support and made `status`
|
load-bearing for global search. Also fixed earlier: engine-down now degrades to
|
||||||
stop following the login redirect (it was falsely reporting "ready").
|
`PROVIDER_UNAVAILABLE` (one-line message) instead of `PROVIDER_ERROR` + a WASM
|
||||||
- **GBrain (real gbrain 0.42.56, pglite engine).** The engine was broken on the
|
stack dump.
|
||||||
host (upstream macOS WASM bug), which exposed two bugs: engine-down failures were
|
- **Sourcebot — live v6.5.0 (Docker) — PROVEN keyless.** Endpoint, body, and
|
||||||
reported as hard `PROVIDER_ERROR` with a raw stack dump instead of a clean
|
response parsing were correct against the real server. Correcting an earlier
|
||||||
`PROVIDER_UNAVAILABLE` degrade (fixed), and the adapter sent flags the real
|
wrong conclusion: Sourcebot does **not** require an API key for local use —
|
||||||
`gbrain` CLI does not define (`sync --strategy`, `search --source`) — corrected
|
enabling anonymous access (`FORCE_ENABLE_ANONYMOUS_ACCESS=true`) serves
|
||||||
to the real surface.
|
`/api/search` keyless, verified with a real hit through the CLI with no key set.
|
||||||
|
The key stays optional; the only fix was messaging (point users to anonymous
|
||||||
|
access first, key as fallback) plus a note that a local repo needs a
|
||||||
|
`remote.origin.url` to be indexed. It is a local tool (code stays on the
|
||||||
|
machine; a boot telemetry ping unless `SOURCEBOT_TELEMETRY_DISABLED=true`).
|
||||||
|
- **Graphify — real install, graphify 0.9.23 — PROVEN.** Correcting an earlier
|
||||||
|
wrong claim of mine: for **code**, `graphify <dir>` and `graphify update <dir>`
|
||||||
|
produce the identical AST graph with **no LLM call**; the LLM only renames
|
||||||
|
community clusters and ingests non-code docs, adding zero nodes/edges, and our
|
||||||
|
parser discards the field it touches. So there is deliberately no LLM mode, and
|
||||||
|
`local=true` is correct. The adapter uses `graphify update`; real `index`+search
|
||||||
|
returned correct `file:line` refs. Also fixed: `search` now reads the indexed
|
||||||
|
repo's graph (persisted root), and `options` reports an installed provider as
|
||||||
|
available.
|
||||||
|
|
||||||
Live end-to-end index+search-with-results was proven for Graphify and Sourcebot;
|
The larger lesson, kept on the record: a `--help` reading or a single agent's
|
||||||
GBrain's was blocked only by the host's broken engine, not by adapter code.
|
conclusion is not proof — running the real tool is. It reversed two of my
|
||||||
|
first-round calls (the gbrain flag removal and the graphify LLM claim).
|
||||||
|
|
||||||
## What this does NOT change
|
## What this does NOT change
|
||||||
|
|
||||||
|
|||||||
@@ -90,11 +90,15 @@ export class GbrainProvider implements CodeProvider {
|
|||||||
|
|
||||||
async refresh(source: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
|
async refresh(source: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
|
||||||
assertEgressConsent(this, opts);
|
assertEgressConsent(this, opts);
|
||||||
// `gbrain sync` has no `--strategy` flag (verified against gbrain 0.42.x --help).
|
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
|
||||||
this.#assertOk(spawnGbrain(["sync", "--source", source.id], {
|
// Two passes, verified end-to-end against real Postgres-backed gbrain 0.42.56:
|
||||||
baseEnv: opts.env,
|
// 1. default sync (markdown strategy) — indexes docs.
|
||||||
timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS,
|
// 2. `sync --strategy code` — the ACTUAL code-indexing pass. Without it code
|
||||||
}));
|
// is never indexed (the whole point of a code provider); `code-def` stays
|
||||||
|
// "not_built" and search only finds incidental doc mentions. `--full`
|
||||||
|
// forces it past the per-source checkpoint the markdown pass advanced.
|
||||||
|
this.#assertOk(spawnGbrain(["sync", "--source", source.id], { baseEnv: opts.env, timeout }));
|
||||||
|
this.#assertOk(spawnGbrain(["sync", "--source", source.id, "--strategy", "code", "--full"], { baseEnv: opts.env, timeout }));
|
||||||
return this.status(source, opts);
|
return this.status(source, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Graphify adapter — real CLI integration (github.com/Graphify-Labs/graphify).
|
* Graphify adapter — real CLI integration (github.com/Graphify-Labs/graphify).
|
||||||
*
|
*
|
||||||
* Graphify is a LOCAL tree-sitter knowledge graph. The genuinely local, no-LLM
|
* Graphify is a LOCAL tree-sitter knowledge graph. For CODE, `graphify <dir>`
|
||||||
* build is `graphify update <dir>` — it writes `<dir>/graphify-out/graph.json`
|
* and `graphify update <dir>` produce the SAME AST graph with NO LLM and NO
|
||||||
* with NO embeddings and NO network (verified against graphify 0.9.23). NOTE:
|
* network (verified against graphify 0.9.23 — both emit `AST extraction on N
|
||||||
* the bare `graphify <dir>` build instead runs LLM semantic extraction (a gemini
|
* code files`, all node origins `ast`). The LLM backend (openai/gemini) is only
|
||||||
* backend needing an API key + network), so this adapter deliberately uses
|
* used to RENAME community clusters (`graphify label` / `cluster-only`) and to
|
||||||
* `graphify update`, which keeps the "local, no egress consent" invariant true.
|
* ingest non-code docs (`graphify add`); it adds zero nodes/edges, and our parser
|
||||||
|
* discards the `community=` field it touches — so an LLM mode would send code
|
||||||
|
* off-machine for no change in search output, and is intentionally not offered.
|
||||||
|
*
|
||||||
|
* This adapter uses `graphify update <dir>` (writes `<dir>/graphify-out/graph.json`
|
||||||
|
* and does clustering in one shot) and stays fully local — nothing leaves the
|
||||||
|
* machine, so `local = true` and no egress consent is needed.
|
||||||
*
|
*
|
||||||
* Query is `graphify query "<q>" --graph <dir>/graphify-out/graph.json`; the
|
* Query is `graphify query "<q>" --graph <dir>/graphify-out/graph.json`; the
|
||||||
* `--graph` flag points at the built graph so search never depends on cwd.
|
* `--graph` flag points at the built graph so search never depends on cwd.
|
||||||
|
|||||||
@@ -46,9 +46,11 @@ export interface SourcebotOptions {
|
|||||||
/** Path to the server's config.json (for register_source). Defaults to SOURCEBOT_CONFIG. */
|
/** Path to the server's config.json (for register_source). Defaults to SOURCEBOT_CONFIG. */
|
||||||
configPath?: string;
|
configPath?: string;
|
||||||
/**
|
/**
|
||||||
* API key for the Sourcebot REST API. Defaults to SOURCEBOT_API_KEY. Sourcebot
|
* OPTIONAL API key for the Sourcebot REST API. Defaults to SOURCEBOT_API_KEY.
|
||||||
* v5 gates `/api/search` behind auth (`Authorization: Bearer <key>`); without
|
* A local instance with anonymous access enabled
|
||||||
* it, `search` gets HTTP 401. Generate one in Settings -> API Keys.
|
* (`FORCE_ENABLE_ANONYMOUS_ACCESS=true`) serves `/api/search` with NO key —
|
||||||
|
* verified keyless against a real Sourcebot v6.5.0. Only set a key when your
|
||||||
|
* instance is login-gated; it is sent as `Authorization: Bearer <key>`.
|
||||||
*/
|
*/
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
/** Injectable fetch for tests. */
|
/** Injectable fetch for tests. */
|
||||||
@@ -93,7 +95,12 @@ export class SourcebotProvider implements CodeProvider {
|
|||||||
return this.capabilities.has(capability);
|
return this.capabilities.has(capability);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add the repo as a local `git` connection in Sourcebot's config.json. */
|
/**
|
||||||
|
* Add the repo as a local `git` connection in Sourcebot's config.json.
|
||||||
|
* NOTE: Sourcebot silently SKIPS a local repo that has no `remote.origin.url`
|
||||||
|
* (logs "Skipping <path> - remote.origin.url not found"); a freshly `git init`'d
|
||||||
|
* repo must set an origin before it will index.
|
||||||
|
*/
|
||||||
async registerSource(repo: RepoRef, opts: OpOptions = {}): Promise<SourceStatus> {
|
async registerSource(repo: RepoRef, opts: OpOptions = {}): Promise<SourceStatus> {
|
||||||
assertEgressConsent(this, opts); // no-op when the server is loopback (local)
|
assertEgressConsent(this, opts); // no-op when the server is loopback (local)
|
||||||
if (!this.#configPath) {
|
if (!this.#configPath) {
|
||||||
@@ -148,7 +155,7 @@ export class SourcebotProvider implements CodeProvider {
|
|||||||
opts.timeout ?? DEFAULT_TIMEOUT_MS,
|
opts.timeout ?? DEFAULT_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
if (res.status === 401 || res.status === 403) {
|
if (res.status === 401 || res.status === 403) {
|
||||||
return { id: "*", state: "unknown", partial: true, detail: "reachable but not authenticated (set SOURCEBOT_API_KEY)" };
|
return { id: "*", state: "unknown", partial: true, detail: "reachable but login-gated (enable anonymous access with FORCE_ENABLE_ANONYMOUS_ACCESS=true for local use, or set SOURCEBOT_API_KEY)" };
|
||||||
}
|
}
|
||||||
return { id: "*", state: res.ok ? "ready" : "unknown", partial: true, detail: `HTTP ${res.status}` };
|
return { id: "*", state: res.ok ? "ready" : "unknown", partial: true, detail: `HTTP ${res.status}` };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -168,7 +175,7 @@ export class SourcebotProvider implements CodeProvider {
|
|||||||
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot unreachable at ${this.#baseUrl}: ${(err as Error).message}`, this.id);
|
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot unreachable at ${this.#baseUrl}: ${(err as Error).message}`, this.id);
|
||||||
}
|
}
|
||||||
if (res.status === 401 || res.status === 403) {
|
if (res.status === 401 || res.status === 403) {
|
||||||
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot requires authentication; set SOURCEBOT_API_KEY (HTTP ${res.status})`, this.id);
|
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot is login-gated (HTTP ${res.status}); enable anonymous access (FORCE_ENABLE_ANONYMOUS_ACCESS=true) for local use, or set SOURCEBOT_API_KEY`, this.id);
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new CodeProviderError("PROVIDER_ERROR", `Sourcebot ${path} returned HTTP ${res.status}`, this.id);
|
if (!res.ok) throw new CodeProviderError("PROVIDER_ERROR", `Sourcebot ${path} returned HTTP ${res.status}`, this.id);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -281,6 +281,21 @@ exit 1
|
|||||||
expect(hits).toEqual([{ ref: "src/x.ts", score: 0.88, snippet: "match", kind: "document" }]);
|
expect(hits).toEqual([{ ref: "src/x.ts", score: 0.88, snippet: "match", kind: "document" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("refresh runs the code-indexing pass (`sync --strategy code --full`)", async () => {
|
||||||
|
// Real gbrain only indexes code when `sync --strategy code` runs; without it
|
||||||
|
// code-def stays not_built. Pin that the adapter issues that pass.
|
||||||
|
const marker = path.join(homeDir, "sync-calls.log");
|
||||||
|
writeShim(`#!/usr/bin/env bash
|
||||||
|
if [ "$1" = "sync" ]; then printf '%s\\n' "$*" >> "${marker}"; exit 0; fi
|
||||||
|
if [ "$1" = "sources" ]; then echo '{"sources":[{"id":"code","local_path":"/r","page_count":1}]}'; exit 0; fi
|
||||||
|
exit 1
|
||||||
|
`);
|
||||||
|
await new GbrainProvider().refresh({ id: "code" }, { env: env(), consented: true });
|
||||||
|
const log = fs.readFileSync(marker, "utf-8");
|
||||||
|
expect(log).toContain("--strategy code");
|
||||||
|
expect(log).toContain("--full");
|
||||||
|
});
|
||||||
|
|
||||||
test("engine-down (pglite WASM) degrades to PROVIDER_UNAVAILABLE, not PROVIDER_ERROR", async () => {
|
test("engine-down (pglite WASM) degrades to PROVIDER_UNAVAILABLE, not PROVIDER_ERROR", async () => {
|
||||||
// Reproduces garrytan/gbrain#223: engine fails to init; must degrade cleanly.
|
// Reproduces garrytan/gbrain#223: engine fails to init; must degrade cleanly.
|
||||||
writeShim(`#!/usr/bin/env bash
|
writeShim(`#!/usr/bin/env bash
|
||||||
|
|||||||
Reference in New Issue
Block a user