Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chenhong268_cbrain.json
T

1 line
143 KiB
JSON

{"repo": "chenhong268/cbrain", "files": {"src/llm/deepseek.ts": {"7a3d8700e2cbb4d75c7f8a5eca848fe09f1a0d1b": {"t": 1785746045.4365695, "n": 2678, "h": "4a38f1588bec2af1", "c": "import { LLMTimeoutError } from \"./provider.js\";\nimport type { LLMProvider, ChatMessage, ChatOptions } from \"./provider.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.deepseek.com\";\nconst DEFAULT_MODEL = \"deepseek-v4-flash\";\n\ninterface DeepSeekChatResponse {\n choices: Array<{\n message: { content: string };\n finish_reason: string;\n }>;\n usage?: { total_tokens: number };\n}\n\nexport interface DeepSeekLLMOptions {\n timeoutMs?: number;\n /** Enable DeepSeek's V4 thinking extension for a compatible proxy/model.\n * Defaults to true only for official api.deepseek.com V4 models. */\n supportsThinking?: boolean;\n}\n\nfunction isOfficialV4Endpoint(baseUrl: string, model: string): boolean {\n try {\n return new URL(baseUrl).hostname === \"api.deepseek.com\" && /^deepseek-v4(?:-|$)/.test(model);\n } catch {\n return false;\n }\n}\n\nexport class DeepSeekLLMProvider implements LLMProvider {\n readonly name = \"deepseek\";\n private apiKey: string;\n private baseUrl: string;\n private model: string;\n private timeoutMs: number;\n private supportsThinking: boolean;\n\n constructor(apiKey: string, baseUrl?: string, model?: string, opts?: DeepSeekLLMOptions) {\n this.apiKey = apiKey;\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;\n this.model = model ?? DEFAULT_MODEL;\n this.timeoutMs = opts?.timeoutMs ?? 30_000;\n this.supportsThinking = opts?.supportsThinking ?? isOfficialV4Endpoint(this.baseUrl, this.model);\n }\n\n async chat(messages: ChatMessage[], options?: ChatOptions): Promise<string> {\n const url = `${this.baseUrl}/chat/completions`;\n const body = JSON.stringify({\n model: this.model,\n messages,\n temperature: 0.1,\n response_format: { type: \"json_object\" },\n ...(options?.thinking && this.supportsThinking ? { thinking: { type: options.thinking } } : {}),\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`DeepSeek API error: ${response.status} ${text}`);\n }\n\n const json = (await response.json()) as DeepSeekChatResponse;\n return json.choices[0]?.message?.content ?? \"\";\n } catch (e) {\n if (controller.signal.aborted) {\n throw new LLMTimeoutError(\"DeepSeek\", this.timeoutMs);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"}, "43ae0457ecf622d8970b4dc610c9c67680214c2e": {"t": 1785759165.6916778, "n": 2678, "h": "4a38f1588bec2af1", "c": "import { LLMTimeoutError } from \"./provider.js\";\nimport type { LLMProvider, ChatMessage, ChatOptions } from \"./provider.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.deepseek.com\";\nconst DEFAULT_MODEL = \"deepseek-v4-flash\";\n\ninterface DeepSeekChatResponse {\n choices: Array<{\n message: { content: string };\n finish_reason: string;\n }>;\n usage?: { total_tokens: number };\n}\n\nexport interface DeepSeekLLMOptions {\n timeoutMs?: number;\n /** Enable DeepSeek's V4 thinking extension for a compatible proxy/model.\n * Defaults to true only for official api.deepseek.com V4 models. */\n supportsThinking?: boolean;\n}\n\nfunction isOfficialV4Endpoint(baseUrl: string, model: string): boolean {\n try {\n return new URL(baseUrl).hostname === \"api.deepseek.com\" && /^deepseek-v4(?:-|$)/.test(model);\n } catch {\n return false;\n }\n}\n\nexport class DeepSeekLLMProvider implements LLMProvider {\n readonly name = \"deepseek\";\n private apiKey: string;\n private baseUrl: string;\n private model: string;\n private timeoutMs: number;\n private supportsThinking: boolean;\n\n constructor(apiKey: string, baseUrl?: string, model?: string, opts?: DeepSeekLLMOptions) {\n this.apiKey = apiKey;\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;\n this.model = model ?? DEFAULT_MODEL;\n this.timeoutMs = opts?.timeoutMs ?? 30_000;\n this.supportsThinking = opts?.supportsThinking ?? isOfficialV4Endpoint(this.baseUrl, this.model);\n }\n\n async chat(messages: ChatMessage[], options?: ChatOptions): Promise<string> {\n const url = `${this.baseUrl}/chat/completions`;\n const body = JSON.stringify({\n model: this.model,\n messages,\n temperature: 0.1,\n response_format: { type: \"json_object\" },\n ...(options?.thinking && this.supportsThinking ? { thinking: { type: options.thinking } } : {}),\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`DeepSeek API error: ${response.status} ${text}`);\n }\n\n const json = (await response.json()) as DeepSeekChatResponse;\n return json.choices[0]?.message?.content ?? \"\";\n } catch (e) {\n if (controller.signal.aborted) {\n throw new LLMTimeoutError(\"DeepSeek\", this.timeoutMs);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"}, "5755c486fabbc6d3f1e8e327339f45741d5d1040": {"t": 1785759167.058136, "n": 2007, "h": "f032cdf3c5ae1ebf", "c": "import type { LLMProvider, ChatMessage } from \"./provider.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.deepseek.com\";\nconst DEFAULT_MODEL = \"deepseek-v4-flash\";\n\ninterface DeepSeekChatResponse {\n choices: Array<{\n message: { content: string };\n finish_reason: string;\n }>;\n usage?: { total_tokens: number };\n}\n\nexport interface DeepSeekLLMOptions {\n timeoutMs?: number;\n}\n\nexport class DeepSeekLLMProvider implements LLMProvider {\n readonly name = \"deepseek\";\n private apiKey: string;\n private baseUrl: string;\n private model: string;\n private timeoutMs: number;\n\n constructor(apiKey: string, baseUrl?: string, model?: string, opts?: DeepSeekLLMOptions) {\n this.apiKey = apiKey;\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;\n this.model = model ?? DEFAULT_MODEL;\n this.timeoutMs = opts?.timeoutMs ?? 30_000;\n }\n\n async chat(messages: ChatMessage[]): Promise<string> {\n const url = `${this.baseUrl}/chat/completions`;\n const body = JSON.stringify({\n model: this.model,\n messages,\n temperature: 0.1,\n response_format: { type: \"json_object\" },\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body,\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`DeepSeek API error: ${response.status} ${text}`);\n }\n\n const json = (await response.json()) as DeepSeekChatResponse;\n return json.choices[0]?.message?.content ?? \"\";\n } catch (e) {\n if (e instanceof DOMException && e.name === \"AbortError\") {\n throw new Error(`DeepSeek LLM request timed out after ${this.timeoutMs}ms`);\n }\n throw e;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"}, "1d6bdabf06d9c87ccf5bfa2e4c80f8aaa01ada1b": {"t": 1785759168.596631, "n": 1408, "h": "711681891af11eaa", "c": "import type { LLMProvider, ChatMessage } from \"./provider.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.deepseek.com\";\nconst DEFAULT_MODEL = \"deepseek-v4-flash\";\n\ninterface DeepSeekChatResponse {\n choices: Array<{\n message: { content: string };\n finish_reason: string;\n }>;\n usage?: { total_tokens: number };\n}\n\nexport class DeepSeekLLMProvider implements LLMProvider {\n readonly name = \"deepseek\";\n private apiKey: string;\n private baseUrl: string;\n private model: string;\n\n constructor(apiKey: string, baseUrl?: string, model?: string) {\n this.apiKey = apiKey;\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;\n this.model = model ?? DEFAULT_MODEL;\n }\n\n async chat(messages: ChatMessage[]): Promise<string> {\n const url = `${this.baseUrl}/chat/completions`;\n const body = JSON.stringify({\n model: this.model,\n messages,\n temperature: 0.1,\n response_format: { type: \"json_object\" },\n });\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`DeepSeek API error: ${response.status} ${text}`);\n }\n\n const json = (await response.json()) as DeepSeekChatResponse;\n return json.choices[0]?.message?.content ?? \"\";\n }\n}\n"}}, "bin/check-first-recall-gate.ts": {"7a3d8700e2cbb4d75c7f8a5eca848fe09f1a0d1b": {"t": 1785747483.7972236, "n": 44327, "h": "84bef9c030392969", "c": "#!/usr/bin/env bun\n// check-first-recall-gate.ts — Offline first-install-to-first-recall v2.0 release gate\n//\n// Usage: bun bin/check-first-recall-gate.ts\n//\n// Proves the packed CLI artifact works end-to-end:\n// init → doctor → mcp-config → skill-pack → ingest → query → migration\n//\n// Fully offline (uses checkout node_modules via symlink). Mock embedding server on localhost.\n// Fully isolated environment. Verified cleanup on ALL paths.\n//\n// Production dependency install is verified separately by: bun run check:install-network\n// v2 release requires BOTH gates to pass.\n//\n// Test-only controls (env vars, ignored in production):\n// GATE_PROJECT_DIR=<path> — override project dir (for shadow-project tests)\n// GATE_FAULT_PROVIDER=1 — use unreachable mock URL → ingest/query fail\n// GATE_FAULT_SKIP_INGEST=1 — skip ingest stage → query on empty brain\n// GATE_FAULT_SKILL=1 — delete SKILL.md → skill-pack fails\n// GATE_FAULT_INIT=1 — block init with pre-existing file → init fails\n// GATE_FAULT_VAULT_NESTED=1 — create runtime/ inside vault → artifacts fail\n// GATE_FAULT_MIGRATION_CORRUPT=1 — corrupt migration fixture DB → migration fails\n// GATE_FAULT_MCP_CRED_LEAK=1 — inject credential into report → privacy fail\n// GATE_FORCE_DETERMINISTIC=1 — skip TCP, use in-process deterministic embedding (#204)\n//\n// Exit codes: 0 = go, 1 = no-go, 2 = fatal error (script bug)\n\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n symlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, resolve, dirname, relative } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { createServer } from \"node:net\";\nimport { fileURLToPath } from \"url\";\nimport { Database } from \"bun:sqlite\";\n\n// ── Types ──\n\ntype GateVerdict = \"go\" | \"no-go\";\n\ninterface AssertionResult {\n readonly check: string;\n readonly passed: boolean;\n readonly actual: string;\n readonly expected: string;\n}\n\ninterface StageResult {\n readonly id: string;\n readonly command: string;\n readonly exitCode: number;\n readonly duration_ms: number;\n readonly passed: boolean;\n readonly assertions: ReadonlyArray<AssertionResult>;\n}\n\ninterface GateReport {\n readonly gate: \"first-recall-v2\";\n readonly version: string;\n readonly timestamp: string;\n readonly verdict: GateVerdict;\n readonly embedding_mode: \"http\" | \"deterministic\";\n readonly stages: ReadonlyArray<StageResult>;\n readonly cleanup: { readonly verified: boolean; readonly path: string };\n readonly duration_ms: number;\n}\n\ninterface GateResult {\n readonly report: GateReport;\n readonly exitCode: number;\n}\n\ninterface IsolationContext {\n readonly tmpdir: string;\n readonly homeDir: string;\n readonly bunInstallDir: string;\n readonly brainDir: string;\n configPath: string;\n readonly cwd: string;\n}\n\ninterface PackResult {\n readonly tarballPath: string;\n readonly pkgDir: string;\n readonly binPath: string;\n}\n\ninterface MockServer {\n readonly url: string;\n readonly port: number;\n stop(): void;\n}\n\n// (#204) How the packed CLI obtains embeddings. http = local mock server\n// (preferred, issue requirement); deterministic = in-process provider when a\n// localhost TCP listener cannot bind. Neither skips the mock — both are mocks.\ntype EmbeddingMode =\n | { readonly kind: \"http\"; readonly mock: MockServer }\n | { readonly kind: \"deterministic\" };\n\ninterface CliResult {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n readonly duration_ms: number;\n}\n\n// ── Constants ──\n\nconst INGEST_TEXT =\n \"实体A是一个重要的概念,它与主题B密切相关。方法C是解决这个问题的关键技术路径。\";\nconst INGEST_UNIQUE_TOKEN = \"实体a\"; // appears in auto-generated slug\nconst QUERY_TEXT = \"实体A 主题B 方法C\"; // includes unique token for deterministic recall\nconst VECTOR_DIM = 2048;\nconst DETERMINISTIC_VECTOR = Array.from({ length: VECTOR_DIM }, (_, i) =>\n Math.sin(i * 0.001) * 0.5,\n);\n\n// ── Mock Embedding Server ──\n\n// (#204) Probe a free localhost port via Node net rather than relying on\n// Bun.serve's port:0 auto-allocation, which some environments/Bun versions\n// misreport as \"Failed to start server. Is port 0 in use?\". Resolves to a\n// concrete port number handed to Bun.serve.\nfunction findFreeLocalhostPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n probe.unref();\n probe.on(\"error\", () => {\n reject(new Error(\"mock embedding server failed to bind a localhost port\"));\n });\n probe.listen(0, \"127.0.0.1\", () => {\n const addr = probe.address();\n const port = typeof addr === \"object\" && addr !== null ? addr.port : 0;\n probe.close(() => {\n if (port > 0) resolve(port);\n else reject(new Error(\"mock embedding server failed to bind a localhost port\"));\n });\n });\n });\n}\n\n// (#204) Deterministic, retryable localhost port selection. Probes a free port,\n// binds Bun.serve to it, and retries with a fresh probe on transient failure.\n// A startup failure surfaces a single sanitized message (no paths/stack/secrets)\n// so the release gate stays diagnosable without leaking environment detail.\nasync function startMockEmbeddingServer(): Promise<MockServer> {\n const MAX_ATTEMPTS = 3;\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n const port = await findFreeLocalhostPort();\n const served = Bun.serve({\n port,\n hostname: \"127.0.0.1\",\n fetch(req) {\n const url = new URL(req.url);\n if (url.pathname === \"/embeddings\" && req.method === \"POST\") {\n return req.json().then((body: { input?: string[] }) => {\n const inputs = body.input ?? [];\n const data = inputs.map((_, i) => ({\n embedding: DETERMINISTIC_VECTOR,\n index: i,\n }));\n return Response.json({\n data,\n usage: { total_tokens: inputs.length * 10 },\n });\n });\n }\n return new Response(\"NOT_FOUND\", { status: 404 });\n },\n });\n return {\n url: `http://127.0.0.1:${served.port}`,\n port: served.port,\n stop() {\n served.stop();\n },\n };\n } catch {\n // Port probe or Bun.serve failed — retry with a fresh localhost port.\n }\n }\n throw new Error(\"mock embedding server failed to bind a localhost port\");\n}\n\n// (#204) Resolve how the packed CLI obtains embeddings.\n// Preferred: HTTP mock server on localhost (issue requires a local mock server).\n// If a TCP listener cannot bind (some environments reject localhost bind on both\n// Bun.serve and node:net), fall back to the in-process deterministic provider\n// (embedding.provider=\"deterministic\") — no socket, no HTTP, no creds. This is\n// NOT a pass downgrade: embeddings still work and ingest+query still run.\nasync function resolveEmbeddingMode(): Promise<EmbeddingMode> {\n // ── Test-only: force the deterministic path (simulates a TCP-less env) ──\n if (process.env.GATE_FORCE_DETERMINISTIC === \"1\") {\n return { kind: \"deterministic\" };\n }\n // ── Test-only fault: unreachable provider URL → ingest/query fail → no-go ──\n if (process.env.GATE_FAULT_PROVIDER === \"1\") {\n return { kind: \"http\", mock: { url: \"http://127.0.0.1:1\", port: 1, stop() {} } };\n }\n try {\n return { kind: \"http\", mock: await startMockEmbeddingServer() };\n } catch {\n // TCP listener unavailable — use in-process deterministic embeddings.\n return { kind: \"deterministic\" };\n }\n}\n\n// ── Isolation ──\n\nfunction createIsolation(): IsolationContext {\n const base = mkdtempSync(join(tmpdir(), \"cbrain-gate-\"));\n const homeDir = join(base, \"home\");\n const bunInstallDir = join(base, \"bun\");\n const brainDir = join(base, \"brain\");\n const cwd = join(base, \"cwd\");\n\n mkdirSync(homeDir, { recursive: true });\n mkdirSync(bunInstallDir, { recursive: true });\n mkdirSync(cwd, { recursive: true });\n\n return { tmpdir: base, homeDir, bunInstallDir, brainDir, configPath: \"\", cwd };\n}\n\n// Build a CLEAN env for subprocesses — allowlist only, no inherited secrets.\nfunction buildIsolatedEnv(\n ctx: IsolationContext,\n extraEnv: Record<string, string> = {},\n): Record<string, string> {\n // Only inherit truly safe system vars\n const safeKeys = [\"LANG\", \"LC_ALL\", \"LC_CTYPE\", \"TERM\", \"TMPDIR\", \"TEMP\", \"TMP\", \"__CF_USER_TEXT_ENCODING\"];\n const inherited: Record<string, string> = {};\n for (const key of safeKeys) {\n if (process.env[key]) inherited[key] = process.env[key]!;\n }\n\n return {\n ...inherited,\n // PATH: need bun + system tools, but no cbrain bin\n PATH: `${ctx.bunInstallDir}/bin:${process.env.PATH ?? \"\"}`.split(\":\")\n .filter((p) => !p.includes(\"cbrain\") || p.includes(\"cbrain-gate\"))\n .join(\":\"),\n HOME: ctx.homeDir,\n BUN_INSTALL: ctx.bunInstallDir,\n XDG_CONFIG_HOME: join(ctx.homeDir, \".config\"),\n XDG_DATA_HOME: join(ctx.homeDir, \".local\", \"share\"),\n XDG_CACHE_HOME: join(ctx.homeDir, \".cache\"),\n CBRAIN_CONFIG: ctx.configPath,\n ZHIPU_API_KEY: \"gate-test-fake-key\",\n // Explicitly clear any inherited provider vars\n ...(process.env.DEEPSEEK_API_KEY ? { DEEPSEEK_API_KEY: \"\" } : {}),\n ...extraEnv,\n };\n}\n\n// ── Pack + Offline Install ──\n// Uses checkout node_modules via symlink — fully offline, no network needed.\n// Production dependency install is verified by check:install-network separately.\n\nfunction packAndInstall(projectDir: string, ctx: IsolationContext, version: string): PackResult {\n // Verify checkout has deps\n if (!existsSync(join(projectDir, \"node_modules\"))) {\n throw new Error(\"node_modules not found in checkout — run bun install first\");\n }\n\n // Deterministic tarball name: cbrain-{version}.tgz\n const tarballName = `cbrain-${version}.tgz`;\n const tarballInProject = join(projectDir, tarballName);\n let backupPath: string | null = null;\n\n // Backup pre-existing same-version tarball (if any) — restored in finally\n if (existsSync(tarballInProject)) {\n backupPath = `${tarballInProject}.gate-backup`;\n renameSync(tarballInProject, backupPath);\n }\n\n try {\n // Pack — always creates cbrain-{version}.tgz in project dir\n execSync(\"bun pm pack\", { cwd: projectDir, encoding: \"utf-8\", timeout: 30_000 });\n\n if (!existsSync(tarballInProject)) {\n throw new Error(\"bun pm pack produced no tarball\");\n }\n\n // Move tarball to isolated dir — never leaves artifacts in project dir\n const packDir = join(ctx.tmpdir, \"pack\");\n mkdirSync(packDir, { recursive: true });\n const isolatedTarball = join(packDir, tarballName);\n renameSync(tarballInProject, isolatedTarball);\n\n // Extract\n execSync(`tar -xzf \"${isolatedTarball}\" -C \"${packDir}\"`, { encoding: \"utf-8\" });\n\n // Find package dir (the extracted directory, not our tarball)\n const entries = readdirSync(packDir).filter((e) => e !== tarballName);\n if (entries.length === 0) throw new Error(\"tarball was empty\");\n const pkgDir = join(packDir, entries[0]);\n\n // OFFLINE: symlink checkout node_modules — no network needed.\n // Production dependency install is proven by check:install-network separately.\n symlinkSync(join(projectDir, \"node_modules\"), join(pkgDir, \"node_modules\"));\n\n return {\n tarballPath: isolatedTarball,\n pkgDir,\n binPath: join(pkgDir, \"src\", \"cli\", \"index.ts\"),\n };\n } finally {\n // Always restore pre-existing tarball — even on failure\n if (backupPath && existsSync(backupPath)) {\n renameSync(backupPath, tarballInProject);\n }\n }\n}\n\n// ── CLI Runner (async — Bun.spawn does NOT block event loop) ──\n\nasync function runCli(\n binPath: string,\n args: string[],\n ctx: IsolationContext,\n env: Record<string, string>,\n timeoutMs = 30_000,\n): Promise<CliResult> {\n const start = performance.now();\n try {\n const proc = Bun.spawn([process.execPath, \"run\", binPath, ...args], {\n cwd: ctx.cwd,\n env,\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n\n // Race process exit vs timeout — always clear the timer\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timeoutHandle = setTimeout(() => {\n proc.kill();\n reject(new Error(`TIMEOUT after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n let exitCode: number;\n try {\n exitCode = await Promise.race([proc.exited, timeoutPromise]);\n } finally {\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n }\n\n const [stdout, stderr] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n ]);\n\n return { exitCode, stdout, stderr, duration_ms: Math.round(performance.now() - start) };\n } catch (e) {\n return {\n exitCode: 1,\n stdout: \"\",\n stderr: e instanceof Error ? e.message : String(e),\n duration_ms: Math.round(performance.now() - start),\n };\n }\n}\n\n// ── Assertion Helpers ──\n// Every stage uses assertions.every(a => a.passed) for its verdict — no exceptions.\n\nfunction assertExitCode(result: CliResult, expected: number): AssertionResult {\n return {\n check: `exit code ${expected}`,\n passed: result.exitCode === expected,\n actual: String(result.exitCode),\n expected: String(expected),\n };\n}\n\nfunction assertExitCodeOkOrWarn(result: CliResult): AssertionResult {\n return {\n check: \"exit code 0 or 1 (warn allowed)\",\n passed: result.exitCode <= 1,\n actual: String(result.exitCode),\n expected: \"0 or 1\",\n };\n}\n\nfunction assertJsonField(stdout: string, field: string, expected: unknown): AssertionResult {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: false, actual: \"JSON parse failed\", expected: String(expected) };\n }\n const actual = parsed[field];\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: actual === expected, actual: String(actual), expected: String(expected) };\n}\n\nfunction assertJsonParseable(stdout: string, label: string): AssertionResult {\n try {\n JSON.parse(stdout);\n return { check: label, passed: true, actual: \"valid JSON\", expected: \"valid JSON\" };\n } catch {\n return { check: label, passed: false, actual: \"invalid JSON\", expected: \"valid JSON\" };\n }\n}\n\nfunction assertFieldExists(stdout: string, field: string): AssertionResult {\n let parsed: Record<string, unknown>;\n try { parsed = JSON.parse(stdout); } catch {\n return { check: `json has \"${field}\"`, passed: false, actual: \"JSON parse failed\", expected: `field \"${field}\" exists` };\n }\n const val = parsed[field];\n return {\n check: `json has \"${field}\"`,\n passed: val !== undefined && val !== null && String(val).length > 0,\n actual: val === undefined ? \"undefined\" : val === null ? \"null\" : String(val).slice(0, 80),\n expected: `non-empty \"${field}\"`,\n };\n}\n\nfunction assertContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: haystack.includes(needle), actual: haystack.includes(needle) ? \"found\" : \"not found\", expected: `contains \"${needle}\"` };\n}\n\nfunction assertNotContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: !haystack.includes(needle), actual: haystack.includes(needle) ? \"found (bad)\" : \"not found (good)\", expected: `does not contain \"${needle}\"` };\n}\n\nfunction assertMatches(text: string, pattern: RegExp, label: string): AssertionResult {\n return { check: label, passed: pattern.test(text), actual: pattern.test(text) ? \"matched\" : \"no match\", expected: `matches ${pattern}` };\n}\n\n/** Derive stage passed from ALL assertions — the single source of truth. */\nfunction verdict(assertions: ReadonlyArray<AssertionResult>): boolean {\n return assertions.every((a) => a.passed);\n}\n\nfunction makeStage(\n id: string,\n command: string,\n result: CliResult,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command, exitCode: result.exitCode, duration_ms: result.duration_ms, passed: verdict(assertions), assertions };\n}\n\nfunction makeSyntheticStage(\n id: string,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command: \"(internal check)\", exitCode: verdict(assertions) ? 0 : 1, duration_ms: 0, passed: verdict(assertions), assertions };\n}\n\n// ── Config Patching ──\n\nfunction patchConfig(configPath: string, mode: EmbeddingMode): void {\n const config = JSON.parse(readFileSync(configPath, \"utf-8\"));\n if (mode.kind === \"deterministic\") {\n // (#204) In-process deterministic embeddings: no HTTP server, no TCP, no creds.\n config.embedding.provider = \"deterministic\";\n config.embedding.apiKey = \"gate-test-fake-key\";\n delete config.embedding.baseUrl;\n } else {\n config.embedding.provider = \"zhipu\";\n config.embedding.baseUrl = mode.mock.url;\n config.embedding.apiKey = \"gate-test-fake-key\";\n }\n config.ner = { enabled: false };\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n}\n\n// ── v1.9 Migration Fixture Builder ──\n// Creates a minimal SQLite DB with pre-migration schema + data.\n// Does NOT use `cbrain init` — schema is built with raw SQL.\n\nconst V19_FIXTURE_SLUG = \"records/v1-9-fixture\";\nconst V19_FIXTURE_TITLE = \"v1.9迁移测试记录\";\nconst V19_FIXTURE_BODY = \"组织D在主题B中采用了方法C的核心策略。这段记录来自模拟的v1.9安装。\";\nconst V19_QUERY_TEXT = \"组织D 主题B\";\n\nfunction buildV19FixtureDB(dbPath: string, vaultPath: string): void {\n mkdirSync(dirname(dbPath), { recursive: true });\n mkdirSync(join(vaultPath, \"records\"), { recursive: true });\n for (const sub of [\"entities\", \"concepts\", \"insights\"]) {\n mkdirSync(join(vaultPath, \"brain\", sub), { recursive: true });\n }\n\n // Create DB with v1.9-era base schema (before migrations)\n const db = new Database(dbPath);\n db.exec(\"PRAGMA journal_mode = WAL\");\n\n // Base tables — minimal v1.9 schema\n db.exec(`CREATE TABLE IF NOT EXISTS pages (\n slug TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n file_path TEXT NOT NULL,\n content_hash TEXT,\n tier INTEGER DEFAULT 3,\n mention_count INTEGER DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n )`);\n db.exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);\n db.exec(`CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n page_slug TEXT NOT NULL,\n chunk_index INTEGER NOT NULL,\n content TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(page_slug, chunk_index)\n )`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(page_slug, content, tokenize='trigram')`);\n db.exec(`CREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_slug TEXT NOT NULL,\n to_slug TEXT NOT NULL,\n relation TEXT NOT NULL DEFAULT 'mentions',\n context TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(from_slug, to_slug, relation)\n )`);\n\n // Insert fixture data\n db.run(\"INSERT INTO pages (slug, type, title, file_path) VALUES (?, ?, ?, ?)\",\n [V19_FIXTURE_SLUG, \"record\", V19_FIXTURE_TITLE, `${V19_FIXTURE_SLUG}.md`]);\n db.run(\"INSERT INTO chunks (page_slug, chunk_index, content) VALUES (?, 0, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO chunks_fts (page_slug, content) VALUES (?, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO config (key, value) VALUES ('schema_version', '1')\");\n\n db.close();\n\n // Create vault markdown file\n const mdPath = join(vaultPath, `${V19_FIXTURE_SLUG}.md`);\n mkdirSync(dirname(mdPath), { recursive: true });\n writeFileSync(mdPath, `---\\ntitle: \"${V19_FIXTURE_TITLE}\"\\ntype: record\\n---\\n\\n${V19_FIXTURE_BODY}\\n`);\n}\n\n// ── Stage 1: init ──\n\nasync function runStageInit(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"init\", \"--dir\", ctx.brainDir, \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"init output is valid JSON\"),\n assertJsonField(result.stdout, \"status\", \"ok\"),\n assertJsonField(result.stdout, \"created\", true),\n ];\n\n // Extract configPath and verify it's inside tmpdir\n try {\n const parsed = JSON.parse(result.stdout);\n const cp = parsed.configPath;\n if (typeof cp === \"string\" && cp.length > 0) {\n ctx.configPath = cp;\n assertions.push(assertContains(cp, ctx.tmpdir, \"config path inside tmpdir\"));\n } else {\n assertions.push({ check: \"configPath present\", passed: false, actual: String(cp), expected: \"non-empty string\" });\n }\n } catch {\n assertions.push({ check: \"configPath present\", passed: false, actual: \"JSON parse failed\", expected: \"non-empty string\" });\n }\n\n return makeStage(\"init\", \"cbrain init --dir <brain> --json\", result, assertions);\n}\n\n// ── Stage 2: doctor --first-run ──\n\nasync function runStageDoctor(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCodeOkOrWarn(result),\n assertJsonParseable(result.stdout, \"doctor output is valid JSON\"),\n assertFieldExists(result.stdout, \"overallStatus\"),\n assertFieldExists(result.stdout, \"readinessState\"),\n ];\n\n // After init + patching: readiness must be ≥ missing_index (creds are set)\n try {\n const parsed = JSON.parse(result.stdout);\n const rs = parsed.readinessState as string | undefined;\n const validStates = [\"missing_index\", \"service_active\", \"ready\"];\n assertions.push({\n check: \"readinessState >= missing_index\",\n passed: !!rs && validStates.includes(rs),\n actual: rs ?? \"missing\",\n expected: \"missing_index | service_active | ready\",\n });\n } catch {\n assertions.push({ check: \"readinessState >= missing_index\", passed: false, actual: \"parse error\", expected: \"missing_index | service_active | ready\" });\n }\n\n return makeStage(\"doctor\", \"cbrain doctor --first-run --json\", result, assertions);\n}\n\n// ── Stage 3: mcp-config ──\n\nasync function runStageMcpConfig(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"mcp-config\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"mcp-config output is valid JSON\"),\n ];\n\n // Parse JSON and verify structure\n try {\n const parsed = JSON.parse(result.stdout);\n const cbrain = parsed.mcpServers?.cbrain as Record<string, unknown> | undefined;\n assertions.push({\n check: \"has mcpServers.cbrain\",\n passed: !!cbrain && typeof cbrain === \"object\",\n actual: cbrain ? \"present\" : \"missing\",\n expected: \"mcpServers.cbrain object\",\n });\n\n if (cbrain) {\n // command/args must point to installed artifact, not source checkout\n const args = cbrain.args as string[] | undefined;\n const argStr = (args ?? []).join(\" \");\n assertions.push(assertContains(argStr, ctx.tmpdir, \"CLI args point into installed package\"));\n assertions.push(assertNotContains(argStr, \"Projects/cbrain\", \"CLI args not in source checkout\"));\n\n // env must have CBRAIN_CONFIG but NOT contain credentials\n const envObj = cbrain.env as Record<string, string> | undefined;\n assertions.push(assertContains(JSON.stringify(envObj ?? {}), \"CBRAIN_CONFIG\", \"env has CBRAIN_CONFIG\"));\n assertions.push(assertNotContains(JSON.stringify(envObj ?? {}), \"gate-test-fake-key\", \"no API key in MCP config env\"));\n }\n } catch {\n assertions.push({ check: \"mcp-config JSON structure\", passed: false, actual: \"parse error\", expected: \"valid structure\" });\n }\n\n return makeStage(\"mcp-config\", \"cbrain mcp-config\", result, assertions);\n}\n\n// ── Stage 4: skill-pack ──\n\nasync function runStageSkillPack(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"skill-pack\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"skill-pack output is valid JSON\"),\n ];\n\n try {\n const parsed = JSON.parse(result.stdout);\n assertions.push({\n check: \"verificationStatus is pass\",\n passed: parsed.verificationStatus === \"pass\",\n actual: String(parsed.verificationStatus),\n expected: \"pass\",\n });\n assertions.push({\n check: \"no missing files\",\n passed: Array.isArray(parsed.missingFiles) && parsed.missingFiles.length === 0,\n actual: JSON.stringify(parsed.missingFiles),\n expected: \"[]\",\n });\n // packPath must be inside installed package, not source checkout\n const packPath = String(parsed.packPath ?? \"\");\n assertions.push(assertContains(packPath, ctx.tmpdir, \"packPath inside installed package\"));\n assertions.push(assertNotContains(packPath, \"Projects/cbrain\", \"packPath not in source checkout\"));\n } catch {\n assertions.push({ check: \"skill-pack JSON fields\", passed: false, actual: \"parse error\", expected: \"valid fields\" });\n }\n\n return makeStage(\"skill-pack\", \"cbrain skill-pack --json\", result, assertions);\n}\n\n// ── Stage 5: ingest ──\n\nasync function runStageIngest(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"ingest\", INGEST_TEXT, \"--no-ner\"], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertContains(result.stdout, \"✓ Created:\", \"page created\"),\n assertMatches(result.stdout, /records\\//, \"slug has records/ prefix\"),\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"output contains unique fixture token\"),\n assertNotContains(result.stderr, \"Error:\", \"no stderr errors\"),\n ];\n\n return makeStage(\"ingest\", 'cbrain ingest \"...\" --no-ner', result, assertions);\n}\n\n// ── Stage 6: query ──\n\nasync function runStageQuery(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"query\", QUERY_TEXT], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n {\n check: \"non-empty output\",\n passed: result.stdout.trim().length > 0,\n actual: result.stdout.trim().length > 0 ? \"has content\" : \"empty\",\n expected: \"non-empty\",\n },\n assertNotContains(result.stdout, \"没有找到相关内容\", \"results found\"),\n // Must recall the specific fixture, not just any records/ match\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"query returns our unique fixture token\"),\n ];\n\n return makeStage(\"query\", `cbrain query \"${QUERY_TEXT}\"`, result, assertions);\n}\n\n// ── Stage 7: v1.9 Migration ──\n// Creates a DB with pre-migration schema + data, then verifies current CLI can read it.\n// Assertions: schema migration ran, integrity check, FK check, idempotent reopen,\n// query returns fixture data, vault/runtime separation.\n\nasync function runStageMigration(binPath: string, ctx: IsolationContext, env: Record<string, string>, mode: EmbeddingMode): Promise<StageResult> {\n const migrationBrain = join(ctx.tmpdir, \"migration-brain\");\n const migrationVault = join(migrationBrain, \"vault\");\n const migrationDbPath = join(migrationBrain, \"brain.sqlite\");\n const migrationLancePath = join(migrationBrain, \"lancedb\");\n const migrationRuntime = join(migrationBrain, \"runtime\");\n mkdirSync(migrationRuntime, { recursive: true });\n\n // Build v1.9 fixture DB (NOT via init — raw SQL)\n buildV19FixtureDB(migrationDbPath, migrationVault);\n\n // ── Test-only fault: corrupt the fixture DB ──\n if (process.env.GATE_FAULT_MIGRATION_CORRUPT === \"1\") {\n // Overwrite DB with garbage — integrity checks must catch this\n writeFileSync(migrationDbPath, \"CORRUPTED_DB_DATA_\" + \"X\".repeat(100));\n }\n\n // Write config pointing to mock server\n const migrationConfig = {\n vaultPath: migrationVault,\n dbPath: migrationDbPath,\n lancePath: migrationLancePath,\n runtimePath: migrationRuntime,\n embedding: mode.kind === \"deterministic\"\n ? { provider: \"deterministic\", apiKey: \"gate-test-fake-key\" }\n : { provider: \"zhipu\", apiKey: \"gate-test-fake-key\", baseUrl: mode.mock.url },\n ner: { enabled: false },\n };\n const migrationConfigPath = join(migrationBrain, \"cbrain.json\");\n writeFileSync(migrationConfigPath, JSON.stringify(migrationConfig, null, 2) + \"\\n\");\n\n const migrationEnv = { ...env, CBRAIN_CONFIG: migrationConfigPath };\n\n // Doctor on migrated DB — current code runs migrations on open\n const doctorResult = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Parse doctor output — must have a valid structured status\n let doctorParsed: Record<string, unknown> | null = null;\n try { doctorParsed = JSON.parse(doctorResult.stdout); } catch { /* handled by assertions */ }\n\n // Query on migrated DB — must find the v1.9 fixture\n const queryResult = await runCli(binPath, [\"query\", V19_QUERY_TEXT], ctx, migrationEnv, 60_000);\n\n // Second doctor call — must be idempotent (same result)\n const doctor2Result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Verify vault/runtime separation on migrated brain\n const vaultOk = !existsSync(join(migrationVault, \"brain.sqlite\"))\n && !existsSync(join(migrationVault, \"lancedb\"))\n && !existsSync(join(migrationVault, \"runtime\"));\n\n // Post-migration DB integrity checks\n let integrityOk = false;\n let fkOk = false;\n let schemaVersionOk = false;\n let requiredColumnsOk = false;\n let migrationV5Ok = false;\n\n try {\n const db = new Database(migrationDbPath, { readonly: true });\n\n // PRAGMA integrity_check\n const integrity = db.prepare(\"PRAGMA integrity_check\").get() as { integrity_check: string } | undefined;\n integrityOk = integrity?.integrity_check === \"ok\";\n\n // PRAGMA foreign_key_check (returns empty if OK)\n const fkViolations = db.prepare(\"PRAGMA foreign_key_check\").all();\n fkOk = Array.isArray(fkViolations) && fkViolations.length === 0;\n\n // Schema version must exist and v5 migration must have run\n const sv = db.prepare(\"SELECT value FROM config WHERE key = 'schema_version'\").get() as { value: string } | undefined;\n schemaVersionOk = !!sv?.value;\n\n // Required post-migration columns must exist (links.weight, pages.expires_at, etc.)\n const pageCols = db.prepare(\"PRAGMA table_info(pages)\").all() as { name: string }[];\n const colNames = pageCols.map((c) => c.name);\n requiredColumnsOk = colNames.includes(\"expires_at\") && colNames.includes(\"confidence_decay\");\n\n // v5 migration marker must exist\n const v5 = db.prepare(\"SELECT value FROM config WHERE key = 'migration_v5_raw_to_records'\").get() as { value: string } | undefined;\n migrationV5Ok = v5?.value === \"1\";\n\n db.close();\n } catch {\n // DB open failure — assertions will show false\n }\n\n const assertions: AssertionResult[] = [\n {\n check: \"migration doctor exit 0 or 1\",\n passed: doctorResult.exitCode <= 1,\n actual: String(doctorResult.exitCode),\n expected: \"0 or 1\",\n },\n assertJsonParseable(doctorResult.stdout, \"migration doctor output is valid JSON\"),\n {\n check: \"migration doctor overallStatus is not error\",\n passed: !!doctorParsed && doctorParsed.overallStatus !== \"error\",\n actual: String(doctorParsed?.overallStatus ?? \"missing\"),\n expected: \"non-error status\",\n },\n {\n check: \"migration query found v1.9 fixture\",\n passed: queryResult.exitCode === 0 && queryResult.stdout.includes(\"组织D\"),\n actual: queryResult.exitCode === 0 ? (queryResult.stdout.includes(\"组织D\") ? \"found\" : \"not found\") : `exit ${queryResult.exitCode}`,\n expected: \"v1.9 fixture content found\",\n },\n {\n check: \"migration vault/runtime separation\",\n passed: vaultOk,\n actual: vaultOk ? \"separated\" : \"leaked\",\n expected: \"vault has no runtime artifacts\",\n },\n {\n check: \"PRAGMA integrity_check ok\",\n passed: integrityOk,\n actual: integrityOk ? \"ok\" : \"failed\",\n expected: \"ok\",\n },\n {\n check: \"PRAGMA foreign_key_check clean\",\n passed: fkOk,\n actual: fkOk ? \"clean\" : \"violations found\",\n expected: \"no violations\",\n },\n {\n check: \"schema_version exists in config\",\n passed: schemaVersionOk,\n actual: schemaVersionOk ? \"present\" : \"missing\",\n expected: \"present\",\n },\n {\n check: \"post-migration columns exist (expires_at, confidence_decay)\",\n passed: requiredColumnsOk,\n actual: requiredColumnsOk ? \"present\" : \"missing\",\n expected: \"columns exist\",\n },\n {\n check: \"migration_v5_raw_to_records marker present\",\n passed: migrationV5Ok,\n actual: migrationV5Ok ? \"present\" : \"missing\",\n expected: \"value '1'\",\n },\n {\n check: \"second doctor run is idempotent (exit 0)\",\n passed: doctor2Result.exitCode === 0,\n actual: String(doctor2Result.exitCode),\n expected: \"0\",\n },\n ];\n\n return makeSyntheticStage(\"migration\", assertions);\n}\n\n// ── Artifact Checks (recursive vault scan) ──\n\nfunction runArtifactChecks(ctx: IsolationContext): AssertionResult[] {\n const vaultPath = join(ctx.brainDir, \"vault\");\n const assertions: AssertionResult[] = [];\n const forbiddenPatterns = [/\\.sqlite(-wal|-shm|-journal)?$/, /^lancedb$/i, /^runtime$/i, /^outputs$/i];\n\n function scan(dir: string): void {\n if (!existsSync(dir)) return;\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n const rel = relative(vaultPath, full);\n for (const pat of forbiddenPatterns) {\n if (pat.test(entry)) {\n assertions.push({\n check: `vault has no \"${rel}\"`,\n passed: false,\n actual: \"found (bad)\",\n expected: \"not found\",\n });\n // Don't double-report\n return;\n }\n }\n try { if (statSync(full).isDirectory()) scan(full); } catch { /* skip */ }\n }\n }\n\n scan(vaultPath);\n\n if (assertions.length === 0) {\n assertions.push({ check: \"vault is clean (recursive scan)\", passed: true, actual: \"no runtime artifacts\", expected: \"clean\" });\n }\n\n return assertions;\n}\n\n// ── Report Sanitization ──\n\n/** Sanitize paths only (HOME, checkout). Used before privacy detection. */\nfunction sanitizePaths(json: string): string {\n const realHome = process.env.HOME ?? \"\";\n let sanitized = json;\n if (realHome) sanitized = sanitized.replaceAll(realHome, \"<HOME>\");\n sanitized = sanitized.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n return sanitized;\n}\n\n/** Full sanitization: paths + credential redaction. Used for final output. */\nfunction sanitizeForOutput(json: string): string {\n let sanitized = sanitizePaths(json);\n // Redact credential-like patterns — detection is not enough; output must not echo them\n sanitized = sanitized.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n return sanitized;\n}\n\n// ── Privacy Check ──\n// Runs on path-sanitized JSON (credentials NOT yet redacted so we can detect them).\n\nfunction runPrivacyCheck(pathSanitizedJson: string): AssertionResult[] {\n return [\n {\n check: \"no real HOME path in output\",\n passed: !pathSanitizedJson.includes(\"/Users/\"),\n actual: pathSanitizedJson.includes(\"/Users/\") ? \"found\" : \"clean\",\n expected: \"no real home paths\",\n },\n {\n check: \"no checkout path in output\",\n passed: !pathSanitizedJson.includes(\"Projects/cbrain\"),\n actual: pathSanitizedJson.includes(\"Projects/cbrain\") ? \"found\" : \"clean\",\n expected: \"no checkout paths\",\n },\n {\n check: \"no credential patterns in output\",\n passed: !/sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) && !pathSanitizedJson.includes(\"Bearer \"),\n actual: /sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) ? \"found\" : \"clean\",\n expected: \"no credential patterns\",\n },\n {\n check: \"no raw vector arrays in output\",\n passed: !/\"(0\\.\\d{4},){10}/.test(pathSanitizedJson),\n actual: /\"(0\\.\\d{4},){10}/.test(pathSanitizedJson) ? \"found vectors\" : \"clean\",\n expected: \"no raw vectors\",\n },\n ];\n}\n\n// ── Main Orchestrator ──\n// Returns {report, exitCode} — does NOT call process.exit().\n// ALL post-isolation work (pack, mock, stages) is inside try/catch/finally.\n// Cleanup in finally ALWAYS runs — even on pack/startup failure.\n//\n// Test-only controls (env vars):\n// GATE_PROJECT_DIR — override project dir (for isolated tests)\n// GATE_FAULT_* — inject faults at specific points\n\nasync function executeGate(): Promise<GateResult> {\n // GATE_PROJECT_DIR allows tests to point the gate at a shadow project\n // without touching the real checkout.\n const projectDir = process.env.GATE_PROJECT_DIR\n ?? resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\n const version = JSON.parse(readFileSync(join(projectDir, \"package.json\"), \"utf-8\")).version;\n const gateStart = performance.now();\n\n const ctx = createIsolation();\n let mock: MockServer | undefined;\n let embeddingMode: EmbeddingMode = { kind: \"deterministic\" };\n const stages: StageResult[] = [];\n let cleanupVerified = false;\n\n try {\n // ALL post-isolation work: pack, mock server, stage execution.\n // If ANY step throws, catch records the failure, and finally ALWAYS cleans up.\n const packResult = packAndInstall(projectDir, ctx, version);\n\n // ── Test-only fault: delete skill file after install ──\n if (process.env.GATE_FAULT_SKILL === \"1\") {\n const skillFile = join(packResult.pkgDir, \"skills\", \"SKILL.md\");\n if (existsSync(skillFile)) rmSync(skillFile, { force: true });\n }\n\n // (#204) Resolve embedding transport: HTTP mock (preferred) or in-process\n // deterministic fallback when a localhost TCP listener cannot bind.\n embeddingMode = await resolveEmbeddingMode();\n if (embeddingMode.kind === \"http\") {\n mock = embeddingMode.mock;\n }\n\n let env = buildIsolatedEnv(ctx);\n\n // ── Test-only fault: block init with pre-existing file ──\n if (process.env.GATE_FAULT_INIT === \"1\") {\n writeFileSync(ctx.brainDir, \"block-init\");\n }\n\n // Stage 1: init\n const initStage = await runStageInit(packResult.binPath, ctx, env);\n stages.push(initStage);\n\n // Subsequent stages only if init passed\n if (initStage.passed) {\n patchConfig(ctx.configPath, embeddingMode);\n env = buildIsolatedEnv(ctx);\n\n stages.push(await runStageDoctor(packResult.binPath, ctx, env));\n stages.push(await runStageMcpConfig(packResult.binPath, ctx, env));\n stages.push(await runStageSkillPack(packResult.binPath, ctx, env));\n\n // ── Test-only fault: skip ingest → empty brain query ──\n if (process.env.GATE_FAULT_SKIP_INGEST !== \"1\") {\n stages.push(await runStageIngest(packResult.binPath, ctx, env));\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n } else {\n // Run query on empty brain — should return no results\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n }\n\n stages.push(await runStageMigration(packResult.binPath, ctx, env, embeddingMode));\n }\n\n // ── Test-only fault: create runtime/ inside vault → artifacts fail ──\n if (process.env.GATE_FAULT_VAULT_NESTED === \"1\") {\n const nestedRuntime = join(ctx.brainDir, \"vault\", \"runtime\");\n mkdirSync(nestedRuntime, { recursive: true });\n writeFileSync(join(nestedRuntime, \"leak.txt\"), \"should not exist\");\n }\n\n // Artifact checks\n stages.push(makeSyntheticStage(\"artifacts\", runArtifactChecks(ctx)));\n } catch (e) {\n stages.push(\n makeSyntheticStage(\"fatal\", [\n {\n check: \"no unhandled errors\",\n passed: false,\n actual: e instanceof Error ? e.message : String(e),\n expected: \"no errors\",\n },\n ]),\n );\n } finally {\n // ── Cleanup (ALWAYS runs — even if catch itself throws) ──\n try {\n mock?.stop();\n } catch {\n /* best effort */\n }\n\n // Tarball is inside ctx.tmpdir — cleaned up with it.\n // Pre-existing tarball was already restored by packAndInstall's inner finally.\n\n if (existsSync(ctx.tmpdir)) {\n try {\n rmSync(ctx.tmpdir, { recursive: true });\n cleanupVerified = !existsSync(ctx.tmpdir);\n } catch {\n cleanupVerified = false;\n }\n } else {\n cleanupVerified = true;\n }\n }\n\n // ── Build report ──\n const allStagesPassed = stages.every((s) => s.passed);\n const rawReport: GateReport = {\n gate: \"first-recall-v2\",\n version,\n timestamp: new Date().toISOString(),\n verdict: allStagesPassed && cleanupVerified ? \"go\" : \"no-go\",\n embedding_mode: embeddingMode.kind,\n stages,\n cleanup: { verified: cleanupVerified, path: cleanupVerified ? \"<cleaned>\" : \"<retained>\" },\n duration_ms: Math.round(performance.now() - gateStart),\n };\n\n // ── Test-only fault: inject credential into report for privacy check ──\n // This tests that the gate's privacy + redaction pipeline works end-to-end:\n // detection → redaction → no-go verdict → clean output.\n if (process.env.GATE_FAULT_MCP_CRED_LEAK === \"1\") {\n const mcpIdx = rawReport.stages.findIndex((s) => s.id === \"mcp-config\");\n if (mcpIdx >= 0) {\n const stage = rawReport.stages[mcpIdx];\n rawReport.stages[mcpIdx] = {\n ...stage,\n assertions: [\n ...stage.assertions,\n {\n check: \"simulated credential leak test\",\n passed: true,\n actual: \"sk-abcdef1234567890abcdef1234567890\",\n expected: \"no credentials in output\",\n },\n ],\n };\n }\n }\n\n // Privacy detection on path-sanitized JSON (credentials NOT yet redacted)\n const pathSanitized = sanitizePaths(JSON.stringify(rawReport, null, 2));\n const privacyAssertions = runPrivacyCheck(pathSanitized);\n const privacyPassed = privacyAssertions.every((a) => a.passed);\n\n // Full sanitization for output: paths + credential redaction\n const fullySanitized = sanitizeForOutput(JSON.stringify(rawReport, null, 2));\n\n let finalReport: GateReport;\n let finalExitCode: number;\n\n if (!privacyPassed) {\n const parsed = JSON.parse(fullySanitized);\n parsed.stages = [...parsed.stages, makeSyntheticStage(\"privacy\", privacyAssertions)];\n parsed.verdict = \"no-go\";\n finalReport = parsed as GateReport;\n finalExitCode = 1;\n } else {\n finalReport = JSON.parse(fullySanitized) as GateReport;\n finalExitCode = rawReport.verdict === \"go\" ? 0 : 1;\n }\n\n return { report: finalReport, exitCode: finalExitCode };\n}\n\n// ── Top-level: run gate → emit report → set exit code ──\n// process.exitCode (not process.exit) lets the runtime clean up naturally.\n// Outer catch handles pre-isolation fatal errors only — post-isolation errors\n// are handled by try/catch/finally inside executeGate().\n\nexecuteGate().then(({ report, exitCode }) => {\n process.stdout.write(JSON.stringify(report, null, 2) + \"\\n\");\n process.exitCode = exitCode;\n}).catch((e) => {\n // Sanitize the fatal error — never echo real paths/stacks/secrets\n let errorDetail = e instanceof Error ? e.message : String(e);\n const realHome = process.env.HOME ?? \"\";\n if (realHome) errorDetail = errorDetail.replaceAll(realHome, \"<HOME>\");\n errorDetail = errorDetail.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n // Strip any remaining absolute user paths\n errorDetail = errorDetail.replace(/\\/Users\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/\\/home\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n // Strip stack traces — only keep the first line\n errorDetail = errorDetail.split(\"\\n\")[0];\n\n const errorReport = {\n gate: \"first-recall-v2\",\n verdict: \"no-go\",\n error: errorDetail,\n cleanup: { verified: false, path: \"<unknown>\" },\n };\n process.stdout.write(JSON.stringify(errorReport, null, 2) + \"\\n\");\n process.exitCode = 2;\n});\n"}, "4eca70f15e327436f6c94dc915e4fd73eefda0c2": {"t": 1785759165.159609, "n": 44327, "h": "84bef9c030392969", "c": "#!/usr/bin/env bun\n// check-first-recall-gate.ts — Offline first-install-to-first-recall v2.0 release gate\n//\n// Usage: bun bin/check-first-recall-gate.ts\n//\n// Proves the packed CLI artifact works end-to-end:\n// init → doctor → mcp-config → skill-pack → ingest → query → migration\n//\n// Fully offline (uses checkout node_modules via symlink). Mock embedding server on localhost.\n// Fully isolated environment. Verified cleanup on ALL paths.\n//\n// Production dependency install is verified separately by: bun run check:install-network\n// v2 release requires BOTH gates to pass.\n//\n// Test-only controls (env vars, ignored in production):\n// GATE_PROJECT_DIR=<path> — override project dir (for shadow-project tests)\n// GATE_FAULT_PROVIDER=1 — use unreachable mock URL → ingest/query fail\n// GATE_FAULT_SKIP_INGEST=1 — skip ingest stage → query on empty brain\n// GATE_FAULT_SKILL=1 — delete SKILL.md → skill-pack fails\n// GATE_FAULT_INIT=1 — block init with pre-existing file → init fails\n// GATE_FAULT_VAULT_NESTED=1 — create runtime/ inside vault → artifacts fail\n// GATE_FAULT_MIGRATION_CORRUPT=1 — corrupt migration fixture DB → migration fails\n// GATE_FAULT_MCP_CRED_LEAK=1 — inject credential into report → privacy fail\n// GATE_FORCE_DETERMINISTIC=1 — skip TCP, use in-process deterministic embedding (#204)\n//\n// Exit codes: 0 = go, 1 = no-go, 2 = fatal error (script bug)\n\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n symlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, resolve, dirname, relative } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { createServer } from \"node:net\";\nimport { fileURLToPath } from \"url\";\nimport { Database } from \"bun:sqlite\";\n\n// ── Types ──\n\ntype GateVerdict = \"go\" | \"no-go\";\n\ninterface AssertionResult {\n readonly check: string;\n readonly passed: boolean;\n readonly actual: string;\n readonly expected: string;\n}\n\ninterface StageResult {\n readonly id: string;\n readonly command: string;\n readonly exitCode: number;\n readonly duration_ms: number;\n readonly passed: boolean;\n readonly assertions: ReadonlyArray<AssertionResult>;\n}\n\ninterface GateReport {\n readonly gate: \"first-recall-v2\";\n readonly version: string;\n readonly timestamp: string;\n readonly verdict: GateVerdict;\n readonly embedding_mode: \"http\" | \"deterministic\";\n readonly stages: ReadonlyArray<StageResult>;\n readonly cleanup: { readonly verified: boolean; readonly path: string };\n readonly duration_ms: number;\n}\n\ninterface GateResult {\n readonly report: GateReport;\n readonly exitCode: number;\n}\n\ninterface IsolationContext {\n readonly tmpdir: string;\n readonly homeDir: string;\n readonly bunInstallDir: string;\n readonly brainDir: string;\n configPath: string;\n readonly cwd: string;\n}\n\ninterface PackResult {\n readonly tarballPath: string;\n readonly pkgDir: string;\n readonly binPath: string;\n}\n\ninterface MockServer {\n readonly url: string;\n readonly port: number;\n stop(): void;\n}\n\n// (#204) How the packed CLI obtains embeddings. http = local mock server\n// (preferred, issue requirement); deterministic = in-process provider when a\n// localhost TCP listener cannot bind. Neither skips the mock — both are mocks.\ntype EmbeddingMode =\n | { readonly kind: \"http\"; readonly mock: MockServer }\n | { readonly kind: \"deterministic\" };\n\ninterface CliResult {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n readonly duration_ms: number;\n}\n\n// ── Constants ──\n\nconst INGEST_TEXT =\n \"实体A是一个重要的概念,它与主题B密切相关。方法C是解决这个问题的关键技术路径。\";\nconst INGEST_UNIQUE_TOKEN = \"实体a\"; // appears in auto-generated slug\nconst QUERY_TEXT = \"实体A 主题B 方法C\"; // includes unique token for deterministic recall\nconst VECTOR_DIM = 2048;\nconst DETERMINISTIC_VECTOR = Array.from({ length: VECTOR_DIM }, (_, i) =>\n Math.sin(i * 0.001) * 0.5,\n);\n\n// ── Mock Embedding Server ──\n\n// (#204) Probe a free localhost port via Node net rather than relying on\n// Bun.serve's port:0 auto-allocation, which some environments/Bun versions\n// misreport as \"Failed to start server. Is port 0 in use?\". Resolves to a\n// concrete port number handed to Bun.serve.\nfunction findFreeLocalhostPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n probe.unref();\n probe.on(\"error\", () => {\n reject(new Error(\"mock embedding server failed to bind a localhost port\"));\n });\n probe.listen(0, \"127.0.0.1\", () => {\n const addr = probe.address();\n const port = typeof addr === \"object\" && addr !== null ? addr.port : 0;\n probe.close(() => {\n if (port > 0) resolve(port);\n else reject(new Error(\"mock embedding server failed to bind a localhost port\"));\n });\n });\n });\n}\n\n// (#204) Deterministic, retryable localhost port selection. Probes a free port,\n// binds Bun.serve to it, and retries with a fresh probe on transient failure.\n// A startup failure surfaces a single sanitized message (no paths/stack/secrets)\n// so the release gate stays diagnosable without leaking environment detail.\nasync function startMockEmbeddingServer(): Promise<MockServer> {\n const MAX_ATTEMPTS = 3;\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n const port = await findFreeLocalhostPort();\n const served = Bun.serve({\n port,\n hostname: \"127.0.0.1\",\n fetch(req) {\n const url = new URL(req.url);\n if (url.pathname === \"/embeddings\" && req.method === \"POST\") {\n return req.json().then((body: { input?: string[] }) => {\n const inputs = body.input ?? [];\n const data = inputs.map((_, i) => ({\n embedding: DETERMINISTIC_VECTOR,\n index: i,\n }));\n return Response.json({\n data,\n usage: { total_tokens: inputs.length * 10 },\n });\n });\n }\n return new Response(\"NOT_FOUND\", { status: 404 });\n },\n });\n return {\n url: `http://127.0.0.1:${served.port}`,\n port: served.port,\n stop() {\n served.stop();\n },\n };\n } catch {\n // Port probe or Bun.serve failed — retry with a fresh localhost port.\n }\n }\n throw new Error(\"mock embedding server failed to bind a localhost port\");\n}\n\n// (#204) Resolve how the packed CLI obtains embeddings.\n// Preferred: HTTP mock server on localhost (issue requires a local mock server).\n// If a TCP listener cannot bind (some environments reject localhost bind on both\n// Bun.serve and node:net), fall back to the in-process deterministic provider\n// (embedding.provider=\"deterministic\") — no socket, no HTTP, no creds. This is\n// NOT a pass downgrade: embeddings still work and ingest+query still run.\nasync function resolveEmbeddingMode(): Promise<EmbeddingMode> {\n // ── Test-only: force the deterministic path (simulates a TCP-less env) ──\n if (process.env.GATE_FORCE_DETERMINISTIC === \"1\") {\n return { kind: \"deterministic\" };\n }\n // ── Test-only fault: unreachable provider URL → ingest/query fail → no-go ──\n if (process.env.GATE_FAULT_PROVIDER === \"1\") {\n return { kind: \"http\", mock: { url: \"http://127.0.0.1:1\", port: 1, stop() {} } };\n }\n try {\n return { kind: \"http\", mock: await startMockEmbeddingServer() };\n } catch {\n // TCP listener unavailable — use in-process deterministic embeddings.\n return { kind: \"deterministic\" };\n }\n}\n\n// ── Isolation ──\n\nfunction createIsolation(): IsolationContext {\n const base = mkdtempSync(join(tmpdir(), \"cbrain-gate-\"));\n const homeDir = join(base, \"home\");\n const bunInstallDir = join(base, \"bun\");\n const brainDir = join(base, \"brain\");\n const cwd = join(base, \"cwd\");\n\n mkdirSync(homeDir, { recursive: true });\n mkdirSync(bunInstallDir, { recursive: true });\n mkdirSync(cwd, { recursive: true });\n\n return { tmpdir: base, homeDir, bunInstallDir, brainDir, configPath: \"\", cwd };\n}\n\n// Build a CLEAN env for subprocesses — allowlist only, no inherited secrets.\nfunction buildIsolatedEnv(\n ctx: IsolationContext,\n extraEnv: Record<string, string> = {},\n): Record<string, string> {\n // Only inherit truly safe system vars\n const safeKeys = [\"LANG\", \"LC_ALL\", \"LC_CTYPE\", \"TERM\", \"TMPDIR\", \"TEMP\", \"TMP\", \"__CF_USER_TEXT_ENCODING\"];\n const inherited: Record<string, string> = {};\n for (const key of safeKeys) {\n if (process.env[key]) inherited[key] = process.env[key]!;\n }\n\n return {\n ...inherited,\n // PATH: need bun + system tools, but no cbrain bin\n PATH: `${ctx.bunInstallDir}/bin:${process.env.PATH ?? \"\"}`.split(\":\")\n .filter((p) => !p.includes(\"cbrain\") || p.includes(\"cbrain-gate\"))\n .join(\":\"),\n HOME: ctx.homeDir,\n BUN_INSTALL: ctx.bunInstallDir,\n XDG_CONFIG_HOME: join(ctx.homeDir, \".config\"),\n XDG_DATA_HOME: join(ctx.homeDir, \".local\", \"share\"),\n XDG_CACHE_HOME: join(ctx.homeDir, \".cache\"),\n CBRAIN_CONFIG: ctx.configPath,\n ZHIPU_API_KEY: \"gate-test-fake-key\",\n // Explicitly clear any inherited provider vars\n ...(process.env.DEEPSEEK_API_KEY ? { DEEPSEEK_API_KEY: \"\" } : {}),\n ...extraEnv,\n };\n}\n\n// ── Pack + Offline Install ──\n// Uses checkout node_modules via symlink — fully offline, no network needed.\n// Production dependency install is verified by check:install-network separately.\n\nfunction packAndInstall(projectDir: string, ctx: IsolationContext, version: string): PackResult {\n // Verify checkout has deps\n if (!existsSync(join(projectDir, \"node_modules\"))) {\n throw new Error(\"node_modules not found in checkout — run bun install first\");\n }\n\n // Deterministic tarball name: cbrain-{version}.tgz\n const tarballName = `cbrain-${version}.tgz`;\n const tarballInProject = join(projectDir, tarballName);\n let backupPath: string | null = null;\n\n // Backup pre-existing same-version tarball (if any) — restored in finally\n if (existsSync(tarballInProject)) {\n backupPath = `${tarballInProject}.gate-backup`;\n renameSync(tarballInProject, backupPath);\n }\n\n try {\n // Pack — always creates cbrain-{version}.tgz in project dir\n execSync(\"bun pm pack\", { cwd: projectDir, encoding: \"utf-8\", timeout: 30_000 });\n\n if (!existsSync(tarballInProject)) {\n throw new Error(\"bun pm pack produced no tarball\");\n }\n\n // Move tarball to isolated dir — never leaves artifacts in project dir\n const packDir = join(ctx.tmpdir, \"pack\");\n mkdirSync(packDir, { recursive: true });\n const isolatedTarball = join(packDir, tarballName);\n renameSync(tarballInProject, isolatedTarball);\n\n // Extract\n execSync(`tar -xzf \"${isolatedTarball}\" -C \"${packDir}\"`, { encoding: \"utf-8\" });\n\n // Find package dir (the extracted directory, not our tarball)\n const entries = readdirSync(packDir).filter((e) => e !== tarballName);\n if (entries.length === 0) throw new Error(\"tarball was empty\");\n const pkgDir = join(packDir, entries[0]);\n\n // OFFLINE: symlink checkout node_modules — no network needed.\n // Production dependency install is proven by check:install-network separately.\n symlinkSync(join(projectDir, \"node_modules\"), join(pkgDir, \"node_modules\"));\n\n return {\n tarballPath: isolatedTarball,\n pkgDir,\n binPath: join(pkgDir, \"src\", \"cli\", \"index.ts\"),\n };\n } finally {\n // Always restore pre-existing tarball — even on failure\n if (backupPath && existsSync(backupPath)) {\n renameSync(backupPath, tarballInProject);\n }\n }\n}\n\n// ── CLI Runner (async — Bun.spawn does NOT block event loop) ──\n\nasync function runCli(\n binPath: string,\n args: string[],\n ctx: IsolationContext,\n env: Record<string, string>,\n timeoutMs = 30_000,\n): Promise<CliResult> {\n const start = performance.now();\n try {\n const proc = Bun.spawn([process.execPath, \"run\", binPath, ...args], {\n cwd: ctx.cwd,\n env,\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n\n // Race process exit vs timeout — always clear the timer\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timeoutHandle = setTimeout(() => {\n proc.kill();\n reject(new Error(`TIMEOUT after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n let exitCode: number;\n try {\n exitCode = await Promise.race([proc.exited, timeoutPromise]);\n } finally {\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n }\n\n const [stdout, stderr] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n ]);\n\n return { exitCode, stdout, stderr, duration_ms: Math.round(performance.now() - start) };\n } catch (e) {\n return {\n exitCode: 1,\n stdout: \"\",\n stderr: e instanceof Error ? e.message : String(e),\n duration_ms: Math.round(performance.now() - start),\n };\n }\n}\n\n// ── Assertion Helpers ──\n// Every stage uses assertions.every(a => a.passed) for its verdict — no exceptions.\n\nfunction assertExitCode(result: CliResult, expected: number): AssertionResult {\n return {\n check: `exit code ${expected}`,\n passed: result.exitCode === expected,\n actual: String(result.exitCode),\n expected: String(expected),\n };\n}\n\nfunction assertExitCodeOkOrWarn(result: CliResult): AssertionResult {\n return {\n check: \"exit code 0 or 1 (warn allowed)\",\n passed: result.exitCode <= 1,\n actual: String(result.exitCode),\n expected: \"0 or 1\",\n };\n}\n\nfunction assertJsonField(stdout: string, field: string, expected: unknown): AssertionResult {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: false, actual: \"JSON parse failed\", expected: String(expected) };\n }\n const actual = parsed[field];\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: actual === expected, actual: String(actual), expected: String(expected) };\n}\n\nfunction assertJsonParseable(stdout: string, label: string): AssertionResult {\n try {\n JSON.parse(stdout);\n return { check: label, passed: true, actual: \"valid JSON\", expected: \"valid JSON\" };\n } catch {\n return { check: label, passed: false, actual: \"invalid JSON\", expected: \"valid JSON\" };\n }\n}\n\nfunction assertFieldExists(stdout: string, field: string): AssertionResult {\n let parsed: Record<string, unknown>;\n try { parsed = JSON.parse(stdout); } catch {\n return { check: `json has \"${field}\"`, passed: false, actual: \"JSON parse failed\", expected: `field \"${field}\" exists` };\n }\n const val = parsed[field];\n return {\n check: `json has \"${field}\"`,\n passed: val !== undefined && val !== null && String(val).length > 0,\n actual: val === undefined ? \"undefined\" : val === null ? \"null\" : String(val).slice(0, 80),\n expected: `non-empty \"${field}\"`,\n };\n}\n\nfunction assertContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: haystack.includes(needle), actual: haystack.includes(needle) ? \"found\" : \"not found\", expected: `contains \"${needle}\"` };\n}\n\nfunction assertNotContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: !haystack.includes(needle), actual: haystack.includes(needle) ? \"found (bad)\" : \"not found (good)\", expected: `does not contain \"${needle}\"` };\n}\n\nfunction assertMatches(text: string, pattern: RegExp, label: string): AssertionResult {\n return { check: label, passed: pattern.test(text), actual: pattern.test(text) ? \"matched\" : \"no match\", expected: `matches ${pattern}` };\n}\n\n/** Derive stage passed from ALL assertions — the single source of truth. */\nfunction verdict(assertions: ReadonlyArray<AssertionResult>): boolean {\n return assertions.every((a) => a.passed);\n}\n\nfunction makeStage(\n id: string,\n command: string,\n result: CliResult,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command, exitCode: result.exitCode, duration_ms: result.duration_ms, passed: verdict(assertions), assertions };\n}\n\nfunction makeSyntheticStage(\n id: string,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command: \"(internal check)\", exitCode: verdict(assertions) ? 0 : 1, duration_ms: 0, passed: verdict(assertions), assertions };\n}\n\n// ── Config Patching ──\n\nfunction patchConfig(configPath: string, mode: EmbeddingMode): void {\n const config = JSON.parse(readFileSync(configPath, \"utf-8\"));\n if (mode.kind === \"deterministic\") {\n // (#204) In-process deterministic embeddings: no HTTP server, no TCP, no creds.\n config.embedding.provider = \"deterministic\";\n config.embedding.apiKey = \"gate-test-fake-key\";\n delete config.embedding.baseUrl;\n } else {\n config.embedding.provider = \"zhipu\";\n config.embedding.baseUrl = mode.mock.url;\n config.embedding.apiKey = \"gate-test-fake-key\";\n }\n config.ner = { enabled: false };\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n}\n\n// ── v1.9 Migration Fixture Builder ──\n// Creates a minimal SQLite DB with pre-migration schema + data.\n// Does NOT use `cbrain init` — schema is built with raw SQL.\n\nconst V19_FIXTURE_SLUG = \"records/v1-9-fixture\";\nconst V19_FIXTURE_TITLE = \"v1.9迁移测试记录\";\nconst V19_FIXTURE_BODY = \"组织D在主题B中采用了方法C的核心策略。这段记录来自模拟的v1.9安装。\";\nconst V19_QUERY_TEXT = \"组织D 主题B\";\n\nfunction buildV19FixtureDB(dbPath: string, vaultPath: string): void {\n mkdirSync(dirname(dbPath), { recursive: true });\n mkdirSync(join(vaultPath, \"records\"), { recursive: true });\n for (const sub of [\"entities\", \"concepts\", \"insights\"]) {\n mkdirSync(join(vaultPath, \"brain\", sub), { recursive: true });\n }\n\n // Create DB with v1.9-era base schema (before migrations)\n const db = new Database(dbPath);\n db.exec(\"PRAGMA journal_mode = WAL\");\n\n // Base tables — minimal v1.9 schema\n db.exec(`CREATE TABLE IF NOT EXISTS pages (\n slug TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n file_path TEXT NOT NULL,\n content_hash TEXT,\n tier INTEGER DEFAULT 3,\n mention_count INTEGER DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n )`);\n db.exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);\n db.exec(`CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n page_slug TEXT NOT NULL,\n chunk_index INTEGER NOT NULL,\n content TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(page_slug, chunk_index)\n )`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(page_slug, content, tokenize='trigram')`);\n db.exec(`CREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_slug TEXT NOT NULL,\n to_slug TEXT NOT NULL,\n relation TEXT NOT NULL DEFAULT 'mentions',\n context TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(from_slug, to_slug, relation)\n )`);\n\n // Insert fixture data\n db.run(\"INSERT INTO pages (slug, type, title, file_path) VALUES (?, ?, ?, ?)\",\n [V19_FIXTURE_SLUG, \"record\", V19_FIXTURE_TITLE, `${V19_FIXTURE_SLUG}.md`]);\n db.run(\"INSERT INTO chunks (page_slug, chunk_index, content) VALUES (?, 0, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO chunks_fts (page_slug, content) VALUES (?, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO config (key, value) VALUES ('schema_version', '1')\");\n\n db.close();\n\n // Create vault markdown file\n const mdPath = join(vaultPath, `${V19_FIXTURE_SLUG}.md`);\n mkdirSync(dirname(mdPath), { recursive: true });\n writeFileSync(mdPath, `---\\ntitle: \"${V19_FIXTURE_TITLE}\"\\ntype: record\\n---\\n\\n${V19_FIXTURE_BODY}\\n`);\n}\n\n// ── Stage 1: init ──\n\nasync function runStageInit(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"init\", \"--dir\", ctx.brainDir, \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"init output is valid JSON\"),\n assertJsonField(result.stdout, \"status\", \"ok\"),\n assertJsonField(result.stdout, \"created\", true),\n ];\n\n // Extract configPath and verify it's inside tmpdir\n try {\n const parsed = JSON.parse(result.stdout);\n const cp = parsed.configPath;\n if (typeof cp === \"string\" && cp.length > 0) {\n ctx.configPath = cp;\n assertions.push(assertContains(cp, ctx.tmpdir, \"config path inside tmpdir\"));\n } else {\n assertions.push({ check: \"configPath present\", passed: false, actual: String(cp), expected: \"non-empty string\" });\n }\n } catch {\n assertions.push({ check: \"configPath present\", passed: false, actual: \"JSON parse failed\", expected: \"non-empty string\" });\n }\n\n return makeStage(\"init\", \"cbrain init --dir <brain> --json\", result, assertions);\n}\n\n// ── Stage 2: doctor --first-run ──\n\nasync function runStageDoctor(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCodeOkOrWarn(result),\n assertJsonParseable(result.stdout, \"doctor output is valid JSON\"),\n assertFieldExists(result.stdout, \"overallStatus\"),\n assertFieldExists(result.stdout, \"readinessState\"),\n ];\n\n // After init + patching: readiness must be ≥ missing_index (creds are set)\n try {\n const parsed = JSON.parse(result.stdout);\n const rs = parsed.readinessState as string | undefined;\n const validStates = [\"missing_index\", \"service_active\", \"ready\"];\n assertions.push({\n check: \"readinessState >= missing_index\",\n passed: !!rs && validStates.includes(rs),\n actual: rs ?? \"missing\",\n expected: \"missing_index | service_active | ready\",\n });\n } catch {\n assertions.push({ check: \"readinessState >= missing_index\", passed: false, actual: \"parse error\", expected: \"missing_index | service_active | ready\" });\n }\n\n return makeStage(\"doctor\", \"cbrain doctor --first-run --json\", result, assertions);\n}\n\n// ── Stage 3: mcp-config ──\n\nasync function runStageMcpConfig(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"mcp-config\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"mcp-config output is valid JSON\"),\n ];\n\n // Parse JSON and verify structure\n try {\n const parsed = JSON.parse(result.stdout);\n const cbrain = parsed.mcpServers?.cbrain as Record<string, unknown> | undefined;\n assertions.push({\n check: \"has mcpServers.cbrain\",\n passed: !!cbrain && typeof cbrain === \"object\",\n actual: cbrain ? \"present\" : \"missing\",\n expected: \"mcpServers.cbrain object\",\n });\n\n if (cbrain) {\n // command/args must point to installed artifact, not source checkout\n const args = cbrain.args as string[] | undefined;\n const argStr = (args ?? []).join(\" \");\n assertions.push(assertContains(argStr, ctx.tmpdir, \"CLI args point into installed package\"));\n assertions.push(assertNotContains(argStr, \"Projects/cbrain\", \"CLI args not in source checkout\"));\n\n // env must have CBRAIN_CONFIG but NOT contain credentials\n const envObj = cbrain.env as Record<string, string> | undefined;\n assertions.push(assertContains(JSON.stringify(envObj ?? {}), \"CBRAIN_CONFIG\", \"env has CBRAIN_CONFIG\"));\n assertions.push(assertNotContains(JSON.stringify(envObj ?? {}), \"gate-test-fake-key\", \"no API key in MCP config env\"));\n }\n } catch {\n assertions.push({ check: \"mcp-config JSON structure\", passed: false, actual: \"parse error\", expected: \"valid structure\" });\n }\n\n return makeStage(\"mcp-config\", \"cbrain mcp-config\", result, assertions);\n}\n\n// ── Stage 4: skill-pack ──\n\nasync function runStageSkillPack(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"skill-pack\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"skill-pack output is valid JSON\"),\n ];\n\n try {\n const parsed = JSON.parse(result.stdout);\n assertions.push({\n check: \"verificationStatus is pass\",\n passed: parsed.verificationStatus === \"pass\",\n actual: String(parsed.verificationStatus),\n expected: \"pass\",\n });\n assertions.push({\n check: \"no missing files\",\n passed: Array.isArray(parsed.missingFiles) && parsed.missingFiles.length === 0,\n actual: JSON.stringify(parsed.missingFiles),\n expected: \"[]\",\n });\n // packPath must be inside installed package, not source checkout\n const packPath = String(parsed.packPath ?? \"\");\n assertions.push(assertContains(packPath, ctx.tmpdir, \"packPath inside installed package\"));\n assertions.push(assertNotContains(packPath, \"Projects/cbrain\", \"packPath not in source checkout\"));\n } catch {\n assertions.push({ check: \"skill-pack JSON fields\", passed: false, actual: \"parse error\", expected: \"valid fields\" });\n }\n\n return makeStage(\"skill-pack\", \"cbrain skill-pack --json\", result, assertions);\n}\n\n// ── Stage 5: ingest ──\n\nasync function runStageIngest(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"ingest\", INGEST_TEXT, \"--no-ner\"], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertContains(result.stdout, \"✓ Created:\", \"page created\"),\n assertMatches(result.stdout, /records\\//, \"slug has records/ prefix\"),\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"output contains unique fixture token\"),\n assertNotContains(result.stderr, \"Error:\", \"no stderr errors\"),\n ];\n\n return makeStage(\"ingest\", 'cbrain ingest \"...\" --no-ner', result, assertions);\n}\n\n// ── Stage 6: query ──\n\nasync function runStageQuery(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"query\", QUERY_TEXT], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n {\n check: \"non-empty output\",\n passed: result.stdout.trim().length > 0,\n actual: result.stdout.trim().length > 0 ? \"has content\" : \"empty\",\n expected: \"non-empty\",\n },\n assertNotContains(result.stdout, \"没有找到相关内容\", \"results found\"),\n // Must recall the specific fixture, not just any records/ match\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"query returns our unique fixture token\"),\n ];\n\n return makeStage(\"query\", `cbrain query \"${QUERY_TEXT}\"`, result, assertions);\n}\n\n// ── Stage 7: v1.9 Migration ──\n// Creates a DB with pre-migration schema + data, then verifies current CLI can read it.\n// Assertions: schema migration ran, integrity check, FK check, idempotent reopen,\n// query returns fixture data, vault/runtime separation.\n\nasync function runStageMigration(binPath: string, ctx: IsolationContext, env: Record<string, string>, mode: EmbeddingMode): Promise<StageResult> {\n const migrationBrain = join(ctx.tmpdir, \"migration-brain\");\n const migrationVault = join(migrationBrain, \"vault\");\n const migrationDbPath = join(migrationBrain, \"brain.sqlite\");\n const migrationLancePath = join(migrationBrain, \"lancedb\");\n const migrationRuntime = join(migrationBrain, \"runtime\");\n mkdirSync(migrationRuntime, { recursive: true });\n\n // Build v1.9 fixture DB (NOT via init — raw SQL)\n buildV19FixtureDB(migrationDbPath, migrationVault);\n\n // ── Test-only fault: corrupt the fixture DB ──\n if (process.env.GATE_FAULT_MIGRATION_CORRUPT === \"1\") {\n // Overwrite DB with garbage — integrity checks must catch this\n writeFileSync(migrationDbPath, \"CORRUPTED_DB_DATA_\" + \"X\".repeat(100));\n }\n\n // Write config pointing to mock server\n const migrationConfig = {\n vaultPath: migrationVault,\n dbPath: migrationDbPath,\n lancePath: migrationLancePath,\n runtimePath: migrationRuntime,\n embedding: mode.kind === \"deterministic\"\n ? { provider: \"deterministic\", apiKey: \"gate-test-fake-key\" }\n : { provider: \"zhipu\", apiKey: \"gate-test-fake-key\", baseUrl: mode.mock.url },\n ner: { enabled: false },\n };\n const migrationConfigPath = join(migrationBrain, \"cbrain.json\");\n writeFileSync(migrationConfigPath, JSON.stringify(migrationConfig, null, 2) + \"\\n\");\n\n const migrationEnv = { ...env, CBRAIN_CONFIG: migrationConfigPath };\n\n // Doctor on migrated DB — current code runs migrations on open\n const doctorResult = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Parse doctor output — must have a valid structured status\n let doctorParsed: Record<string, unknown> | null = null;\n try { doctorParsed = JSON.parse(doctorResult.stdout); } catch { /* handled by assertions */ }\n\n // Query on migrated DB — must find the v1.9 fixture\n const queryResult = await runCli(binPath, [\"query\", V19_QUERY_TEXT], ctx, migrationEnv, 60_000);\n\n // Second doctor call — must be idempotent (same result)\n const doctor2Result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Verify vault/runtime separation on migrated brain\n const vaultOk = !existsSync(join(migrationVault, \"brain.sqlite\"))\n && !existsSync(join(migrationVault, \"lancedb\"))\n && !existsSync(join(migrationVault, \"runtime\"));\n\n // Post-migration DB integrity checks\n let integrityOk = false;\n let fkOk = false;\n let schemaVersionOk = false;\n let requiredColumnsOk = false;\n let migrationV5Ok = false;\n\n try {\n const db = new Database(migrationDbPath, { readonly: true });\n\n // PRAGMA integrity_check\n const integrity = db.prepare(\"PRAGMA integrity_check\").get() as { integrity_check: string } | undefined;\n integrityOk = integrity?.integrity_check === \"ok\";\n\n // PRAGMA foreign_key_check (returns empty if OK)\n const fkViolations = db.prepare(\"PRAGMA foreign_key_check\").all();\n fkOk = Array.isArray(fkViolations) && fkViolations.length === 0;\n\n // Schema version must exist and v5 migration must have run\n const sv = db.prepare(\"SELECT value FROM config WHERE key = 'schema_version'\").get() as { value: string } | undefined;\n schemaVersionOk = !!sv?.value;\n\n // Required post-migration columns must exist (links.weight, pages.expires_at, etc.)\n const pageCols = db.prepare(\"PRAGMA table_info(pages)\").all() as { name: string }[];\n const colNames = pageCols.map((c) => c.name);\n requiredColumnsOk = colNames.includes(\"expires_at\") && colNames.includes(\"confidence_decay\");\n\n // v5 migration marker must exist\n const v5 = db.prepare(\"SELECT value FROM config WHERE key = 'migration_v5_raw_to_records'\").get() as { value: string } | undefined;\n migrationV5Ok = v5?.value === \"1\";\n\n db.close();\n } catch {\n // DB open failure — assertions will show false\n }\n\n const assertions: AssertionResult[] = [\n {\n check: \"migration doctor exit 0 or 1\",\n passed: doctorResult.exitCode <= 1,\n actual: String(doctorResult.exitCode),\n expected: \"0 or 1\",\n },\n assertJsonParseable(doctorResult.stdout, \"migration doctor output is valid JSON\"),\n {\n check: \"migration doctor overallStatus is not error\",\n passed: !!doctorParsed && doctorParsed.overallStatus !== \"error\",\n actual: String(doctorParsed?.overallStatus ?? \"missing\"),\n expected: \"non-error status\",\n },\n {\n check: \"migration query found v1.9 fixture\",\n passed: queryResult.exitCode === 0 && queryResult.stdout.includes(\"组织D\"),\n actual: queryResult.exitCode === 0 ? (queryResult.stdout.includes(\"组织D\") ? \"found\" : \"not found\") : `exit ${queryResult.exitCode}`,\n expected: \"v1.9 fixture content found\",\n },\n {\n check: \"migration vault/runtime separation\",\n passed: vaultOk,\n actual: vaultOk ? \"separated\" : \"leaked\",\n expected: \"vault has no runtime artifacts\",\n },\n {\n check: \"PRAGMA integrity_check ok\",\n passed: integrityOk,\n actual: integrityOk ? \"ok\" : \"failed\",\n expected: \"ok\",\n },\n {\n check: \"PRAGMA foreign_key_check clean\",\n passed: fkOk,\n actual: fkOk ? \"clean\" : \"violations found\",\n expected: \"no violations\",\n },\n {\n check: \"schema_version exists in config\",\n passed: schemaVersionOk,\n actual: schemaVersionOk ? \"present\" : \"missing\",\n expected: \"present\",\n },\n {\n check: \"post-migration columns exist (expires_at, confidence_decay)\",\n passed: requiredColumnsOk,\n actual: requiredColumnsOk ? \"present\" : \"missing\",\n expected: \"columns exist\",\n },\n {\n check: \"migration_v5_raw_to_records marker present\",\n passed: migrationV5Ok,\n actual: migrationV5Ok ? \"present\" : \"missing\",\n expected: \"value '1'\",\n },\n {\n check: \"second doctor run is idempotent (exit 0)\",\n passed: doctor2Result.exitCode === 0,\n actual: String(doctor2Result.exitCode),\n expected: \"0\",\n },\n ];\n\n return makeSyntheticStage(\"migration\", assertions);\n}\n\n// ── Artifact Checks (recursive vault scan) ──\n\nfunction runArtifactChecks(ctx: IsolationContext): AssertionResult[] {\n const vaultPath = join(ctx.brainDir, \"vault\");\n const assertions: AssertionResult[] = [];\n const forbiddenPatterns = [/\\.sqlite(-wal|-shm|-journal)?$/, /^lancedb$/i, /^runtime$/i, /^outputs$/i];\n\n function scan(dir: string): void {\n if (!existsSync(dir)) return;\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n const rel = relative(vaultPath, full);\n for (const pat of forbiddenPatterns) {\n if (pat.test(entry)) {\n assertions.push({\n check: `vault has no \"${rel}\"`,\n passed: false,\n actual: \"found (bad)\",\n expected: \"not found\",\n });\n // Don't double-report\n return;\n }\n }\n try { if (statSync(full).isDirectory()) scan(full); } catch { /* skip */ }\n }\n }\n\n scan(vaultPath);\n\n if (assertions.length === 0) {\n assertions.push({ check: \"vault is clean (recursive scan)\", passed: true, actual: \"no runtime artifacts\", expected: \"clean\" });\n }\n\n return assertions;\n}\n\n// ── Report Sanitization ──\n\n/** Sanitize paths only (HOME, checkout). Used before privacy detection. */\nfunction sanitizePaths(json: string): string {\n const realHome = process.env.HOME ?? \"\";\n let sanitized = json;\n if (realHome) sanitized = sanitized.replaceAll(realHome, \"<HOME>\");\n sanitized = sanitized.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n return sanitized;\n}\n\n/** Full sanitization: paths + credential redaction. Used for final output. */\nfunction sanitizeForOutput(json: string): string {\n let sanitized = sanitizePaths(json);\n // Redact credential-like patterns — detection is not enough; output must not echo them\n sanitized = sanitized.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n return sanitized;\n}\n\n// ── Privacy Check ──\n// Runs on path-sanitized JSON (credentials NOT yet redacted so we can detect them).\n\nfunction runPrivacyCheck(pathSanitizedJson: string): AssertionResult[] {\n return [\n {\n check: \"no real HOME path in output\",\n passed: !pathSanitizedJson.includes(\"/Users/\"),\n actual: pathSanitizedJson.includes(\"/Users/\") ? \"found\" : \"clean\",\n expected: \"no real home paths\",\n },\n {\n check: \"no checkout path in output\",\n passed: !pathSanitizedJson.includes(\"Projects/cbrain\"),\n actual: pathSanitizedJson.includes(\"Projects/cbrain\") ? \"found\" : \"clean\",\n expected: \"no checkout paths\",\n },\n {\n check: \"no credential patterns in output\",\n passed: !/sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) && !pathSanitizedJson.includes(\"Bearer \"),\n actual: /sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) ? \"found\" : \"clean\",\n expected: \"no credential patterns\",\n },\n {\n check: \"no raw vector arrays in output\",\n passed: !/\"(0\\.\\d{4},){10}/.test(pathSanitizedJson),\n actual: /\"(0\\.\\d{4},){10}/.test(pathSanitizedJson) ? \"found vectors\" : \"clean\",\n expected: \"no raw vectors\",\n },\n ];\n}\n\n// ── Main Orchestrator ──\n// Returns {report, exitCode} — does NOT call process.exit().\n// ALL post-isolation work (pack, mock, stages) is inside try/catch/finally.\n// Cleanup in finally ALWAYS runs — even on pack/startup failure.\n//\n// Test-only controls (env vars):\n// GATE_PROJECT_DIR — override project dir (for isolated tests)\n// GATE_FAULT_* — inject faults at specific points\n\nasync function executeGate(): Promise<GateResult> {\n // GATE_PROJECT_DIR allows tests to point the gate at a shadow project\n // without touching the real checkout.\n const projectDir = process.env.GATE_PROJECT_DIR\n ?? resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\n const version = JSON.parse(readFileSync(join(projectDir, \"package.json\"), \"utf-8\")).version;\n const gateStart = performance.now();\n\n const ctx = createIsolation();\n let mock: MockServer | undefined;\n let embeddingMode: EmbeddingMode = { kind: \"deterministic\" };\n const stages: StageResult[] = [];\n let cleanupVerified = false;\n\n try {\n // ALL post-isolation work: pack, mock server, stage execution.\n // If ANY step throws, catch records the failure, and finally ALWAYS cleans up.\n const packResult = packAndInstall(projectDir, ctx, version);\n\n // ── Test-only fault: delete skill file after install ──\n if (process.env.GATE_FAULT_SKILL === \"1\") {\n const skillFile = join(packResult.pkgDir, \"skills\", \"SKILL.md\");\n if (existsSync(skillFile)) rmSync(skillFile, { force: true });\n }\n\n // (#204) Resolve embedding transport: HTTP mock (preferred) or in-process\n // deterministic fallback when a localhost TCP listener cannot bind.\n embeddingMode = await resolveEmbeddingMode();\n if (embeddingMode.kind === \"http\") {\n mock = embeddingMode.mock;\n }\n\n let env = buildIsolatedEnv(ctx);\n\n // ── Test-only fault: block init with pre-existing file ──\n if (process.env.GATE_FAULT_INIT === \"1\") {\n writeFileSync(ctx.brainDir, \"block-init\");\n }\n\n // Stage 1: init\n const initStage = await runStageInit(packResult.binPath, ctx, env);\n stages.push(initStage);\n\n // Subsequent stages only if init passed\n if (initStage.passed) {\n patchConfig(ctx.configPath, embeddingMode);\n env = buildIsolatedEnv(ctx);\n\n stages.push(await runStageDoctor(packResult.binPath, ctx, env));\n stages.push(await runStageMcpConfig(packResult.binPath, ctx, env));\n stages.push(await runStageSkillPack(packResult.binPath, ctx, env));\n\n // ── Test-only fault: skip ingest → empty brain query ──\n if (process.env.GATE_FAULT_SKIP_INGEST !== \"1\") {\n stages.push(await runStageIngest(packResult.binPath, ctx, env));\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n } else {\n // Run query on empty brain — should return no results\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n }\n\n stages.push(await runStageMigration(packResult.binPath, ctx, env, embeddingMode));\n }\n\n // ── Test-only fault: create runtime/ inside vault → artifacts fail ──\n if (process.env.GATE_FAULT_VAULT_NESTED === \"1\") {\n const nestedRuntime = join(ctx.brainDir, \"vault\", \"runtime\");\n mkdirSync(nestedRuntime, { recursive: true });\n writeFileSync(join(nestedRuntime, \"leak.txt\"), \"should not exist\");\n }\n\n // Artifact checks\n stages.push(makeSyntheticStage(\"artifacts\", runArtifactChecks(ctx)));\n } catch (e) {\n stages.push(\n makeSyntheticStage(\"fatal\", [\n {\n check: \"no unhandled errors\",\n passed: false,\n actual: e instanceof Error ? e.message : String(e),\n expected: \"no errors\",\n },\n ]),\n );\n } finally {\n // ── Cleanup (ALWAYS runs — even if catch itself throws) ──\n try {\n mock?.stop();\n } catch {\n /* best effort */\n }\n\n // Tarball is inside ctx.tmpdir — cleaned up with it.\n // Pre-existing tarball was already restored by packAndInstall's inner finally.\n\n if (existsSync(ctx.tmpdir)) {\n try {\n rmSync(ctx.tmpdir, { recursive: true });\n cleanupVerified = !existsSync(ctx.tmpdir);\n } catch {\n cleanupVerified = false;\n }\n } else {\n cleanupVerified = true;\n }\n }\n\n // ── Build report ──\n const allStagesPassed = stages.every((s) => s.passed);\n const rawReport: GateReport = {\n gate: \"first-recall-v2\",\n version,\n timestamp: new Date().toISOString(),\n verdict: allStagesPassed && cleanupVerified ? \"go\" : \"no-go\",\n embedding_mode: embeddingMode.kind,\n stages,\n cleanup: { verified: cleanupVerified, path: cleanupVerified ? \"<cleaned>\" : \"<retained>\" },\n duration_ms: Math.round(performance.now() - gateStart),\n };\n\n // ── Test-only fault: inject credential into report for privacy check ──\n // This tests that the gate's privacy + redaction pipeline works end-to-end:\n // detection → redaction → no-go verdict → clean output.\n if (process.env.GATE_FAULT_MCP_CRED_LEAK === \"1\") {\n const mcpIdx = rawReport.stages.findIndex((s) => s.id === \"mcp-config\");\n if (mcpIdx >= 0) {\n const stage = rawReport.stages[mcpIdx];\n rawReport.stages[mcpIdx] = {\n ...stage,\n assertions: [\n ...stage.assertions,\n {\n check: \"simulated credential leak test\",\n passed: true,\n actual: \"sk-abcdef1234567890abcdef1234567890\",\n expected: \"no credentials in output\",\n },\n ],\n };\n }\n }\n\n // Privacy detection on path-sanitized JSON (credentials NOT yet redacted)\n const pathSanitized = sanitizePaths(JSON.stringify(rawReport, null, 2));\n const privacyAssertions = runPrivacyCheck(pathSanitized);\n const privacyPassed = privacyAssertions.every((a) => a.passed);\n\n // Full sanitization for output: paths + credential redaction\n const fullySanitized = sanitizeForOutput(JSON.stringify(rawReport, null, 2));\n\n let finalReport: GateReport;\n let finalExitCode: number;\n\n if (!privacyPassed) {\n const parsed = JSON.parse(fullySanitized);\n parsed.stages = [...parsed.stages, makeSyntheticStage(\"privacy\", privacyAssertions)];\n parsed.verdict = \"no-go\";\n finalReport = parsed as GateReport;\n finalExitCode = 1;\n } else {\n finalReport = JSON.parse(fullySanitized) as GateReport;\n finalExitCode = rawReport.verdict === \"go\" ? 0 : 1;\n }\n\n return { report: finalReport, exitCode: finalExitCode };\n}\n\n// ── Top-level: run gate → emit report → set exit code ──\n// process.exitCode (not process.exit) lets the runtime clean up naturally.\n// Outer catch handles pre-isolation fatal errors only — post-isolation errors\n// are handled by try/catch/finally inside executeGate().\n\nexecuteGate().then(({ report, exitCode }) => {\n process.stdout.write(JSON.stringify(report, null, 2) + \"\\n\");\n process.exitCode = exitCode;\n}).catch((e) => {\n // Sanitize the fatal error — never echo real paths/stacks/secrets\n let errorDetail = e instanceof Error ? e.message : String(e);\n const realHome = process.env.HOME ?? \"\";\n if (realHome) errorDetail = errorDetail.replaceAll(realHome, \"<HOME>\");\n errorDetail = errorDetail.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n // Strip any remaining absolute user paths\n errorDetail = errorDetail.replace(/\\/Users\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/\\/home\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n // Strip stack traces — only keep the first line\n errorDetail = errorDetail.split(\"\\n\")[0];\n\n const errorReport = {\n gate: \"first-recall-v2\",\n verdict: \"no-go\",\n error: errorDetail,\n cleanup: { verified: false, path: \"<unknown>\" },\n };\n process.stdout.write(JSON.stringify(errorReport, null, 2) + \"\\n\");\n process.exitCode = 2;\n});\n"}, "62f400c3e77db2755bbcaa920f676f589662a7f9": {"t": 1785759166.7295115, "n": 40415, "h": "af4f6f11478eb593", "c": "#!/usr/bin/env bun\n// check-first-recall-gate.ts — Offline first-install-to-first-recall v2.0 release gate\n//\n// Usage: bun bin/check-first-recall-gate.ts\n//\n// Proves the packed CLI artifact works end-to-end:\n// init → doctor → mcp-config → skill-pack → ingest → query → migration\n//\n// Fully offline (uses checkout node_modules via symlink). Mock embedding server on localhost.\n// Fully isolated environment. Verified cleanup on ALL paths.\n//\n// Production dependency install is verified separately by: bun run check:install-network\n// v2 release requires BOTH gates to pass.\n//\n// Test-only controls (env vars, ignored in production):\n// GATE_PROJECT_DIR=<path> — override project dir (for shadow-project tests)\n// GATE_FAULT_PROVIDER=1 — use unreachable mock URL → ingest/query fail\n// GATE_FAULT_SKIP_INGEST=1 — skip ingest stage → query on empty brain\n// GATE_FAULT_SKILL=1 — delete SKILL.md → skill-pack fails\n// GATE_FAULT_INIT=1 — block init with pre-existing file → init fails\n// GATE_FAULT_VAULT_NESTED=1 — create runtime/ inside vault → artifacts fail\n// GATE_FAULT_MIGRATION_CORRUPT=1 — corrupt migration fixture DB → migration fails\n// GATE_FAULT_MCP_CRED_LEAK=1 — inject credential into report → privacy fail\n//\n// Exit codes: 0 = go, 1 = no-go, 2 = fatal error (script bug)\n\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n symlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, resolve, dirname, relative } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { fileURLToPath } from \"url\";\nimport { Database } from \"bun:sqlite\";\n\n// ── Types ──\n\ntype GateVerdict = \"go\" | \"no-go\";\n\ninterface AssertionResult {\n readonly check: string;\n readonly passed: boolean;\n readonly actual: string;\n readonly expected: string;\n}\n\ninterface StageResult {\n readonly id: string;\n readonly command: string;\n readonly exitCode: number;\n readonly duration_ms: number;\n readonly passed: boolean;\n readonly assertions: ReadonlyArray<AssertionResult>;\n}\n\ninterface GateReport {\n readonly gate: \"first-recall-v2\";\n readonly version: string;\n readonly timestamp: string;\n readonly verdict: GateVerdict;\n readonly stages: ReadonlyArray<StageResult>;\n readonly cleanup: { readonly verified: boolean; readonly path: string };\n readonly duration_ms: number;\n}\n\ninterface GateResult {\n readonly report: GateReport;\n readonly exitCode: number;\n}\n\ninterface IsolationContext {\n readonly tmpdir: string;\n readonly homeDir: string;\n readonly bunInstallDir: string;\n readonly brainDir: string;\n configPath: string;\n readonly cwd: string;\n}\n\ninterface PackResult {\n readonly tarballPath: string;\n readonly pkgDir: string;\n readonly binPath: string;\n}\n\ninterface MockServer {\n readonly url: string;\n readonly port: number;\n stop(): void;\n}\n\ninterface CliResult {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n readonly duration_ms: number;\n}\n\n// ── Constants ──\n\nconst INGEST_TEXT =\n \"实体A是一个重要的概念,它与主题B密切相关。方法C是解决这个问题的关键技术路径。\";\nconst INGEST_UNIQUE_TOKEN = \"实体a\"; // appears in auto-generated slug\nconst QUERY_TEXT = \"实体A 主题B 方法C\"; // includes unique token for deterministic recall\nconst VECTOR_DIM = 2048;\nconst DETERMINISTIC_VECTOR = Array.from({ length: VECTOR_DIM }, (_, i) =>\n Math.sin(i * 0.001) * 0.5,\n);\n\n// ── Mock Embedding Server ──\n\nfunction startMockEmbeddingServer(): MockServer {\n const server = Bun.serve({\n port: 0,\n hostname: \"127.0.0.1\",\n fetch(req) {\n const url = new URL(req.url);\n if (url.pathname === \"/embeddings\" && req.method === \"POST\") {\n return req.json().then((body: { input?: string[] }) => {\n const inputs = body.input ?? [];\n const data = inputs.map((_, i) => ({\n embedding: DETERMINISTIC_VECTOR,\n index: i,\n }));\n return Response.json({\n data,\n usage: { total_tokens: inputs.length * 10 },\n });\n });\n }\n return new Response(\"NOT_FOUND\", { status: 404 });\n },\n });\n\n return {\n url: `http://127.0.0.1:${server.port}`,\n port: server.port,\n stop() {\n server.stop();\n },\n };\n}\n\n// ── Isolation ──\n\nfunction createIsolation(): IsolationContext {\n const base = mkdtempSync(join(tmpdir(), \"cbrain-gate-\"));\n const homeDir = join(base, \"home\");\n const bunInstallDir = join(base, \"bun\");\n const brainDir = join(base, \"brain\");\n const cwd = join(base, \"cwd\");\n\n mkdirSync(homeDir, { recursive: true });\n mkdirSync(bunInstallDir, { recursive: true });\n mkdirSync(cwd, { recursive: true });\n\n return { tmpdir: base, homeDir, bunInstallDir, brainDir, configPath: \"\", cwd };\n}\n\n// Build a CLEAN env for subprocesses — allowlist only, no inherited secrets.\nfunction buildIsolatedEnv(\n ctx: IsolationContext,\n extraEnv: Record<string, string> = {},\n): Record<string, string> {\n // Only inherit truly safe system vars\n const safeKeys = [\"LANG\", \"LC_ALL\", \"LC_CTYPE\", \"TERM\", \"TMPDIR\", \"TEMP\", \"TMP\", \"__CF_USER_TEXT_ENCODING\"];\n const inherited: Record<string, string> = {};\n for (const key of safeKeys) {\n if (process.env[key]) inherited[key] = process.env[key]!;\n }\n\n return {\n ...inherited,\n // PATH: need bun + system tools, but no cbrain bin\n PATH: `${ctx.bunInstallDir}/bin:${process.env.PATH ?? \"\"}`.split(\":\")\n .filter((p) => !p.includes(\"cbrain\") || p.includes(\"cbrain-gate\"))\n .join(\":\"),\n HOME: ctx.homeDir,\n BUN_INSTALL: ctx.bunInstallDir,\n XDG_CONFIG_HOME: join(ctx.homeDir, \".config\"),\n XDG_DATA_HOME: join(ctx.homeDir, \".local\", \"share\"),\n XDG_CACHE_HOME: join(ctx.homeDir, \".cache\"),\n CBRAIN_CONFIG: ctx.configPath,\n ZHIPU_API_KEY: \"gate-test-fake-key\",\n // Explicitly clear any inherited provider vars\n ...(process.env.DEEPSEEK_API_KEY ? { DEEPSEEK_API_KEY: \"\" } : {}),\n ...extraEnv,\n };\n}\n\n// ── Pack + Offline Install ──\n// Uses checkout node_modules via symlink — fully offline, no network needed.\n// Production dependency install is verified by check:install-network separately.\n\nfunction packAndInstall(projectDir: string, ctx: IsolationContext, version: string): PackResult {\n // Verify checkout has deps\n if (!existsSync(join(projectDir, \"node_modules\"))) {\n throw new Error(\"node_modules not found in checkout — run bun install first\");\n }\n\n // Deterministic tarball name: cbrain-{version}.tgz\n const tarballName = `cbrain-${version}.tgz`;\n const tarballInProject = join(projectDir, tarballName);\n let backupPath: string | null = null;\n\n // Backup pre-existing same-version tarball (if any) — restored in finally\n if (existsSync(tarballInProject)) {\n backupPath = `${tarballInProject}.gate-backup`;\n renameSync(tarballInProject, backupPath);\n }\n\n try {\n // Pack — always creates cbrain-{version}.tgz in project dir\n execSync(\"bun pm pack\", { cwd: projectDir, encoding: \"utf-8\", timeout: 30_000 });\n\n if (!existsSync(tarballInProject)) {\n throw new Error(\"bun pm pack produced no tarball\");\n }\n\n // Move tarball to isolated dir — never leaves artifacts in project dir\n const packDir = join(ctx.tmpdir, \"pack\");\n mkdirSync(packDir, { recursive: true });\n const isolatedTarball = join(packDir, tarballName);\n renameSync(tarballInProject, isolatedTarball);\n\n // Extract\n execSync(`tar -xzf \"${isolatedTarball}\" -C \"${packDir}\"`, { encoding: \"utf-8\" });\n\n // Find package dir (the extracted directory, not our tarball)\n const entries = readdirSync(packDir).filter((e) => e !== tarballName);\n if (entries.length === 0) throw new Error(\"tarball was empty\");\n const pkgDir = join(packDir, entries[0]);\n\n // OFFLINE: symlink checkout node_modules — no network needed.\n // Production dependency install is proven by check:install-network separately.\n symlinkSync(join(projectDir, \"node_modules\"), join(pkgDir, \"node_modules\"));\n\n return {\n tarballPath: isolatedTarball,\n pkgDir,\n binPath: join(pkgDir, \"src\", \"cli\", \"index.ts\"),\n };\n } finally {\n // Always restore pre-existing tarball — even on failure\n if (backupPath && existsSync(backupPath)) {\n renameSync(backupPath, tarballInProject);\n }\n }\n}\n\n// ── CLI Runner (async — Bun.spawn does NOT block event loop) ──\n\nasync function runCli(\n binPath: string,\n args: string[],\n ctx: IsolationContext,\n env: Record<string, string>,\n timeoutMs = 30_000,\n): Promise<CliResult> {\n const start = performance.now();\n try {\n const proc = Bun.spawn([process.execPath, \"run\", binPath, ...args], {\n cwd: ctx.cwd,\n env,\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n\n // Race process exit vs timeout — always clear the timer\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timeoutHandle = setTimeout(() => {\n proc.kill();\n reject(new Error(`TIMEOUT after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n let exitCode: number;\n try {\n exitCode = await Promise.race([proc.exited, timeoutPromise]);\n } finally {\n if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);\n }\n\n const [stdout, stderr] = await Promise.all([\n new Response(proc.stdout).text(),\n new Response(proc.stderr).text(),\n ]);\n\n return { exitCode, stdout, stderr, duration_ms: Math.round(performance.now() - start) };\n } catch (e) {\n return {\n exitCode: 1,\n stdout: \"\",\n stderr: e instanceof Error ? e.message : String(e),\n duration_ms: Math.round(performance.now() - start),\n };\n }\n}\n\n// ── Assertion Helpers ──\n// Every stage uses assertions.every(a => a.passed) for its verdict — no exceptions.\n\nfunction assertExitCode(result: CliResult, expected: number): AssertionResult {\n return {\n check: `exit code ${expected}`,\n passed: result.exitCode === expected,\n actual: String(result.exitCode),\n expected: String(expected),\n };\n}\n\nfunction assertExitCodeOkOrWarn(result: CliResult): AssertionResult {\n return {\n check: \"exit code 0 or 1 (warn allowed)\",\n passed: result.exitCode <= 1,\n actual: String(result.exitCode),\n expected: \"0 or 1\",\n };\n}\n\nfunction assertJsonField(stdout: string, field: string, expected: unknown): AssertionResult {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: false, actual: \"JSON parse failed\", expected: String(expected) };\n }\n const actual = parsed[field];\n return { check: `json.${field} === ${JSON.stringify(expected)}`, passed: actual === expected, actual: String(actual), expected: String(expected) };\n}\n\nfunction assertJsonParseable(stdout: string, label: string): AssertionResult {\n try {\n JSON.parse(stdout);\n return { check: label, passed: true, actual: \"valid JSON\", expected: \"valid JSON\" };\n } catch {\n return { check: label, passed: false, actual: \"invalid JSON\", expected: \"valid JSON\" };\n }\n}\n\nfunction assertFieldExists(stdout: string, field: string): AssertionResult {\n let parsed: Record<string, unknown>;\n try { parsed = JSON.parse(stdout); } catch {\n return { check: `json has \"${field}\"`, passed: false, actual: \"JSON parse failed\", expected: `field \"${field}\" exists` };\n }\n const val = parsed[field];\n return {\n check: `json has \"${field}\"`,\n passed: val !== undefined && val !== null && String(val).length > 0,\n actual: val === undefined ? \"undefined\" : val === null ? \"null\" : String(val).slice(0, 80),\n expected: `non-empty \"${field}\"`,\n };\n}\n\nfunction assertContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: haystack.includes(needle), actual: haystack.includes(needle) ? \"found\" : \"not found\", expected: `contains \"${needle}\"` };\n}\n\nfunction assertNotContains(haystack: string, needle: string, label: string): AssertionResult {\n return { check: label, passed: !haystack.includes(needle), actual: haystack.includes(needle) ? \"found (bad)\" : \"not found (good)\", expected: `does not contain \"${needle}\"` };\n}\n\nfunction assertMatches(text: string, pattern: RegExp, label: string): AssertionResult {\n return { check: label, passed: pattern.test(text), actual: pattern.test(text) ? \"matched\" : \"no match\", expected: `matches ${pattern}` };\n}\n\n/** Derive stage passed from ALL assertions — the single source of truth. */\nfunction verdict(assertions: ReadonlyArray<AssertionResult>): boolean {\n return assertions.every((a) => a.passed);\n}\n\nfunction makeStage(\n id: string,\n command: string,\n result: CliResult,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command, exitCode: result.exitCode, duration_ms: result.duration_ms, passed: verdict(assertions), assertions };\n}\n\nfunction makeSyntheticStage(\n id: string,\n assertions: ReadonlyArray<AssertionResult>,\n): StageResult {\n return { id, command: \"(internal check)\", exitCode: verdict(assertions) ? 0 : 1, duration_ms: 0, passed: verdict(assertions), assertions };\n}\n\n// ── Config Patching ──\n\nfunction patchConfig(configPath: string, mockUrl: string): void {\n const config = JSON.parse(readFileSync(configPath, \"utf-8\"));\n config.embedding.baseUrl = mockUrl;\n config.embedding.apiKey = \"gate-test-fake-key\";\n config.ner = { enabled: false };\n writeFileSync(configPath, JSON.stringify(config, null, 2) + \"\\n\");\n}\n\n// ── v1.9 Migration Fixture Builder ──\n// Creates a minimal SQLite DB with pre-migration schema + data.\n// Does NOT use `cbrain init` — schema is built with raw SQL.\n\nconst V19_FIXTURE_SLUG = \"records/v1-9-fixture\";\nconst V19_FIXTURE_TITLE = \"v1.9迁移测试记录\";\nconst V19_FIXTURE_BODY = \"组织D在主题B中采用了方法C的核心策略。这段记录来自模拟的v1.9安装。\";\nconst V19_QUERY_TEXT = \"组织D 主题B\";\n\nfunction buildV19FixtureDB(dbPath: string, vaultPath: string): void {\n mkdirSync(dirname(dbPath), { recursive: true });\n mkdirSync(join(vaultPath, \"records\"), { recursive: true });\n for (const sub of [\"entities\", \"concepts\", \"insights\"]) {\n mkdirSync(join(vaultPath, \"brain\", sub), { recursive: true });\n }\n\n // Create DB with v1.9-era base schema (before migrations)\n const db = new Database(dbPath);\n db.exec(\"PRAGMA journal_mode = WAL\");\n\n // Base tables — minimal v1.9 schema\n db.exec(`CREATE TABLE IF NOT EXISTS pages (\n slug TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n file_path TEXT NOT NULL,\n content_hash TEXT,\n tier INTEGER DEFAULT 3,\n mention_count INTEGER DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n )`);\n db.exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);\n db.exec(`CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n page_slug TEXT NOT NULL,\n chunk_index INTEGER NOT NULL,\n content TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(page_slug, chunk_index)\n )`);\n db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(page_slug, content, tokenize='trigram')`);\n db.exec(`CREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_slug TEXT NOT NULL,\n to_slug TEXT NOT NULL,\n relation TEXT NOT NULL DEFAULT 'mentions',\n context TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(from_slug, to_slug, relation)\n )`);\n\n // Insert fixture data\n db.run(\"INSERT INTO pages (slug, type, title, file_path) VALUES (?, ?, ?, ?)\",\n [V19_FIXTURE_SLUG, \"record\", V19_FIXTURE_TITLE, `${V19_FIXTURE_SLUG}.md`]);\n db.run(\"INSERT INTO chunks (page_slug, chunk_index, content) VALUES (?, 0, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO chunks_fts (page_slug, content) VALUES (?, ?)\",\n [V19_FIXTURE_SLUG, V19_FIXTURE_BODY]);\n db.run(\"INSERT INTO config (key, value) VALUES ('schema_version', '1')\");\n\n db.close();\n\n // Create vault markdown file\n const mdPath = join(vaultPath, `${V19_FIXTURE_SLUG}.md`);\n mkdirSync(dirname(mdPath), { recursive: true });\n writeFileSync(mdPath, `---\\ntitle: \"${V19_FIXTURE_TITLE}\"\\ntype: record\\n---\\n\\n${V19_FIXTURE_BODY}\\n`);\n}\n\n// ── Stage 1: init ──\n\nasync function runStageInit(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"init\", \"--dir\", ctx.brainDir, \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"init output is valid JSON\"),\n assertJsonField(result.stdout, \"status\", \"ok\"),\n assertJsonField(result.stdout, \"created\", true),\n ];\n\n // Extract configPath and verify it's inside tmpdir\n try {\n const parsed = JSON.parse(result.stdout);\n const cp = parsed.configPath;\n if (typeof cp === \"string\" && cp.length > 0) {\n ctx.configPath = cp;\n assertions.push(assertContains(cp, ctx.tmpdir, \"config path inside tmpdir\"));\n } else {\n assertions.push({ check: \"configPath present\", passed: false, actual: String(cp), expected: \"non-empty string\" });\n }\n } catch {\n assertions.push({ check: \"configPath present\", passed: false, actual: \"JSON parse failed\", expected: \"non-empty string\" });\n }\n\n return makeStage(\"init\", \"cbrain init --dir <brain> --json\", result, assertions);\n}\n\n// ── Stage 2: doctor --first-run ──\n\nasync function runStageDoctor(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCodeOkOrWarn(result),\n assertJsonParseable(result.stdout, \"doctor output is valid JSON\"),\n assertFieldExists(result.stdout, \"overallStatus\"),\n assertFieldExists(result.stdout, \"readinessState\"),\n ];\n\n // After init + patching: readiness must be ≥ missing_index (creds are set)\n try {\n const parsed = JSON.parse(result.stdout);\n const rs = parsed.readinessState as string | undefined;\n const validStates = [\"missing_index\", \"service_active\", \"ready\"];\n assertions.push({\n check: \"readinessState >= missing_index\",\n passed: !!rs && validStates.includes(rs),\n actual: rs ?? \"missing\",\n expected: \"missing_index | service_active | ready\",\n });\n } catch {\n assertions.push({ check: \"readinessState >= missing_index\", passed: false, actual: \"parse error\", expected: \"missing_index | service_active | ready\" });\n }\n\n return makeStage(\"doctor\", \"cbrain doctor --first-run --json\", result, assertions);\n}\n\n// ── Stage 3: mcp-config ──\n\nasync function runStageMcpConfig(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"mcp-config\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"mcp-config output is valid JSON\"),\n ];\n\n // Parse JSON and verify structure\n try {\n const parsed = JSON.parse(result.stdout);\n const cbrain = parsed.mcpServers?.cbrain as Record<string, unknown> | undefined;\n assertions.push({\n check: \"has mcpServers.cbrain\",\n passed: !!cbrain && typeof cbrain === \"object\",\n actual: cbrain ? \"present\" : \"missing\",\n expected: \"mcpServers.cbrain object\",\n });\n\n if (cbrain) {\n // command/args must point to installed artifact, not source checkout\n const args = cbrain.args as string[] | undefined;\n const argStr = (args ?? []).join(\" \");\n assertions.push(assertContains(argStr, ctx.tmpdir, \"CLI args point into installed package\"));\n assertions.push(assertNotContains(argStr, \"Projects/cbrain\", \"CLI args not in source checkout\"));\n\n // env must have CBRAIN_CONFIG but NOT contain credentials\n const envObj = cbrain.env as Record<string, string> | undefined;\n assertions.push(assertContains(JSON.stringify(envObj ?? {}), \"CBRAIN_CONFIG\", \"env has CBRAIN_CONFIG\"));\n assertions.push(assertNotContains(JSON.stringify(envObj ?? {}), \"gate-test-fake-key\", \"no API key in MCP config env\"));\n }\n } catch {\n assertions.push({ check: \"mcp-config JSON structure\", passed: false, actual: \"parse error\", expected: \"valid structure\" });\n }\n\n return makeStage(\"mcp-config\", \"cbrain mcp-config\", result, assertions);\n}\n\n// ── Stage 4: skill-pack ──\n\nasync function runStageSkillPack(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"skill-pack\", \"--json\"], ctx, env);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertJsonParseable(result.stdout, \"skill-pack output is valid JSON\"),\n ];\n\n try {\n const parsed = JSON.parse(result.stdout);\n assertions.push({\n check: \"verificationStatus is pass\",\n passed: parsed.verificationStatus === \"pass\",\n actual: String(parsed.verificationStatus),\n expected: \"pass\",\n });\n assertions.push({\n check: \"no missing files\",\n passed: Array.isArray(parsed.missingFiles) && parsed.missingFiles.length === 0,\n actual: JSON.stringify(parsed.missingFiles),\n expected: \"[]\",\n });\n // packPath must be inside installed package, not source checkout\n const packPath = String(parsed.packPath ?? \"\");\n assertions.push(assertContains(packPath, ctx.tmpdir, \"packPath inside installed package\"));\n assertions.push(assertNotContains(packPath, \"Projects/cbrain\", \"packPath not in source checkout\"));\n } catch {\n assertions.push({ check: \"skill-pack JSON fields\", passed: false, actual: \"parse error\", expected: \"valid fields\" });\n }\n\n return makeStage(\"skill-pack\", \"cbrain skill-pack --json\", result, assertions);\n}\n\n// ── Stage 5: ingest ──\n\nasync function runStageIngest(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"ingest\", INGEST_TEXT, \"--no-ner\"], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n assertContains(result.stdout, \"✓ Created:\", \"page created\"),\n assertMatches(result.stdout, /records\\//, \"slug has records/ prefix\"),\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"output contains unique fixture token\"),\n assertNotContains(result.stderr, \"Error:\", \"no stderr errors\"),\n ];\n\n return makeStage(\"ingest\", 'cbrain ingest \"...\" --no-ner', result, assertions);\n}\n\n// ── Stage 6: query ──\n\nasync function runStageQuery(binPath: string, ctx: IsolationContext, env: Record<string, string>): Promise<StageResult> {\n const result = await runCli(binPath, [\"query\", QUERY_TEXT], ctx, env, 60_000);\n\n const assertions: AssertionResult[] = [\n assertExitCode(result, 0),\n {\n check: \"non-empty output\",\n passed: result.stdout.trim().length > 0,\n actual: result.stdout.trim().length > 0 ? \"has content\" : \"empty\",\n expected: \"non-empty\",\n },\n assertNotContains(result.stdout, \"没有找到相关内容\", \"results found\"),\n // Must recall the specific fixture, not just any records/ match\n assertContains(result.stdout, INGEST_UNIQUE_TOKEN, \"query returns our unique fixture token\"),\n ];\n\n return makeStage(\"query\", `cbrain query \"${QUERY_TEXT}\"`, result, assertions);\n}\n\n// ── Stage 7: v1.9 Migration ──\n// Creates a DB with pre-migration schema + data, then verifies current CLI can read it.\n// Assertions: schema migration ran, integrity check, FK check, idempotent reopen,\n// query returns fixture data, vault/runtime separation.\n\nasync function runStageMigration(binPath: string, ctx: IsolationContext, env: Record<string, string>, mockUrl: string): Promise<StageResult> {\n const migrationBrain = join(ctx.tmpdir, \"migration-brain\");\n const migrationVault = join(migrationBrain, \"vault\");\n const migrationDbPath = join(migrationBrain, \"brain.sqlite\");\n const migrationLancePath = join(migrationBrain, \"lancedb\");\n const migrationRuntime = join(migrationBrain, \"runtime\");\n mkdirSync(migrationRuntime, { recursive: true });\n\n // Build v1.9 fixture DB (NOT via init — raw SQL)\n buildV19FixtureDB(migrationDbPath, migrationVault);\n\n // ── Test-only fault: corrupt the fixture DB ──\n if (process.env.GATE_FAULT_MIGRATION_CORRUPT === \"1\") {\n // Overwrite DB with garbage — integrity checks must catch this\n writeFileSync(migrationDbPath, \"CORRUPTED_DB_DATA_\" + \"X\".repeat(100));\n }\n\n // Write config pointing to mock server\n const migrationConfig = {\n vaultPath: migrationVault,\n dbPath: migrationDbPath,\n lancePath: migrationLancePath,\n runtimePath: migrationRuntime,\n embedding: { provider: \"zhipu\", apiKey: \"gate-test-fake-key\", baseUrl: mockUrl },\n ner: { enabled: false },\n };\n const migrationConfigPath = join(migrationBrain, \"cbrain.json\");\n writeFileSync(migrationConfigPath, JSON.stringify(migrationConfig, null, 2) + \"\\n\");\n\n const migrationEnv = { ...env, CBRAIN_CONFIG: migrationConfigPath };\n\n // Doctor on migrated DB — current code runs migrations on open\n const doctorResult = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Parse doctor output — must have a valid structured status\n let doctorParsed: Record<string, unknown> | null = null;\n try { doctorParsed = JSON.parse(doctorResult.stdout); } catch { /* handled by assertions */ }\n\n // Query on migrated DB — must find the v1.9 fixture\n const queryResult = await runCli(binPath, [\"query\", V19_QUERY_TEXT], ctx, migrationEnv, 60_000);\n\n // Second doctor call — must be idempotent (same result)\n const doctor2Result = await runCli(binPath, [\"doctor\", \"--first-run\", \"--json\"], ctx, migrationEnv);\n\n // Verify vault/runtime separation on migrated brain\n const vaultOk = !existsSync(join(migrationVault, \"brain.sqlite\"))\n && !existsSync(join(migrationVault, \"lancedb\"))\n && !existsSync(join(migrationVault, \"runtime\"));\n\n // Post-migration DB integrity checks\n let integrityOk = false;\n let fkOk = false;\n let schemaVersionOk = false;\n let requiredColumnsOk = false;\n let migrationV5Ok = false;\n\n try {\n const db = new Database(migrationDbPath, { readonly: true });\n\n // PRAGMA integrity_check\n const integrity = db.prepare(\"PRAGMA integrity_check\").get() as { integrity_check: string } | undefined;\n integrityOk = integrity?.integrity_check === \"ok\";\n\n // PRAGMA foreign_key_check (returns empty if OK)\n const fkViolations = db.prepare(\"PRAGMA foreign_key_check\").all();\n fkOk = Array.isArray(fkViolations) && fkViolations.length === 0;\n\n // Schema version must exist and v5 migration must have run\n const sv = db.prepare(\"SELECT value FROM config WHERE key = 'schema_version'\").get() as { value: string } | undefined;\n schemaVersionOk = !!sv?.value;\n\n // Required post-migration columns must exist (links.weight, pages.expires_at, etc.)\n const pageCols = db.prepare(\"PRAGMA table_info(pages)\").all() as { name: string }[];\n const colNames = pageCols.map((c) => c.name);\n requiredColumnsOk = colNames.includes(\"expires_at\") && colNames.includes(\"confidence_decay\");\n\n // v5 migration marker must exist\n const v5 = db.prepare(\"SELECT value FROM config WHERE key = 'migration_v5_raw_to_records'\").get() as { value: string } | undefined;\n migrationV5Ok = v5?.value === \"1\";\n\n db.close();\n } catch {\n // DB open failure — assertions will show false\n }\n\n const assertions: AssertionResult[] = [\n {\n check: \"migration doctor exit 0 or 1\",\n passed: doctorResult.exitCode <= 1,\n actual: String(doctorResult.exitCode),\n expected: \"0 or 1\",\n },\n assertJsonParseable(doctorResult.stdout, \"migration doctor output is valid JSON\"),\n {\n check: \"migration doctor overallStatus is not error\",\n passed: !!doctorParsed && doctorParsed.overallStatus !== \"error\",\n actual: String(doctorParsed?.overallStatus ?? \"missing\"),\n expected: \"non-error status\",\n },\n {\n check: \"migration query found v1.9 fixture\",\n passed: queryResult.exitCode === 0 && queryResult.stdout.includes(\"组织D\"),\n actual: queryResult.exitCode === 0 ? (queryResult.stdout.includes(\"组织D\") ? \"found\" : \"not found\") : `exit ${queryResult.exitCode}`,\n expected: \"v1.9 fixture content found\",\n },\n {\n check: \"migration vault/runtime separation\",\n passed: vaultOk,\n actual: vaultOk ? \"separated\" : \"leaked\",\n expected: \"vault has no runtime artifacts\",\n },\n {\n check: \"PRAGMA integrity_check ok\",\n passed: integrityOk,\n actual: integrityOk ? \"ok\" : \"failed\",\n expected: \"ok\",\n },\n {\n check: \"PRAGMA foreign_key_check clean\",\n passed: fkOk,\n actual: fkOk ? \"clean\" : \"violations found\",\n expected: \"no violations\",\n },\n {\n check: \"schema_version exists in config\",\n passed: schemaVersionOk,\n actual: schemaVersionOk ? \"present\" : \"missing\",\n expected: \"present\",\n },\n {\n check: \"post-migration columns exist (expires_at, confidence_decay)\",\n passed: requiredColumnsOk,\n actual: requiredColumnsOk ? \"present\" : \"missing\",\n expected: \"columns exist\",\n },\n {\n check: \"migration_v5_raw_to_records marker present\",\n passed: migrationV5Ok,\n actual: migrationV5Ok ? \"present\" : \"missing\",\n expected: \"value '1'\",\n },\n {\n check: \"second doctor run is idempotent (exit 0)\",\n passed: doctor2Result.exitCode === 0,\n actual: String(doctor2Result.exitCode),\n expected: \"0\",\n },\n ];\n\n return makeSyntheticStage(\"migration\", assertions);\n}\n\n// ── Artifact Checks (recursive vault scan) ──\n\nfunction runArtifactChecks(ctx: IsolationContext): AssertionResult[] {\n const vaultPath = join(ctx.brainDir, \"vault\");\n const assertions: AssertionResult[] = [];\n const forbiddenPatterns = [/\\.sqlite(-wal|-shm|-journal)?$/, /^lancedb$/i, /^runtime$/i, /^outputs$/i];\n\n function scan(dir: string): void {\n if (!existsSync(dir)) return;\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n const rel = relative(vaultPath, full);\n for (const pat of forbiddenPatterns) {\n if (pat.test(entry)) {\n assertions.push({\n check: `vault has no \"${rel}\"`,\n passed: false,\n actual: \"found (bad)\",\n expected: \"not found\",\n });\n // Don't double-report\n return;\n }\n }\n try { if (statSync(full).isDirectory()) scan(full); } catch { /* skip */ }\n }\n }\n\n scan(vaultPath);\n\n if (assertions.length === 0) {\n assertions.push({ check: \"vault is clean (recursive scan)\", passed: true, actual: \"no runtime artifacts\", expected: \"clean\" });\n }\n\n return assertions;\n}\n\n// ── Report Sanitization ──\n\n/** Sanitize paths only (HOME, checkout). Used before privacy detection. */\nfunction sanitizePaths(json: string): string {\n const realHome = process.env.HOME ?? \"\";\n let sanitized = json;\n if (realHome) sanitized = sanitized.replaceAll(realHome, \"<HOME>\");\n sanitized = sanitized.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n return sanitized;\n}\n\n/** Full sanitization: paths + credential redaction. Used for final output. */\nfunction sanitizeForOutput(json: string): string {\n let sanitized = sanitizePaths(json);\n // Redact credential-like patterns — detection is not enough; output must not echo them\n sanitized = sanitized.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n return sanitized;\n}\n\n// ── Privacy Check ──\n// Runs on path-sanitized JSON (credentials NOT yet redacted so we can detect them).\n\nfunction runPrivacyCheck(pathSanitizedJson: string): AssertionResult[] {\n return [\n {\n check: \"no real HOME path in output\",\n passed: !pathSanitizedJson.includes(\"/Users/\"),\n actual: pathSanitizedJson.includes(\"/Users/\") ? \"found\" : \"clean\",\n expected: \"no real home paths\",\n },\n {\n check: \"no checkout path in output\",\n passed: !pathSanitizedJson.includes(\"Projects/cbrain\"),\n actual: pathSanitizedJson.includes(\"Projects/cbrain\") ? \"found\" : \"clean\",\n expected: \"no checkout paths\",\n },\n {\n check: \"no credential patterns in output\",\n passed: !/sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) && !pathSanitizedJson.includes(\"Bearer \"),\n actual: /sk-[a-f0-9]{8,}/i.test(pathSanitizedJson) ? \"found\" : \"clean\",\n expected: \"no credential patterns\",\n },\n {\n check: \"no raw vector arrays in output\",\n passed: !/\"(0\\.\\d{4},){10}/.test(pathSanitizedJson),\n actual: /\"(0\\.\\d{4},){10}/.test(pathSanitizedJson) ? \"found vectors\" : \"clean\",\n expected: \"no raw vectors\",\n },\n ];\n}\n\n// ── Main Orchestrator ──\n// Returns {report, exitCode} — does NOT call process.exit().\n// ALL post-isolation work (pack, mock, stages) is inside try/catch/finally.\n// Cleanup in finally ALWAYS runs — even on pack/startup failure.\n//\n// Test-only controls (env vars):\n// GATE_PROJECT_DIR — override project dir (for isolated tests)\n// GATE_FAULT_* — inject faults at specific points\n\nasync function executeGate(): Promise<GateResult> {\n // GATE_PROJECT_DIR allows tests to point the gate at a shadow project\n // without touching the real checkout.\n const projectDir = process.env.GATE_PROJECT_DIR\n ?? resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\n const version = JSON.parse(readFileSync(join(projectDir, \"package.json\"), \"utf-8\")).version;\n const gateStart = performance.now();\n\n const ctx = createIsolation();\n let mock: MockServer | undefined;\n const stages: StageResult[] = [];\n let cleanupVerified = false;\n\n try {\n // ALL post-isolation work: pack, mock server, stage execution.\n // If ANY step throws, catch records the failure, and finally ALWAYS cleans up.\n const packResult = packAndInstall(projectDir, ctx, version);\n\n // ── Test-only fault: delete skill file after install ──\n if (process.env.GATE_FAULT_SKILL === \"1\") {\n const skillFile = join(packResult.pkgDir, \"skills\", \"SKILL.md\");\n if (existsSync(skillFile)) rmSync(skillFile, { force: true });\n }\n\n // ── Test-only fault: use unreachable provider ──\n if (process.env.GATE_FAULT_PROVIDER === \"1\") {\n mock = { url: \"http://127.0.0.1:1\", port: 1, stop() {} };\n } else {\n mock = startMockEmbeddingServer();\n }\n\n let env = buildIsolatedEnv(ctx);\n\n // ── Test-only fault: block init with pre-existing file ──\n if (process.env.GATE_FAULT_INIT === \"1\") {\n writeFileSync(ctx.brainDir, \"block-init\");\n }\n\n // Stage 1: init\n const initStage = await runStageInit(packResult.binPath, ctx, env);\n stages.push(initStage);\n\n // Subsequent stages only if init passed\n if (initStage.passed) {\n patchConfig(ctx.configPath, mock.url);\n env = buildIsolatedEnv(ctx);\n\n stages.push(await runStageDoctor(packResult.binPath, ctx, env));\n stages.push(await runStageMcpConfig(packResult.binPath, ctx, env));\n stages.push(await runStageSkillPack(packResult.binPath, ctx, env));\n\n // ── Test-only fault: skip ingest → empty brain query ──\n if (process.env.GATE_FAULT_SKIP_INGEST !== \"1\") {\n stages.push(await runStageIngest(packResult.binPath, ctx, env));\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n } else {\n // Run query on empty brain — should return no results\n stages.push(await runStageQuery(packResult.binPath, ctx, env));\n }\n\n stages.push(await runStageMigration(packResult.binPath, ctx, env, mock.url));\n }\n\n // ── Test-only fault: create runtime/ inside vault → artifacts fail ──\n if (process.env.GATE_FAULT_VAULT_NESTED === \"1\") {\n const nestedRuntime = join(ctx.brainDir, \"vault\", \"runtime\");\n mkdirSync(nestedRuntime, { recursive: true });\n writeFileSync(join(nestedRuntime, \"leak.txt\"), \"should not exist\");\n }\n\n // Artifact checks\n stages.push(makeSyntheticStage(\"artifacts\", runArtifactChecks(ctx)));\n } catch (e) {\n stages.push(\n makeSyntheticStage(\"fatal\", [\n {\n check: \"no unhandled errors\",\n passed: false,\n actual: e instanceof Error ? e.message : String(e),\n expected: \"no errors\",\n },\n ]),\n );\n } finally {\n // ── Cleanup (ALWAYS runs — even if catch itself throws) ──\n try {\n mock?.stop();\n } catch {\n /* best effort */\n }\n\n // Tarball is inside ctx.tmpdir — cleaned up with it.\n // Pre-existing tarball was already restored by packAndInstall's inner finally.\n\n if (existsSync(ctx.tmpdir)) {\n try {\n rmSync(ctx.tmpdir, { recursive: true });\n cleanupVerified = !existsSync(ctx.tmpdir);\n } catch {\n cleanupVerified = false;\n }\n } else {\n cleanupVerified = true;\n }\n }\n\n // ── Build report ──\n const allStagesPassed = stages.every((s) => s.passed);\n const rawReport: GateReport = {\n gate: \"first-recall-v2\",\n version,\n timestamp: new Date().toISOString(),\n verdict: allStagesPassed && cleanupVerified ? \"go\" : \"no-go\",\n stages,\n cleanup: { verified: cleanupVerified, path: cleanupVerified ? \"<cleaned>\" : \"<retained>\" },\n duration_ms: Math.round(performance.now() - gateStart),\n };\n\n // ── Test-only fault: inject credential into report for privacy check ──\n // This tests that the gate's privacy + redaction pipeline works end-to-end:\n // detection → redaction → no-go verdict → clean output.\n if (process.env.GATE_FAULT_MCP_CRED_LEAK === \"1\") {\n const mcpIdx = rawReport.stages.findIndex((s) => s.id === \"mcp-config\");\n if (mcpIdx >= 0) {\n const stage = rawReport.stages[mcpIdx];\n rawReport.stages[mcpIdx] = {\n ...stage,\n assertions: [\n ...stage.assertions,\n {\n check: \"simulated credential leak test\",\n passed: true,\n actual: \"sk-abcdef1234567890abcdef1234567890\",\n expected: \"no credentials in output\",\n },\n ],\n };\n }\n }\n\n // Privacy detection on path-sanitized JSON (credentials NOT yet redacted)\n const pathSanitized = sanitizePaths(JSON.stringify(rawReport, null, 2));\n const privacyAssertions = runPrivacyCheck(pathSanitized);\n const privacyPassed = privacyAssertions.every((a) => a.passed);\n\n // Full sanitization for output: paths + credential redaction\n const fullySanitized = sanitizeForOutput(JSON.stringify(rawReport, null, 2));\n\n let finalReport: GateReport;\n let finalExitCode: number;\n\n if (!privacyPassed) {\n const parsed = JSON.parse(fullySanitized);\n parsed.stages = [...parsed.stages, makeSyntheticStage(\"privacy\", privacyAssertions)];\n parsed.verdict = \"no-go\";\n finalReport = parsed as GateReport;\n finalExitCode = 1;\n } else {\n finalReport = JSON.parse(fullySanitized) as GateReport;\n finalExitCode = rawReport.verdict === \"go\" ? 0 : 1;\n }\n\n return { report: finalReport, exitCode: finalExitCode };\n}\n\n// ── Top-level: run gate → emit report → set exit code ──\n// process.exitCode (not process.exit) lets the runtime clean up naturally.\n// Outer catch handles pre-isolation fatal errors only — post-isolation errors\n// are handled by try/catch/finally inside executeGate().\n\nexecuteGate().then(({ report, exitCode }) => {\n process.stdout.write(JSON.stringify(report, null, 2) + \"\\n\");\n process.exitCode = exitCode;\n}).catch((e) => {\n // Sanitize the fatal error — never echo real paths/stacks/secrets\n let errorDetail = e instanceof Error ? e.message : String(e);\n const realHome = process.env.HOME ?? \"\";\n if (realHome) errorDetail = errorDetail.replaceAll(realHome, \"<HOME>\");\n errorDetail = errorDetail.replaceAll(\"Projects/cbrain\", \"<CHECKOUT>\");\n // Strip any remaining absolute user paths\n errorDetail = errorDetail.replace(/\\/Users\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/\\/home\\/[^\\s:\"]+/g, \"<HOME>/...\");\n errorDetail = errorDetail.replace(/sk-[a-f0-9]{8,}/gi, \"<REDACTED>\");\n // Strip stack traces — only keep the first line\n errorDetail = errorDetail.split(\"\\n\")[0];\n\n const errorReport = {\n gate: \"first-recall-v2\",\n verdict: \"no-go\",\n error: errorDetail,\n cleanup: { verified: false, path: \"<unknown>\" },\n };\n process.stdout.write(JSON.stringify(errorReport, null, 2) + \"\\n\");\n process.exitCode = 2;\n});\n"}}}}