diff --git a/.gitignore b/.gitignore index 8cafa1a..cd5e08c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ # Runtime state and local databases .refiner/ **/.refiner/ +.claude/worktrees/ +**/.claude/worktrees/ .gemini/memory.json **/.gemini/memory.json **/.gemini/blackboard.json @@ -19,6 +21,10 @@ .env .env.* !.env.example +.gemini-refiner.json +.universal-refiner.json +**/.gemini-refiner.json +**/.universal-refiner.json # Editor and OS artifacts .vscode/ diff --git a/README.md b/README.md index 989de9f..513830d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ That distinction matters because this repo is about credible system direction, n ```mermaid flowchart LR - CLI["AI CLI\n(Claude / Cursor)"] -->|"stdio"| PI["PromptImprover\n(gemini-prompt-refiner)"] + CLI["AI CLI\n(Claude / Cursor)"] -->|"stdio"| PI["PromptImprover\n(universal-refiner)"] subgraph internal["PromptImprover Engine"] RAG["RAG Snippets\n(FlexSearch)"] Memory["SQLite Memory\n(LocalBrain)"] @@ -77,7 +77,7 @@ cd Promptimprover ./build_and_install.sh ``` -Both installers perform a deterministic dependency install, run the full test suite, build the package, install it globally, and verify the `gemini-prompt-refiner` command. Add that command to your MCP client configuration. See the [Setup Guide](https://github.com/Coding-Autopilot-System/Promptimprover/wiki/Setup-Guide) for full configuration instructions. +Both installers perform a deterministic dependency install, run the full test suite, build the package, install it globally, and verify the `universal-refiner` command. Add that command to your MCP client configuration. See the [Setup Guide](https://github.com/Coding-Autopilot-System/Promptimprover/wiki/Setup-Guide) for full configuration instructions. For optional automatic pre-prompt linting and post-execution recording, see the [cross-CLI automation guide](./docs/cross-cli-automation.md). Claude Code and Gemini CLI expose the required lifecycle hooks. Codex currently requires MCP-first instructions or explicit helper invocation because its hook lifecycle does not transparently intercept each prompt. @@ -85,7 +85,7 @@ For optional automatic pre-prompt linting and post-execution recording, see the PromptImprover uses a local OpenAI-compatible endpoint before optional MCP sampling. The safe defaults target `http://localhost:9000/v1`, use `gemma3:12b` first, and fall back to `gemma3:1b`. If neither local model nor MCP sampling is available, rule-based refinement continues without semantic output. -Override the defaults per repository with `.gemini-refiner.json`: +Override the defaults per repository with `.universal-refiner.json`: ```json { diff --git a/build_and_install.ps1 b/build_and_install.ps1 index 45881f1..a21b836 100644 --- a/build_and_install.ps1 +++ b/build_and_install.ps1 @@ -13,9 +13,9 @@ try { npm install --global . --no-fund $package = Get-Content .\package.json -Raw | ConvertFrom-Json - $command = Get-Command gemini-prompt-refiner -ErrorAction Stop + $command = Get-Command universal-refiner -ErrorAction Stop Write-Host "Prompt Refiner v$($package.version) installed: $($command.Source)" -ForegroundColor Green } finally { Pop-Location -} \ No newline at end of file +} diff --git a/build_and_install.sh b/build_and_install.sh index 2d57c53..e506972 100755 --- a/build_and_install.sh +++ b/build_and_install.sh @@ -11,6 +11,6 @@ npm test npm run build npm install --global . --no-fund -command -v gemini-prompt-refiner >/dev/null 2>&1 +command -v universal-refiner >/dev/null 2>&1 VERSION=$(node -p "require('./package.json').version") -printf 'Prompt Refiner v%s installed: %s\n' "$VERSION" "$(command -v gemini-prompt-refiner)" \ No newline at end of file +printf 'Prompt Refiner v%s installed: %s\n' "$VERSION" "$(command -v universal-refiner)" \ No newline at end of file diff --git a/docs/cross-cli-automation.md b/docs/cross-cli-automation.md index 29ed0d5..e5daa83 100644 --- a/docs/cross-cli-automation.md +++ b/docs/cross-cli-automation.md @@ -5,7 +5,7 @@ PromptImprover ships fail-open pre-prompt and post-execution helpers: - `promptimprover-hook-pre` makes one latency-safe rule-based `lint_prompt` call, creates a trackable prompt ID, and injects advisory context. Interactive MCP linting continues to use semantic providers by default. - `promptimprover-hook-post` records privacy-safe completion metadata with `record_agent_output`. -Both commands read hook JSON from stdin, write JSON only to stdout, report failures to stderr, and always allow the client to continue. They start the same built MCP server used by `gemini-prompt-refiner`. Set `PROMPTIMPROVER_SERVER_PATH` only when testing a nonstandard build. +Both commands read hook JSON from stdin, write JSON only to stdout, report failures to stderr, and always allow the client to continue. They start the same built MCP server used by `universal-refiner`. Set `PROMPTIMPROVER_SERVER_PATH` only when testing a nonstandard build. The helpers store only prompt ID, client name, and creation time in the OS temporary directory. They do not persist prompt or response bodies. Completion records contain output length rather than response text. diff --git a/docs/operator-testing.md b/docs/operator-testing.md index 7e9ddf8..708a732 100644 --- a/docs/operator-testing.md +++ b/docs/operator-testing.md @@ -127,7 +127,7 @@ npm.cmd run acceptance:package-runtime Expected result: ```text -Package runtime smoke passed: installed gemini-prompt-refiner-8.0.0 and served /api/health on . +Package runtime smoke passed: installed universal-refiner-8.0.0 and served /api/health on . ``` This catches missing production dependencies that are hidden by the local workspace. diff --git a/docs/portfolio-proof.md b/docs/portfolio-proof.md index 21aef9f..e41a8f7 100644 --- a/docs/portfolio-proof.md +++ b/docs/portfolio-proof.md @@ -4,7 +4,7 @@ This repo is best read as an enterprise-oriented MCP and prompt-governance proto ## Present-State Evidence -- `universal-refiner/package.json` defines the active package as `gemini-prompt-refiner` and describes it as cross-CLI prompt refinement using an MCP server. +- `universal-refiner/package.json` defines the active package as `universal-refiner` and describes it as cross-CLI prompt refinement using an MCP server. - `universal-refiner/tests/` contains targeted Vitest coverage for detectors, history, lessons, memory, snippets, predictive refinement, timeline behavior, and server behavior. - `build_and_install.ps1` installs the `universal-refiner` package globally as `prompt-refiner`, showing the intended operator entry point. diff --git a/universal-refiner/.gemini/settings.json b/universal-refiner/.gemini/settings.json index ed9ef66..81b4c21 100644 --- a/universal-refiner/.gemini/settings.json +++ b/universal-refiner/.gemini/settings.json @@ -1,7 +1,7 @@ { "mcpServers": { "prompt-refiner": { - "command": "gemini-prompt-refiner", + "command": "universal-refiner", "args": [] } } diff --git a/universal-refiner/.universal-refiner.example.json b/universal-refiner/.universal-refiner.example.json new file mode 100644 index 0000000..68bb6a6 --- /dev/null +++ b/universal-refiner/.universal-refiner.example.json @@ -0,0 +1,18 @@ +{ + "_comment_usage": "Copy this file to .universal-refiner.json in the universal-refiner directory. ConfigManager reads .universal-refiner.json at server startup; restart the MCP server after changes.", + "_comment_fields": { + "semantic.localEnabled": "true = LocalOpenAiProvider is tried first. false = skip local model entirely.", + "semantic.baseUrl": "OpenAI-compatible /v1 base URL. Ollama default: http://localhost:11434/v1. LM Studio default: http://localhost:1234/v1.", + "semantic.models": "Model names to try in order. First reachable model wins. Use exact Ollama model tags.", + "semantic.mcpSamplingEnabled": "true = fall back to MCP sampling if local model fails or is unreachable.", + "semantic.allowNonLoopback": "Set to true only if baseUrl is a non-loopback host. Leave false for localhost." + }, + "semantic": { + "localEnabled": true, + "baseUrl": "http://localhost:11434/v1", + "models": ["gemma3:12b", "gemma3"], + "mcpSamplingEnabled": true, + "timeoutMs": 120000, + "temperature": 0.2 + } +} diff --git a/universal-refiner/README.md b/universal-refiner/README.md new file mode 100644 index 0000000..a75fa49 --- /dev/null +++ b/universal-refiner/README.md @@ -0,0 +1,111 @@ +# Universal Refiner + +MCP server for prompt governance. Provides the `refine_prompt` tool that enriches +prompts with project mandates, agentic context, and semantic refinement via a local +model or MCP sampling fallback. + +## Quick Start + +```powershell +# From the universal-refiner directory +npm run build +node dist/src/index.js +``` + +The server registers as an MCP stdio transport. Global registration is managed by +`scripts/operations/register-global.ps1`. + +## Local Model Configuration + +`refine_prompt` routes semantic refinement through `LocalOpenAiProvider` first (tier-0), +then falls back to `McpSamplingProvider` if the local model is unreachable or returns an error. + +Configuration is read from `.universal-refiner.json` in the working directory at **server startup**. +After changing this file you must **restart the MCP server process** for the change to take effect. + +### Wiring Ollama / Gemma + +1. Start Ollama with the Gemma model: + + ```powershell + ollama pull gemma3:12b + ollama serve + ``` + + Ollama listens at `http://localhost:11434` by default. + +2. Create `.universal-refiner.json` in `universal-refiner/`: + + ```json + { + "semantic": { + "localEnabled": true, + "baseUrl": "http://localhost:11434/v1", + "models": ["gemma3:12b", "gemma3"], + "mcpSamplingEnabled": true + } + } + ``` + + Copy `.universal-refiner.example.json` as a starting point. + +3. Restart the MCP server. + +### Config Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `semantic.localEnabled` | boolean | `true` | Enable `LocalOpenAiProvider` as tier-0 | +| `semantic.baseUrl` | string | `http://localhost:9000/v1` | OpenAI-compatible `/v1` base URL. **Ollama default is port 11434, not 9000.** | +| `semantic.models` | string[] | `["gemma3:12b", "gemma3:1b"]` | Model names tried in order. First reachable model wins. | +| `semantic.mcpSamplingEnabled` | boolean | `true` | Fall back to MCP sampling if local model fails | +| `semantic.timeoutMs` | number | `120000` | Request timeout in milliseconds | +| `semantic.temperature` | number | `0.2` | Sampling temperature (0–2) | +| `semantic.allowNonLoopback` | boolean | `false` | Must be `true` for non-loopback base URLs (e.g., remote server). Leave `false` for localhost. | + +> **Important:** The hardcoded default `baseUrl` is port 9000, not 11434. Ollama serves on +> port 11434. Without a `.universal-refiner.json` overriding `baseUrl`, `LocalOpenAiProvider` +> will silently fail to connect and fall through to MCP sampling. + +### LM Studio + +LM Studio exposes the same OpenAI-compatible `/v1` API. Use: + +```json +{ + "semantic": { + "localEnabled": true, + "baseUrl": "http://localhost:1234/v1", + "models": ["gemma-3-12b-it"] + } +} +``` + +### Provider Chain + +When a `refine_prompt` call arrives: + +1. `LocalOpenAiProvider` is tried first (if `localEnabled: true`). + - Iterates `models` in order. On model failure, moves to the next model. + - Returns `null` if all models fail (triggers fallback). +2. `McpSamplingProvider` is tried next (if `mcpSamplingEnabled: true`). +3. If both fail, `refine_prompt` returns the original prompt unchanged. + +## Configuration File Reference + +See `.universal-refiner.example.json` for an annotated template. + +## Release Gate + +```powershell +npm run release:verify +``` + +Runs build, 100% test coverage, MCP acceptance, semantic fallback, stress/soak, and audit checks. + +## Security + +- Never commit `.universal-refiner.json` if it contains sensitive values. + Add it to `.gitignore` if you customise it beyond the example defaults. +- `allowNonLoopback: false` (default) prevents the local provider from contacting + non-loopback hosts, limiting the blast radius of a misconfigured `baseUrl`. diff --git a/universal-refiner/package-lock.json b/universal-refiner/package-lock.json index 25934ee..b3db2ee 100644 --- a/universal-refiner/package-lock.json +++ b/universal-refiner/package-lock.json @@ -1,11 +1,11 @@ { - "name": "gemini-prompt-refiner", + "name": "universal-refiner", "version": "8.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "gemini-prompt-refiner", + "name": "universal-refiner", "version": "8.0.0", "license": "MIT", "dependencies": { @@ -19,9 +19,9 @@ "zod": "^4.3.6" }, "bin": { - "gemini-prompt-refiner": "dist/src/index.js", "promptimprover-hook-post": "dist/hooks/post-execution.js", - "promptimprover-hook-pre": "dist/hooks/pre-prompt.js" + "promptimprover-hook-pre": "dist/hooks/pre-prompt.js", + "universal-refiner": "dist/src/index.js" }, "devDependencies": { "@emnapi/core": "^1.11.1", diff --git a/universal-refiner/package.json b/universal-refiner/package.json index 6a756f8..01381f6 100644 --- a/universal-refiner/package.json +++ b/universal-refiner/package.json @@ -1,10 +1,10 @@ { - "name": "gemini-prompt-refiner", + "name": "universal-refiner", "version": "8.0.0", "description": "Cross-CLI prompt refinement using an MCP server.", "main": "dist/src/index.js", "bin": { - "gemini-prompt-refiner": "./dist/src/index.js", + "universal-refiner": "./dist/src/index.js", "promptimprover-hook-pre": "./dist/hooks/pre-prompt.js", "promptimprover-hook-post": "./dist/hooks/post-execution.js" }, diff --git a/universal-refiner/scripts/acceptance/package-runtime-smoke.mjs b/universal-refiner/scripts/acceptance/package-runtime-smoke.mjs index dd33fc3..5e0b6e0 100644 --- a/universal-refiner/scripts/acceptance/package-runtime-smoke.mjs +++ b/universal-refiner/scripts/acceptance/package-runtime-smoke.mjs @@ -30,8 +30,8 @@ try { await runNpm(["install", "--global", "--prefix", prefixDir, "--no-fund", tarball]); const bin = process.platform === "win32" - ? join(prefixDir, "gemini-prompt-refiner.cmd") - : join(prefixDir, "bin", "gemini-prompt-refiner"); + ? join(prefixDir, "universal-refiner.cmd") + : join(prefixDir, "bin", "universal-refiner"); const packageRoot = await findInstalledPackageRoot(prefixDir); const installedEntry = join(packageRoot, "dist", "src", "index.js"); await access(bin); @@ -116,12 +116,12 @@ function runNpm(args) { async function findInstalledPackageRoot(prefixDir) { const candidates = process.platform === "win32" ? [ - join(prefixDir, "node_modules", "gemini-prompt-refiner"), - join(prefixDir, "lib", "node_modules", "gemini-prompt-refiner"), + join(prefixDir, "node_modules", "universal-refiner"), + join(prefixDir, "lib", "node_modules", "universal-refiner"), ] : [ - join(prefixDir, "lib", "node_modules", "gemini-prompt-refiner"), - join(prefixDir, "node_modules", "gemini-prompt-refiner"), + join(prefixDir, "lib", "node_modules", "universal-refiner"), + join(prefixDir, "node_modules", "universal-refiner"), ]; for (const candidate of candidates) { diff --git a/universal-refiner/src/core/config.ts b/universal-refiner/src/core/config.ts index 68d6af3..0610eee 100644 --- a/universal-refiner/src/core/config.ts +++ b/universal-refiner/src/core/config.ts @@ -19,7 +19,8 @@ export interface SemanticConfig { } export class ConfigManager { - private static CONFIG_FILE = ".gemini-refiner.json"; + private static CONFIG_FILE = ".universal-refiner.json"; + private static LEGACY_CONFIG_FILE = ".gemini-refiner.json"; private static DEFAULT_SEMANTIC_CONFIG: SemanticConfig = { localEnabled: true, mcpSamplingEnabled: true, @@ -31,7 +32,7 @@ export class ConfigManager { }; static loadConfig(rootPath: string = "."): RefinerConfig { - const configPath = path.join(rootPath, this.CONFIG_FILE); + const configPath = this.resolveConfigPath(rootPath); if (!fs.existsSync(configPath)) { return {}; } @@ -45,6 +46,28 @@ export class ConfigManager { } } + private static resolveConfigPath(rootPath: string): string { + const configPath = path.join(rootPath, this.CONFIG_FILE); + if (fs.existsSync(configPath)) { + return configPath; + } + + const legacyPath = path.join(rootPath, this.LEGACY_CONFIG_FILE); + if (fs.existsSync(legacyPath)) { + console.warn(`${this.LEGACY_CONFIG_FILE} is deprecated; rename it to ${this.CONFIG_FILE}.`); + return legacyPath; + } + + return configPath; + } + + static mergeConfig(rootPath: string = ".", updates: Partial): void { + const configPath = path.join(rootPath, this.CONFIG_FILE); + const current = this.loadConfig(rootPath); + const merged = { ...current, ...updates }; + fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8"); + } + static getSemanticConfig(rootPath: string = "."): SemanticConfig { const semantic = this.loadConfig(rootPath).semantic || {}; const defaults = this.DEFAULT_SEMANTIC_CONFIG; diff --git a/universal-refiner/src/core/semantic-provider.ts b/universal-refiner/src/core/semantic-provider.ts index 716c45c..faf7cbc 100644 --- a/universal-refiner/src/core/semantic-provider.ts +++ b/universal-refiner/src/core/semantic-provider.ts @@ -67,6 +67,7 @@ export class LocalOpenAiProvider implements SemanticProvider { stream: false, temperature: this.options.temperature, max_tokens: request.maxTokens, + keep_alive: -1, }), signal: AbortSignal.timeout(this.options.timeoutMs), }); diff --git a/universal-refiner/src/history/timeline.ts b/universal-refiner/src/history/timeline.ts index b22d929..48a3b16 100644 --- a/universal-refiner/src/history/timeline.ts +++ b/universal-refiner/src/history/timeline.ts @@ -6,6 +6,8 @@ export interface TimelineEntry { timestamp: string; summary: string; author?: string; + event_type?: string; + severity?: string; details?: any; } @@ -35,12 +37,12 @@ export class TimelineProvider { // Filter out prompt_recorded events because we already have the prompt record itself const events = db.prepare(` - SELECT 'log' as type, id, timestamp, summary, event_type as author, details_json as details - FROM events - WHERE event_type NOT IN ('prompt_recorded', 'prompt_processed') - ORDER BY timestamp DESC - LIMIT ? - `).all(limit); + SELECT 'log' as type, id, timestamp, summary, event_type as author, event_type, severity, details_json as details + FROM events + WHERE event_type NOT IN ('prompt_recorded', 'prompt_processed') + ORDER BY timestamp DESC + LIMIT ? + `).all(limit); const unified: TimelineEntry[] = [ ...prompts.map((p: any) => ({ ...p, details: { intent: p.details } })), diff --git a/universal-refiner/src/memory/neural-snippets.ts b/universal-refiner/src/memory/neural-snippets.ts index 4296ffa..7185fd5 100644 --- a/universal-refiner/src/memory/neural-snippets.ts +++ b/universal-refiner/src/memory/neural-snippets.ts @@ -38,14 +38,18 @@ export class NeuralSnippets { const files = fs.readdirSync(dir, { withFileTypes: true }); for (const file of files) { const name = path.join(dir, file.name); - const stat = fs.lstatSync(name); - if (stat.isSymbolicLink()) continue; - - const canonicalName = fs.realpathSync.native(name); - if (!this.isWithinRoot(root, canonicalName)) continue; + let canonicalName: string; + try { + const stat = fs.lstatSync(name); + if (stat.isSymbolicLink()) continue; + canonicalName = fs.realpathSync.native(name); + if (!this.isWithinRoot(root, canonicalName)) continue; + } catch (err) { + continue; + } if (file.isDirectory()) { - const ignoreDirs = ["node_modules", "dist", "build", "out", "coverage", "tests", "test"]; + const ignoreDirs = ["node_modules", "dist", "build", "out", "coverage", "tests", "test", ".git", ".pytest_cache", ".venv", ".sandbox", "tmp"]; if (!ignoreDirs.includes(file.name) && !file.name.startsWith(".")) { await this.walkDir(canonicalName, root, fileList); } diff --git a/universal-refiner/tests/config.test.ts b/universal-refiner/tests/config.test.ts index 8bc486a..7993099 100644 --- a/universal-refiner/tests/config.test.ts +++ b/universal-refiner/tests/config.test.ts @@ -17,11 +17,11 @@ describe("ConfigManager", () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it("should load mandates from .gemini-refiner.json", () => { + it("should load mandates from .universal-refiner.json", () => { const config = { mandates: ["Always use tabs", "Write JSDoc for all functions"] }; - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify(config)); + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), JSON.stringify(config)); const loaded = ConfigManager.loadConfig(tmpDir); expect(loaded.mandates).toContain("Always use tabs"); @@ -33,6 +33,41 @@ describe("ConfigManager", () => { expect(loaded).toEqual({}); }); + it("falls back to legacy .gemini-refiner.json with a deprecation warning", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ + mandates: ["legacy mandate"], + semantic: { models: ["legacy-model"] }, + })); + + expect(ConfigManager.loadConfig(tmpDir).mandates).toEqual(["legacy mandate"]); + expect(ConfigManager.getSemanticConfig(tmpDir).models).toEqual(["legacy-model"]); + expect(warn).toHaveBeenCalledWith(expect.stringContaining(".gemini-refiner.json is deprecated")); + }); + + it("prefers .universal-refiner.json over legacy config when both exist", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); + fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ mandates: ["legacy"] })); + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), JSON.stringify({ mandates: ["current"] })); + + expect(ConfigManager.loadConfig(tmpDir).mandates).toEqual(["current"]); + expect(warn).not.toHaveBeenCalled(); + }); + + it("mergeConfig writes the new filename while preserving loaded legacy config", () => { + vi.spyOn(console, "warn").mockImplementation(() => undefined); + fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ + mandates: ["legacy"], + })); + + ConfigManager.mergeConfig(tmpDir, { ignoredPaths: ["dist"] }); + + expect(JSON.parse(fs.readFileSync(path.join(tmpDir, ".universal-refiner.json"), "utf8"))).toEqual({ + mandates: ["legacy"], + ignoredPaths: ["dist"], + }); + }); + it("should use quality-first local semantic defaults", () => { const config = ConfigManager.getSemanticConfig(tmpDir); expect(config.baseUrl).toBe("http://localhost:9000/v1"); @@ -41,7 +76,7 @@ describe("ConfigManager", () => { }); it("should merge semantic overrides with safe defaults", () => { - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), JSON.stringify({ semantic: { models: ["gemma3:1b"], timeoutMs: 5000 } })); @@ -52,7 +87,7 @@ describe("ConfigManager", () => { }); it("should reject malformed semantic overrides", () => { - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), JSON.stringify({ semantic: { models: [42], timeoutMs: -1, temperature: 99, baseUrl: "" } })); @@ -64,7 +99,7 @@ describe("ConfigManager", () => { }); it("returns an empty config and reports invalid JSON", () => { - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), "{"); + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), "{"); const error = vi.spyOn(console, "error").mockImplementation(() => undefined); expect(ConfigManager.loadConfig(tmpDir)).toEqual({}); @@ -72,7 +107,7 @@ describe("ConfigManager", () => { }); it("accepts all bounded semantic overrides", () => { - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), JSON.stringify({ + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), JSON.stringify({ semantic: { localEnabled: false, mcpSamplingEnabled: false, diff --git a/universal-refiner/tests/coverage-gaps.test.ts b/universal-refiner/tests/coverage-gaps.test.ts index 7c6b572..39812fa 100644 --- a/universal-refiner/tests/coverage-gaps.test.ts +++ b/universal-refiner/tests/coverage-gaps.test.ts @@ -17,7 +17,7 @@ describe("ConfigManager gap coverage", () => { it("returns an empty object when the repository config contains invalid JSON", async () => { const { ConfigManager } = await import("../src/core/config.js"); - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), "{ NOT VALID JSON }"); + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), "{ NOT VALID JSON }"); expect(ConfigManager.loadConfig(tmpDir)).toEqual({}); }); diff --git a/universal-refiner/tests/dashboard-api.test.ts b/universal-refiner/tests/dashboard-api.test.ts index d3bc3a5..cdb65a6 100644 --- a/universal-refiner/tests/dashboard-api.test.ts +++ b/universal-refiner/tests/dashboard-api.test.ts @@ -116,7 +116,7 @@ describe("dashboard review and health APIs", () => { }); it("returns sanitized semantic provider and runtime health", async () => { - fs.writeFileSync(path.join(repoDir, ".gemini-refiner.json"), JSON.stringify({ + fs.writeFileSync(path.join(repoDir, ".universal-refiner.json"), JSON.stringify({ semantic: { baseUrl: "http://secret-user:secret-pass@localhost:9000/v1?token=secret", models: ["gemma3:12b"], diff --git a/universal-refiner/tests/snippets.test.ts b/universal-refiner/tests/snippets.test.ts index 0ff5905..d5b4081 100644 --- a/universal-refiner/tests/snippets.test.ts +++ b/universal-refiner/tests/snippets.test.ts @@ -1,5 +1,5 @@ // secret-scan: allow-fixture -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; @@ -98,4 +98,24 @@ def helper(): fs.rmSync(outside, { recursive: true, force: true }); } }); + + it("skips entries when canonical path resolution fails", async () => { + const safeFile = path.join(tmpDir, "safe.ts"); + const brokenFile = path.join(tmpDir, "broken.ts"); + fs.writeFileSync(safeFile, "export function safeResolution() {}"); + fs.writeFileSync(brokenFile, "export function brokenResolution() {}"); + + const originalRealpath = fs.realpathSync.native; + vi.spyOn(fs.realpathSync, "native").mockImplementation((target) => { + if (String(target).endsWith("broken.ts")) { + throw new Error("transient filesystem error"); + } + return originalRealpath(target); + }); + + await NeuralSnippets.initialize(tmpDir); + + expect(await NeuralSnippets.search("safeResolution", tmpDir)).toMatchObject([{ symbolName: "safeResolution" }]); + expect(await NeuralSnippets.search("brokenResolution", tmpDir)).toEqual([]); + }); }); diff --git a/universal-refiner/tests/write-gaps.cjs b/universal-refiner/tests/write-gaps.cjs index 39b5cfa..730c539 100644 --- a/universal-refiner/tests/write-gaps.cjs +++ b/universal-refiner/tests/write-gaps.cjs @@ -21,7 +21,7 @@ describe("ConfigManager gap coverage", () => { it("returns an empty object when the repository config contains invalid JSON", async () => { const { ConfigManager } = await import("../src/core/config.js"); - fs.writeFileSync(path.join(tmpDir, ".gemini-refiner.json"), "{ NOT VALID JSON }"); + fs.writeFileSync(path.join(tmpDir, ".universal-refiner.json"), "{ NOT VALID JSON }"); expect(ConfigManager.loadConfig(tmpDir)).toEqual({}); });