Files
Dragan Spiridonov 5d187de2a1 fix(install,rvf,hypergraph): unblock Windows install, fix RVF FsyncFailed, synthesize covers edges
Bundles fixes for issue #439 (Windows install) and Jordi RUFLO patches
P020 (RVF init) + P220 (hypergraph blindness).

Windows install (#439):
- Move hnswlib-node from dependencies to optionalDependencies so
  installs no longer fail on Windows boxes without VS C++ Build Tools;
  HnswAdapter already routes around a missing native binary at runtime.
- Replace HNSWIndex.ts top-level static `import hnswlib from
  'hnswlib-node'` with a lazy require so package load survives the
  optional dep being absent.
- README + preinstall.cjs: document toolchain requirement and disclose
  that the JS fallback degrades to O(N) brute-force when @ruvector/gnn
  is also unavailable (Windows default — no win32 prebuilds ship).
- ADR-090 amendment reconciling the move with ADR-081's optional-dep +
  JS-fallback intent.

RVF init (Jordi P020):
- Root cause: shared-rvf-adapter and pattern-store factory always called
  RvfDatabase.create(), which throws `0x0303 FsyncFailed` when the .rvf
  file already exists from a prior init phase. Reproduced and confirmed
  against both 0.1.7 and 0.1.8 native binaries — not a version
  regression.
- Fix: race-tolerant open-or-create ladder (try open → fall back to
  create → retry open on create-race) with dim() verification after
  open. Mismatched-dim files are closed and the caller degrades rather
  than corrupting silently.

Hypergraph (Jordi P220):
- Drop the original flushEdgesToHypergraph (wrote `call`/`extends`
  edges that no production query reads — devil's-advocate review caught
  it as dead infrastructure).
- Add HypergraphEngine.synthesizeTestCoverage that re-tags test files
  type='test' and writes `covers` edges from each test file to
  functions in the source files it imports. This is the shape
  findUntestedFunctions / findImpactedTests actually filter on.
- E2E verified: untested correctly excludes covered functions;
  impacted-tests returns the right test file.

Verification:
- `npm run build` clean
- 276/276 tests pass across HNSW, RVF, KG, coordinator, hypergraph
  suites (5 new synthesizeTestCoverage tests; replaced 4 obsolete
  flush tests)
- E2E on fresh project: aqe init clean, hooks stats shows
  nativeAvailable: true, code index logs `Test coverage synthesized`,
  hg untested + hg impacted return useful results

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:22:58 +00:00

315 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Preinstall script for agentic-qe
* Detects and handles conflicts from:
* - agentic-qe v2 (old package)
* - @agentic-qe/v3 (alpha package)
*
* Uses CommonJS for maximum Node.js compatibility
*/
const { execSync, spawnSync } = require('child_process');
const { existsSync, unlinkSync, lstatSync } = require('fs');
const { join } = require('path');
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const YELLOW = '\x1b[33m';
const GREEN = '\x1b[32m';
const CYAN = '\x1b[36m';
const RED = '\x1b[31m';
function log(msg) {
console.log(`${CYAN}[agentic-qe]${RESET} ${msg}`);
}
function warn(msg) {
console.log(`${YELLOW}[agentic-qe]${RESET} ${msg}`);
}
function error(msg) {
console.log(`${RED}[agentic-qe]${RESET} ${msg}`);
}
function success(msg) {
console.log(`${GREEN}[agentic-qe]${RESET} ${msg}`);
}
function findExistingAqeBinary() {
// Try 'which' command first
try {
const result = spawnSync('which', ['aqe'], { encoding: 'utf-8' });
if (result.status === 0 && result.stdout.trim()) {
return result.stdout.trim();
}
} catch {
// which command failed
}
// Try to find via npm prefix
try {
const prefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
const binPath = join(prefix, 'bin', 'aqe');
if (existsSync(binPath)) {
return binPath;
}
} catch {
// Ignore
}
return null;
}
function checkPackageInstalled(packageName) {
try {
const result = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
return result.includes(`${packageName}@`);
} catch {
return false;
}
}
function getPackageVersion(packageName) {
try {
const result = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
const regex = new RegExp(`${packageName.replace('/', '\\/')}@([\\d\\.\\-a-z]+)`);
const match = result.match(regex);
return match ? match[1] : null;
} catch {
return null;
}
}
function uninstallPackage(packageName) {
try {
log(`Uninstalling ${packageName}...`);
execSync(`npm uninstall -g ${packageName}`, {
encoding: 'utf-8',
stdio: 'inherit'
});
success(`Successfully uninstalled ${packageName}`);
return true;
} catch (err) {
error(`Failed to uninstall ${packageName}: ${err.message}`);
return false;
}
}
function removeStaleSymlink(binaryPath) {
try {
const stats = lstatSync(binaryPath);
if (stats.isSymbolicLink()) {
unlinkSync(binaryPath);
success(`Removed stale symlink: ${binaryPath}`);
return true;
}
} catch {
// Not a symlink or can't remove
}
return false;
}
function emitWindowsToolchainNotice() {
// Only show on Windows. Optional native dep `hnswlib-node` compiles from
// source via node-gyp and needs a C++ toolchain to install. If it fails
// the install still succeeds (it's optional) — this notice just tells the
// user what to install if they want the faster native HNSW backend.
// See README "Windows install" section and ADR-090 amendment 2026-05-08.
if (process.platform !== 'win32') return;
if (process.env.AQE_SKIP_WINDOWS_NOTICE === 'true') return;
console.log('');
log(`${BOLD}Windows install notice${RESET}`);
console.log(
` agentic-qe will attempt to compile the optional native HNSW backend`
);
console.log(` (hnswlib-node) during install. This requires:`);
console.log('');
console.log(` - ${BOLD}Python 3${RESET} on PATH, and`);
console.log(
` - ${BOLD}Visual Studio 2022 Build Tools${RESET} with the` +
` 'Desktop development with C++' workload, OR`
);
console.log(
` - ${BOLD}Visual Studio 2026${RESET} with the same workload AND` +
` ${BOLD}npm >= 11.6.3${RESET}`
);
console.log(` (run: ${CYAN}npm install -g npm@latest${RESET})`);
console.log('');
console.log(
` ${YELLOW}If the native build fails, install still succeeds${RESET} —`
);
console.log(
` AQE falls back to a pure-JS HNSW backend at runtime. That fallback`
);
console.log(
` is correct but degrades to O(N) brute-force when @ruvector/gnn is`
);
console.log(
` also unavailable (the default on Windows — no win32 prebuilds`
);
console.log(
` ship). Fine for small projects; for codebases with tens of`
);
console.log(
` thousands of vectors or more, install the toolchain.`
);
console.log('');
console.log(
` Set ${CYAN}AQE_SKIP_WINDOWS_NOTICE=true${RESET} to suppress this notice.`
);
console.log('');
}
function main() {
// Skip in CI environments unless explicitly requested
if (process.env.CI && !process.env.AQE_PREINSTALL_CHECK) {
return;
}
emitWindowsToolchainNotice();
const aqeBinary = findExistingAqeBinary();
// Check for both old package names
const v2Installed = checkPackageInstalled('agentic-qe');
const alphaInstalled = checkPackageInstalled('@agentic-qe/v3');
const v2Version = v2Installed ? getPackageVersion('agentic-qe') : null;
const alphaVersion = alphaInstalled ? getPackageVersion('@agentic-qe/v3') : null;
// If the currently installed agentic-qe is version 3.x, we're good (user is upgrading within v3)
if (v2Version && v2Version.startsWith('3.')) {
// User is upgrading from 3.x to newer 3.x - this should work normally
return;
}
if (!aqeBinary && !v2Installed && !alphaInstalled) {
// Clean install, nothing to do
return;
}
console.log('');
log(`${BOLD}Checking for existing agentic-qe installation...${RESET}`);
// Handle alpha package conflict
if (alphaInstalled) {
warn(`Found @agentic-qe/v3@${alphaVersion || 'alpha'} installed globally`);
warn('The alpha package uses the same "aqe" binary name as the released version');
console.log('');
// Check if we can auto-migrate
const autoMigrate = process.env.AQE_AUTO_MIGRATE === 'true' ||
process.env.npm_config_yes === 'true';
if (autoMigrate) {
log('Auto-migrating from alpha to release...');
if (uninstallPackage('@agentic-qe/v3')) {
success('Migration complete. Continuing with install...');
return;
} else {
error('Auto-migration failed. Please run manually:');
console.log(` ${CYAN}npm uninstall -g @agentic-qe/v3 && npm install -g agentic-qe@latest${RESET}`);
process.exit(1);
}
}
// Interactive mode
console.log(`${YELLOW}To upgrade from alpha to release:${RESET}`);
console.log('');
console.log(` ${BOLD}Option 1: Auto-migrate (recommended)${RESET}`);
console.log(` ${CYAN}AQE_AUTO_MIGRATE=true npm install -g agentic-qe@latest${RESET}`);
console.log('');
console.log(` ${BOLD}Option 2: Manual uninstall first${RESET}`);
console.log(` ${CYAN}npm uninstall -g @agentic-qe/v3${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest${RESET}`);
console.log('');
console.log(` ${BOLD}Option 3: Force overwrite${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest --force${RESET}`);
console.log('');
process.exit(1);
}
// Handle v2 package conflict
if (v2Installed && !v2Version?.startsWith('3.')) {
warn(`Found agentic-qe v${v2Version || '2.x'} installed globally`);
warn('The v2 package uses the same "aqe" binary name as v3');
console.log('');
const autoMigrate = process.env.AQE_AUTO_MIGRATE === 'true' ||
process.env.npm_config_yes === 'true';
if (autoMigrate) {
log('Auto-migrating from v2 to v3...');
if (uninstallPackage('agentic-qe')) {
success('Migration complete. Continuing with v3 install...');
return;
} else {
error('Auto-migration failed. Please run manually:');
console.log(` ${CYAN}npm uninstall -g agentic-qe && npm install -g agentic-qe@latest${RESET}`);
process.exit(1);
}
}
console.log(`${YELLOW}To upgrade from v2 to v3:${RESET}`);
console.log('');
console.log(` ${BOLD}Option 1: Auto-migrate (recommended)${RESET}`);
console.log(` ${CYAN}AQE_AUTO_MIGRATE=true npm install -g agentic-qe@latest${RESET}`);
console.log('');
console.log(` ${BOLD}Option 2: Manual uninstall first${RESET}`);
console.log(` ${CYAN}npm uninstall -g agentic-qe${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest${RESET}`);
console.log('');
console.log(` ${BOLD}Option 3: Force overwrite${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest --force${RESET}`);
console.log('');
process.exit(1);
} else if (aqeBinary && !v2Installed && !alphaInstalled) {
// Binary exists but no known package found - stale symlink or different package
warn(`Found existing 'aqe' binary at: ${aqeBinary}`);
// Try to identify what it's from
try {
const realpath = require('fs').realpathSync(aqeBinary);
if (realpath.includes('agentic-qe')) {
warn('This appears to be from a previous agentic-qe installation.');
}
} catch {
// Can't resolve realpath
}
console.log('');
console.log(`${YELLOW}To resolve the conflict:${RESET}`);
console.log('');
console.log(` ${BOLD}Option 1: Remove existing binary${RESET}`);
console.log(` ${CYAN}rm ${aqeBinary}${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest${RESET}`);
console.log('');
console.log(` ${BOLD}Option 2: Force overwrite${RESET}`);
console.log(` ${CYAN}npm install -g agentic-qe@latest --force${RESET}`);
console.log('');
process.exit(1);
}
}
try {
main();
} catch (err) {
// Don't let preinstall failures block installation
// Just log and continue
if (process.env.DEBUG) {
error(`Preinstall check error: ${err.message}`);
}
}